diff --git a/.github/workflows/ci-v2.yaml b/.github/workflows/ci-v2.yaml index c736496..b17e125 100644 --- a/.github/workflows/ci-v2.yaml +++ b/.github/workflows/ci-v2.yaml @@ -1,9 +1,14 @@ name: CI v2 # NO paths filter (the adversarial review's high finding): a PR touching only -# corvus-sdk or the Makefile got ZERO CI — the blind spot. The suite is fast -# (the test ~16s, the smoke ~5s): every PR and main push runs everything. +# corvus-sdk or the Makefile got ZERO CI — the blind spot. The suite is fast: +# every PR and main push runs everything. # workflow_dispatch stays as the manual re-run lever for any sha. +# +# EVERY component runs its own harness. The live three (v2, sdk, cli) run a real +# build > lint > test cycle; the legacy/config three run their guards. The +# interpreter is pinned to 3.12 here and in the Makefiles — a mismatch changes the +# coverage number and used to hide behind a 7-point tolerance. on: push: branches: [main] @@ -11,7 +16,36 @@ on: workflow_dispatch: jobs: - test: + harness: + name: harness (${{ matrix.component }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + component: + - corvus-v2 + - corvus-sdk + - corvus-cli + - corvus-server + - corvus-hooks + - corvus-splunk + defaults: + run: + working-directory: ${{ matrix.component }} + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + - name: Setup (pinned venv + deps) + run: make setup + - name: Build > Lint > Test + run: make cycle + + phase-gates: + # The P1 capability gate, actually gated: BUILD-PLAN.md requires 'the + # conformance suite runs green in CI with the mutation evidence'. At HEAD + # neither ran in CI — they were manual targets. They are manual no longer. runs-on: ubuntu-latest defaults: run: @@ -21,32 +55,17 @@ jobs: - uses: actions/setup-python@v6 with: python-version: "3.12" - - name: Install - run: | - python -m venv .venv - . .venv/bin/activate - pip install -r requirements.txt - pip install pytest pytest-cov httpx ruff==0.17.1 - - name: Lint - run: | - . .venv/bin/activate - ruff check corvus/ tests/ - - name: Tests with coverage - run: | - . .venv/bin/activate - pytest tests/ -v --cov=corvus --cov-report=term --cov-report=json:coverage.json --junitxml=junit.xml - - name: The metrics drift guard - run: | - . .venv/bin/activate - python scripts/gen_metrics.py --coverage coverage.json --junit junit.xml \ - --out /tmp/metrics-check.json --source "$GITHUB_SHA" - python scripts/check_metrics.py --expected ../docs/canonical-path/dev-metrics.json \ - --actual /tmp/metrics-check.json - - name: Coverage summary - run: | - . .venv/bin/activate - python -c 'import json; c = json.load(open("coverage.json")); t = c["totals"]; print("coverage: %.1f%% (%d/%d lines)" % (t["percent_covered"], t["covered_lines"], t["num_statements"]))' + - name: Setup + run: make setup + - name: Mutation evidence + run: make mutation + - name: Conformance run + run: make conformance + smoke: + # Kept from the original gate: the components install and import cleanly. + # Each component's build step now proves this too, but this job stays as the + # independent cross-component check that a packaging change cannot silently break. runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 diff --git a/Makefile b/Makefile index e952af1..f46ff52 100644 --- a/Makefile +++ b/Makefile @@ -1,66 +1,82 @@ -# The Corvus dev harness — the Build > Test > Fix cycle, one command per step. -# `make cycle` is the full loop; every step is idempotent and fresh-clone safe. - -VENV := corvus-v2/.venv -PY := $(VENV)/bin/python -PIP := $(VENV)/bin/pip -METRICS := docs/canonical-path/dev-metrics.json - -.PHONY: setup lint test metrics run check cycle clean - -setup: ## Bootstrap the venv + the deps (fresh-clone safe) - python3 -m venv $(VENV) - $(PIP) install --quiet --upgrade pip - $(PIP) install --quiet -r corvus-v2/requirements.txt - $(PIP) install --quiet pytest pytest-cov ruff httpx - @echo "setup: ok" - -lint: ## Ruff over the v2 package and the tests - $(PY) -m ruff check corvus-v2/corvus/ corvus-v2/tests/ - @echo "lint: ok" - -test: ## The tests with the coverage measurement - cd corvus-v2 && .venv/bin/python -m pytest tests/ -q \ - --cov=corvus --cov-report=term --cov-report=json:coverage.json \ - --junitxml=junit.xml - @echo "test: ok" - -metrics: ## Regenerate the dev-metrics artifact from the last test run - $(PY) corvus-v2/scripts/gen_metrics.py \ - --coverage corvus-v2/coverage.json --junit corvus-v2/junit.xml --out $(METRICS) \ - --source "$$(git rev-parse --short HEAD)" --duration "$$(cd corvus-v2 && .venv/bin/python -c "import json;print(json.load(open('coverage.json')).get('duration',0) if False else 0)")" - @echo "metrics: regenerated" - -metrics-check: ## The drift guard: the committed metrics must match the fresh run - $(PY) corvus-v2/scripts/gen_metrics.py \ - --coverage corvus-v2/coverage.json --junit corvus-v2/junit.xml --out /tmp/metrics-check.json \ - --source "$$(git rev-parse --short HEAD)" - $(PY) corvus-v2/scripts/check_metrics.py --expected $(METRICS) --actual /tmp/metrics-check.json - -run: ## The server up + the live health check (the dev loop's "build" step) - cd corvus-v2 && .venv/bin/python -c "\ -import sys, threading, time, json, urllib.request;\ -sys.path.insert(0, '.');\ -from corvus.app import create_app;\ -from corvus.config import FleetConfig;\ -cfg = FleetConfig(name='dev-fleet', node_id='dev-1', api_key='dev-local-key');\ -app = create_app(config=cfg, db_path=':memory:');\ -t = threading.Thread(target=lambda: __import__('uvicorn').run(app, host='127.0.0.1', port=8932, log_level='error'), daemon=True);\ -t.start(); time.sleep(1.5);\ -r = json.loads(urllib.request.urlopen(urllib.request.Request('http://127.0.0.1:8932/health', headers={'X-Corvus-Key': 'dev-key'}), timeout=5).read());\ -assert r['status'] == 'ok', r;\ -print('run: ok —', r['fleet'])" - -check: lint test metrics-check ## The gates the CI runs (identical locally) - -cycle: check run ## THE Build > Test > Fix loop, one command - -clean: ## The artifacts - rm -rf $(VENV) corvus-v2/.pytest_cache corvus-v2/coverage.json /tmp/metrics-check.json - -# WP14: the mutation evidence + the conformance run (the phase-gate targets) -mutation: - cd corvus-v2 && .venv/bin/python -c "import sys; sys.path.insert(0, '.'); from corvus.conformance import run_mutation_suite; import json; r = run_mutation_suite(); print(json.dumps(r, indent=1)); exit(0 if r['all_mutants_caught'] else 1)" - -conformance: - cd corvus-v2 && .venv/bin/python -m corvus.engine_shim --port 8933 & sleep 2; .venv/bin/python -c "import sys; sys.path.insert(0, '.'); from corvus.conformance import conformance_run; import json; r = conformance_run('http://127.0.0.1:8933'); print(json.dumps(r, indent=1)); exit(0 if r['failed'] == 0 else 1)"; pkill -f "engine_shim --port 8933" || true +# The Corvus dev harness — the Build > Test > Fix cycle for EVERY component. +# Each component owns its cycle in /Makefile; this file is the umbrella +# that runs them all, so "does the repo work?" has one answer. +# +# Live components (real harnesses): corvus-v2, corvus-sdk, corvus-cli +# Legacy/config guards: corvus-server, corvus-hooks, corvus-splunk +# v1 is archived at branch v1-archive and is NOT deployed; its guards exist only +# so archived code cannot silently rot. No tests are resurrected for it. +# +# The component lists below are NOT cosmetic: the legacy three do not declare +# build/test (nothing to build or test), and corvus-splunk has no lint/fix (it is +# config, not Python). Looping a target over a component that lacks it aborts the +# whole umbrella with "No rule to make target", so each target runs over exactly +# the components that define it. +# +# The interpreter is PINNED to python3.12 (CI uses 3.12). Bare `python3` on this +# host is 3.15, and running the suite on it changes the coverage number enough to +# matter — that mismatch is what the pin removes. + +PYTHON ?= python3.12 + +# Live three: the full harness. +LIVE := corvus-v2 corvus-sdk corvus-cli +# Everything: setup, cycle and clean exist everywhere. +ALL := $(LIVE) corvus-server corvus-hooks corvus-splunk +# Python components: they lint and autofix. corvus-splunk is config (validate only). +PY := $(LIVE) corvus-server corvus-hooks + +V2 := corvus-v2 + +export PYTHON + +.PHONY: help setup build lint test fix cycle check clean metrics metrics-check run mutation conformance + +help: ## This list + @grep -E '^[a-z-]+:.*##' $(MAKEFILE_LIST) | sed 's/:.*##/ —/' + +setup: ## Bootstrap every component's venv + deps (fresh-clone safe) + @for c in $(ALL); do printf '\n== %s ==\n' "$$c"; $(MAKE) -C $$c setup || exit 1; done + @echo "\nsetup: all components ok" + +build: ## The live three's build proof (the legacy guards have nothing to build) + @for c in $(LIVE); do printf '\n== %s ==\n' "$$c"; $(MAKE) -C $$c build || exit 1; done + @echo "\nbuild: all live components ok" + +lint: ## Ruff over every Python component + @for c in $(PY); do printf '\n== %s ==\n' "$$c"; $(MAKE) -C $$c lint || exit 1; done + @echo "\nlint: all Python components ok" + +test: ## The live three's tests (the legacy guards have no tests by design) + @for c in $(LIVE); do printf '\n== %s ==\n' "$$c"; $(MAKE) -C $$c test || exit 1; done + @echo "\ntest: all live components ok" + +fix: ## The autofix step across every Python component + @for c in $(PY); do printf '\n== %s ==\n' "$$c"; $(MAKE) -C $$c fix || exit 1; done + @echo "\nfix: all Python components ok — run `make lint` to confirm" + +cycle: setup ## THE loop: setup, then every component's build > lint > test + @for c in $(ALL); do printf '\n===== %s =====\n' "$$c"; $(MAKE) -C $$c cycle || exit 1; done + @echo "\ncycle: ALL COMPONENTS OK" + +check: cycle ## The full gate set (what CI runs) — identical to cycle + +clean: ## Remove every component's regenerable state + @for c in $(ALL); do $(MAKE) -C $$c clean; done + @echo "clean: ok — run `make setup` to rebuild" + +# ── v2-specific targets, kept at the root for continuity ──────────────────── +metrics: ## Regenerate the committed dev-metrics artifact + $(MAKE) -C $(V2) metrics + +metrics-check: ## The metrics drift guard + $(MAKE) -C $(V2) metrics-check + +run: ## The engine up + the live proof + $(MAKE) -C $(V2) build + +mutation: ## The mutation evidence (the phase-gate target) + $(MAKE) -C $(V2) mutation + +conformance: ## The conformance run (the phase-gate target) + $(MAKE) -C $(V2) conformance diff --git a/corvus-cli/.gitignore b/corvus-cli/.gitignore new file mode 100644 index 0000000..7c5cc03 --- /dev/null +++ b/corvus-cli/.gitignore @@ -0,0 +1,9 @@ +# corvus-cli harness artifacts (component-local: the root .gitignore is not ours to edit) +.venv/ +.pytest_cache/ +.ruff_cache/ +.coverage +coverage.json +junit.xml +__pycache__/ +*.egg-info/ diff --git a/corvus-cli/Makefile b/corvus-cli/Makefile new file mode 100644 index 0000000..21a0767 --- /dev/null +++ b/corvus-cli/Makefile @@ -0,0 +1,47 @@ +# corvus-cli dev harness — the Build > Test > Fix cycle, one command per step. +# Every target is idempotent and fresh-clone safe; run from this directory (make -C corvus-cli ...). + +PYTHON ?= python3.12 +VENV := .venv +PY := $(VENV)/bin/python +PIP := $(VENV)/bin/pip +RUFF := $(VENV)/bin/ruff + +.PHONY: setup build lint test fix cycle clean + +setup: ## Create the 3.12 venv, install the deps and the pinned tooling + @test -d $(VENV) || $(PYTHON) -m venv $(VENV) + $(PIP) install --quiet --upgrade pip + $(PIP) install --quiet -e . + $(PIP) install --quiet ruff==0.17.1 pytest pytest-cov + @echo "setup: ok" + +build: setup ## Editable install + proof the entrypoint dispatches + $(PIP) install --quiet -e . + $(PIP) check # the resolved dependency set must be internally consistent + $(PY) -c "import corvus_cli.main as m; assert m.app is not None; print('import: ok')" + $(VENV)/bin/corvus --help > /dev/null + $(VENV)/bin/corvus check-target --help > /dev/null + @echo "build: ok" + +lint: ## Ruff over the package and the tests + $(RUFF) check src/ tests/ + @echo "lint: ok" + +test: ## The tests with coverage + junit evidence + $(PY) -m pytest tests/ -q --cov=corvus_cli --cov-report=term \ + --cov-report=json:coverage.json --junitxml=junit.xml + @echo "test: ok" + +fix: ## Apply the ruff autofixes (no `ruff format` — the repo does not use it) + $(RUFF) check --fix src/ tests/ + @echo "fix: ok" + +cycle: build lint test ## The one command: build, lint, test + +# clean removes only artifacts this harness generates (venv, caches, reports); nothing tracked +# or hand-written is in that list. PREFLIGHT:rollback-written — the rollback is `make setup && make cycle`. +clean: ## Drop the venv and the test artifacts + rm -rf $(VENV) .pytest_cache .ruff_cache src/corvus_cli/__pycache__ tests/__pycache__ + rm -f .coverage coverage.json junit.xml + @echo "clean: ok" diff --git a/corvus-cli/pyproject.toml b/corvus-cli/pyproject.toml index 3624fdd..281a257 100644 --- a/corvus-cli/pyproject.toml +++ b/corvus-cli/pyproject.toml @@ -3,7 +3,9 @@ name = "corvus-cli" version = "0.1.0" requires-python = ">=3.11" description = "CLI for the Corvus operational governance API" -dependencies = ["httpx>=0.27", "typer>=0.15", "rich>=13"] +# Floors are the versions the harness tested (python3.12), capped at the next +# MAJOR so a breaking release fails at install time rather than silently. +dependencies = ["httpx>=0.28,<1.0", "typer>=0.27,<1.0", "rich>=15,<16"] [project.scripts] corvus = "corvus_cli.main:app" diff --git a/corvus-cli/tests/conftest.py b/corvus-cli/tests/conftest.py new file mode 100644 index 0000000..a661cca --- /dev/null +++ b/corvus-cli/tests/conftest.py @@ -0,0 +1,82 @@ +"""Fixtures for the CLI suite: a hermetic environment and an in-memory HTTP transport. + +No test ever opens a socket - every httpx.Client the CLI builds gets a MockTransport. +""" + +from __future__ import annotations + +import json +from dataclasses import dataclass, field +from typing import Any + +import httpx +import pytest + +from corvus_cli import main + + +@pytest.fixture(autouse=True) +def isolated_env(monkeypatch, tmp_path): + """Hermetic env: no real ~/.corvus.json and no inherited CORVUS_* variables.""" + monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.delenv("CORVUS_URL", raising=False) + monkeypatch.delenv("CORVUS_TOKEN", raising=False) + return tmp_path + + +@dataclass +class Call: + """One request the CLI actually sent.""" + + method: str + url: str + path: str + headers: dict + params: dict + body: Any = None + + +@dataclass +class FakeAPI: + """A route table plus a record of what was requested.""" + + routes: dict = field(default_factory=dict) + calls: list = field(default_factory=list) + + def on(self, method: str, path: str, payload: Any, status: int = 200) -> FakeAPI: + self.routes[(method.upper(), path)] = (status, payload) + return self + + def handler(self, request: httpx.Request) -> httpx.Response: + self.calls.append( + Call( + method=request.method, + url=str(request.url), + path=request.url.path, + headers=dict(request.headers), + params=dict(request.url.params), + body=json.loads(request.content) if request.content else None, + ) + ) + status, payload = self.routes.get((request.method, request.url.path), (404, {"detail": "no route"})) + return httpx.Response(status, json=payload) + + def call(self, method: str, path: str) -> Call: + """The one recorded call to (method, path).""" + matches = [c for c in self.calls if c.method == method.upper() and c.path == path] + assert len(matches) == 1, f"expected exactly one {method} {path}, recorded {self.calls}" + return matches[0] + + +@pytest.fixture +def api(monkeypatch) -> FakeAPI: + """Route every httpx.Client through an in-memory transport (never a live server).""" + fake = FakeAPI() + real_client = httpx.Client + + def factory(*args, **kwargs): + kwargs["transport"] = httpx.MockTransport(fake.handler) + return real_client(*args, **kwargs) + + monkeypatch.setattr(main.httpx, "Client", factory) + return fake diff --git a/corvus-cli/tests/test_config.py b/corvus-cli/tests/test_config.py new file mode 100644 index 0000000..ae5603d --- /dev/null +++ b/corvus-cli/tests/test_config.py @@ -0,0 +1,76 @@ +"""config.py - resolution order is env var, then ~/.corvus.json, then the default.""" + +from __future__ import annotations + +import json + +from corvus_cli import config + + +def _write_config(home, payload: dict) -> None: + (home / ".corvus.json").write_text(json.dumps(payload)) + + +def test_base_url_defaults_to_localhost_when_nothing_configured(): + assert config.get_base_url() == "http://localhost:8000" + + +def test_base_url_comes_from_env(monkeypatch): + monkeypatch.setenv("CORVUS_URL", "http://corvus.test:9420/") + assert config.get_base_url() == "http://corvus.test:9420" + + +def test_base_url_strips_every_trailing_slash(monkeypatch): + monkeypatch.setenv("CORVUS_URL", "http://corvus.test:9420///") + assert config.get_base_url() == "http://corvus.test:9420" + + +def test_base_url_comes_from_config_file(isolated_env): + _write_config(isolated_env, {"url": "https://corvus.example/"}) + assert config.get_base_url() == "https://corvus.example" + + +def test_base_url_config_file_without_url_key_falls_back(isolated_env): + _write_config(isolated_env, {"token": "t"}) + assert config.get_base_url() == "http://localhost:8000" + + +def test_base_url_env_wins_over_config_file(monkeypatch, isolated_env): + _write_config(isolated_env, {"url": "https://from-file"}) + monkeypatch.setenv("CORVUS_URL", "https://from-env") + assert config.get_base_url() == "https://from-env" + + +def test_empty_env_url_falls_through_to_config_file(monkeypatch, isolated_env): + _write_config(isolated_env, {"url": "https://from-file"}) + monkeypatch.setenv("CORVUS_URL", "") + assert config.get_base_url() == "https://from-file" + + +def test_token_comes_from_env(monkeypatch): + monkeypatch.setenv("CORVUS_TOKEN", "env-token") + assert config.get_token() == "env-token" + + +def test_token_comes_from_config_file(isolated_env): + _write_config(isolated_env, {"token": "file-token"}) + assert config.get_token() == "file-token" + + +def test_token_defaults_to_empty_string(): + assert config.get_token() == "" + + +def test_token_env_wins_over_config_file(monkeypatch, isolated_env): + _write_config(isolated_env, {"token": "file-token"}) + monkeypatch.setenv("CORVUS_TOKEN", "env-token") + assert config.get_token() == "env-token" + + +def test_load_config_file_returns_empty_dict_when_absent(): + assert config._load_config_file() == {} + + +def test_load_config_file_reads_the_json(isolated_env): + _write_config(isolated_env, {"url": "https://corvus.example", "token": "abc"}) + assert config._load_config_file() == {"url": "https://corvus.example", "token": "abc"} diff --git a/corvus-cli/tests/test_main.py b/corvus-cli/tests/test_main.py new file mode 100644 index 0000000..39c5a62 --- /dev/null +++ b/corvus-cli/tests/test_main.py @@ -0,0 +1,428 @@ +"""main.py - every command exercised through typer's CliRunner against a mocked HTTP layer.""" + +from __future__ import annotations + +import runpy +import sys +import warnings + +import pytest +from typer.testing import CliRunner + +from corvus_cli.main import app + +runner = CliRunner() + + +def run(*args: str): + """Invoke the CLI and fail loudly on a non-zero exit.""" + result = runner.invoke(app, list(args)) + assert result.exit_code == 0, f"exit {result.exit_code}: {result.stdout} {result.stderr}" + return result + + +# -- Pre-action checks -- + + +def test_check_target_reports_recommendation_and_counts(api): + api.on("GET", "/ops/events/targets/caddy/status", { + "recommendation": "GO", + "active_changes": [{}, {}], + "recent_incidents": [{}], + }) + out = run("check-target", "caddy").stdout + assert "GO" in out + assert "caddy" in out + assert "Active changes: 2" in out + assert "Recent incidents: 1" in out + + +def test_check_target_unknown_recommendation_has_no_count_lines(api): + api.on("GET", "/ops/events/targets/caddy/status", {}) + out = run("check-target", "caddy").stdout + assert "UNKNOWN" in out + assert "Active changes" not in out + assert "Recent incidents" not in out + + +def test_unrouted_request_exits_1_and_names_the_status(api): + result = runner.invoke(app, ["metrics"]) + assert result.exit_code == 1 + assert "Error 404" in result.stdout + + +def test_server_error_body_is_printed_and_exits_1(api): + api.on("GET", "/ops/events/context", {"detail": "boom"}, status=500) + result = runner.invoke(app, ["context"]) + assert result.exit_code == 1 + assert "Error 500" in result.stdout + assert "boom" in result.stdout + + +def test_blast_radius_with_no_dependencies(api): + api.on("GET", "/ops/graph/blast-radius/redis", {"affected": []}) + out = run("blast-radius", "redis").stdout + assert "redis" in out + assert "No downstream dependencies found" in out + + +def test_blast_radius_renders_a_table_and_handles_missing_fields(api): + api.on("GET", "/ops/graph/blast-radius/caddy", { + "affected": [ + {"name": "redis", "host": "tmtdockp01", "depth": 2}, + {"name": "db"}, + ], + }) + out = run("blast-radius", "caddy").stdout + assert "Blast Radius: caddy" in out + assert "redis" in out + assert "tmtdockp01" in out + assert "db" in out + assert "?" in out + + +def test_dependency_chain_prints_the_payload(api): + api.on("GET", "/ops/graph/dependency-chain/caddy", {"service": "caddy", "upstream": ["dns", "proxy"]}) + out = run("dependency-chain", "caddy").stdout + assert "caddy" in out + assert "upstream" in out + assert "dns" in out + + +# -- Context -- + + +def test_context_summarises_events_incidents_changes_and_gaps(api): + api.on("GET", "/ops/events/context", { + "events_24h": [{}, {}, {}], + "active_incidents": [ + {"id": "INC-1", "target": "caddy", "severity": "critical", "title": "Down"}, + ], + "active_changes": [{}], + "gaps": {"total_open_gaps": 7}, + }) + out = run("context").stdout + assert "Events (24h): 3" in out + assert "Active incidents: 1" in out + assert "Active changes: 1" in out + assert "Open gaps: 7" in out + assert "INC-1" in out + assert "critical" in out + + +def test_context_with_no_incidents_skips_the_table(api): + api.on("GET", "/ops/events/context", {"events_24h": [], "active_incidents": [], "active_changes": []}) + out = run("context").stdout + assert "Open gaps: 0" in out + assert "Active Incidents" not in out + + +# -- Changes -- + + +def test_create_change_splits_comma_separated_targets(api): + api.on("POST", "/ops/changes", {"id": "CHG-1"}) + out = run("create-change", "caddy, redis ,db", "-d", "drain the pool").stdout + assert "Change created: CHG-1" in out + body = api.call("POST", "/ops/changes").body + assert body["targets"] == ["caddy", "redis", "db"] + assert body["description"] == "drain the pool" + assert body["created_by"] == "corvus-cli" + assert body["rollback_plan"] == "" + assert body["project"] == "" + + +def test_create_change_sends_the_optional_fields(api): + api.on("POST", "/ops/changes", {"id": "CHG-2"}) + run( + "create-change", "caddy", "-d", "drain", + "--operator", "todd", "--rollback", "restart caddy", "--project", "ai", + ) + body = api.call("POST", "/ops/changes").body + assert body["created_by"] == "todd" + assert body["rollback_plan"] == "restart caddy" + assert body["project"] == "ai" + + +def test_close_change_defaults_to_completed(api): + api.on("PATCH", "/ops/changes/CHG-1", {"id": "CHG-1"}) + out = run("close-change", "CHG-1").stdout + assert "Change closed: CHG-1" in out + assert api.call("PATCH", "/ops/changes/CHG-1").body == {"status": "completed", "outcome": "completed"} + + +def test_close_change_uses_the_given_outcome(api): + api.on("PATCH", "/ops/changes/CHG-9", {"id": "CHG-9"}) + run("close-change", "CHG-9", "--outcome", "failed") + assert api.call("PATCH", "/ops/changes/CHG-9").body["outcome"] == "failed" + + +def test_changes_with_none_active(api): + api.on("GET", "/ops/changes/active", []) + assert "No active changes" in run("changes").stdout + + +def test_changes_parses_targets_given_as_a_json_string(api): + api.on("GET", "/ops/changes/active", [ + {"id": "CHG-1", "targets": '["caddy", "redis"]', "description": "drain", "created_by": "todd"}, + {"id": "CHG-2", "targets": ["db"], "description": "restart", "created_by": "bot"}, + ]) + out = run("changes").stdout + assert "CHG-1" in out + assert "caddy, redis" in out + assert "CHG-2" in out + assert "db" in out + + +# -- Events -- + + +def test_emit_event_sends_the_payload(api): + api.on("POST", "/ops/events", {"id": "EVT-1"}) + out = run( + "emit-event", "-s", "agent", "-t", "change.started", + "--target", "proxy", "--severity", "warning", + ).stdout + assert "Event emitted: EVT-1" in out + assert api.call("POST", "/ops/events").body == { + "source": "agent", + "type": "change.started", + "target": "proxy", + "severity": "warning", + "data": {}, + } + + +def test_emit_event_defaults_target_and_severity(api): + api.on("POST", "/ops/events", {"id": "EVT-2"}) + run("emit-event", "--source", "agent", "--type", "session.started") + body = api.call("POST", "/ops/events").body + assert body["target"] == "" + assert body["severity"] == "info" + + +# -- Incidents -- + + +def test_incidents_with_none_found_sends_no_filters(api): + api.on("GET", "/ops/incidents", []) + assert "No incidents found" in run("incidents").stdout + assert api.call("GET", "/ops/incidents").params == {} + + +def test_incidents_sends_every_filter_as_a_param(api): + api.on("GET", "/ops/incidents", []) + run("incidents", "--status", "open", "--target", "caddy", "--severity", "critical") + assert api.call("GET", "/ops/incidents").params == { + "status": "open", + "target": "caddy", + "severity": "critical", + } + + +def test_incidents_renders_a_table(api): + api.on("GET", "/ops/incidents", [ + {"id": "INC-1", "target": "caddy", "severity": "critical", "status": "open", "title": "Down"}, + ]) + out = run("incidents").stdout + assert "Incidents" in out + assert "INC-1" in out + assert "caddy" in out + assert "open" in out + + +def test_create_incident_sends_the_payload(api): + api.on("POST", "/ops/incidents", {"id": "INC-2"}) + out = run( + "create-incident", "caddy", "-t", "Down", + "--description", "502s", "--severity", "critical", "--detected-by", "agent", + ).stdout + assert "Incident created: INC-2" in out + assert api.call("POST", "/ops/incidents").body == { + "target": "caddy", + "title": "Down", + "description": "502s", + "severity": "critical", + "detected_by": "agent", + } + + +# -- CMDB -- + + +def test_services_with_none_registered(api): + api.on("GET", "/ops/cmdb", []) + assert "No services found" in run("services").stdout + assert api.call("GET", "/ops/cmdb").params == {} + + +def test_services_filters_and_marks_critical(api): + api.on("GET", "/ops/cmdb", [ + {"name": "caddy", "service_type": "proxy", "host": "tmtdockp01", "critical": True}, + {"name": "db", "critical": False}, + ]) + out = run("services", "--service-type", "proxy", "--host", "tmtdockp01").stdout + assert api.call("GET", "/ops/cmdb").params == {"service_type": "proxy", "host": "tmtdockp01"} + assert "caddy" in out + assert "🔴" in out + assert "db" in out + assert "-" in out + + +def test_service_prints_the_details(api): + api.on("GET", "/ops/cmdb/caddy", {"name": "caddy", "critical": True, "host": "tmtdockp01"}) + out = run("service", "caddy").stdout + assert "caddy" in out + assert "critical" in out + assert "tmtdockp01" in out + + +# -- Triage -- + + +def test_triage_renders_the_full_diagnosis(api): + api.on("POST", "/ops/runbooks/triage", { + "status": "triaged", + "triage_id": "TRG-1", + "diagnosis": "port 443 closed", + "confidence": 0.87, + "escalation_required": True, + }) + out = run("triage", "caddy", "--host", "tmtdockp01", "--service-type", "proxy").stdout + assert "triaged — TRG-1" in out + assert "Diagnosis: port 443 closed" in out + assert "Confidence: 87%" in out + assert "Escalation required" in out + assert api.call("POST", "/ops/runbooks/triage").body == { + "target": "caddy", + "host": "tmtdockp01", + "service_type": "proxy", + } + + +def test_triage_minimal_response(api): + api.on("POST", "/ops/runbooks/triage", {}) + out = run("triage", "caddy").stdout + assert "unknown — ?" in out + assert "Diagnosis" not in out + assert "Escalation" not in out + + +# -- Metrics, gaps, signal quality -- + + +def test_metrics_reports_every_field(api): + api.on("GET", "/ops/metrics", { + "events_24h": 42, + "open_incidents": 2, + "active_changes": 1, + "false_positive_rate": 3.5, + "compliance_rate": 91.2, + "total_services": 12, + }) + out = run("metrics").stdout + assert "Events (24h): 42" in out + assert "Open incidents: 2" in out + assert "Active changes: 1" in out + assert "FP rate: 3.5%" in out + assert "Compliance rate: 91.2%" in out + assert "Total services: 12" in out + + +def test_metrics_defaults_to_zero(api): + api.on("GET", "/ops/metrics", {}) + out = run("metrics").stdout + assert "Events (24h): 0" in out + assert "FP rate: 0%" in out + + +def test_gaps_lists_the_workstream_breakdown(api): + api.on("GET", "/ops/gaps", {"total_open_gaps": 5, "by_workstream": {"accuracy": 2, "coverage": 3}}) + out = run("gaps").stdout + assert "Open gaps: 5" in out + assert "accuracy: 2" in out + assert "coverage: 3" in out + + +def test_gaps_without_a_workstream_breakdown(api): + api.on("GET", "/ops/gaps", {"total_open_gaps": 0}) + out = run("gaps").stdout + assert "Open gaps: 0" in out + + +def test_signal_quality_defaults_to_seven_days(api): + api.on("GET", "/ops/signal-quality", { + "total_resolved": 10, + "false_positives": 1, + "false_positive_rate": 10.0, + }) + out = run("signal-quality").stdout + assert "Total resolved (7d): 10" in out + assert "False positives: 1" in out + assert "FP rate: 10.0%" in out + assert api.call("GET", "/ops/signal-quality").params == {"days": "7"} + + +def test_signal_quality_honours_a_custom_window(api): + api.on("GET", "/ops/signal-quality", {"total_resolved": 4}) + out = run("signal-quality", "--days", "30").stdout + assert "Total resolved (30d): 4" in out + assert api.call("GET", "/ops/signal-quality").params == {"days": "30"} + + +# -- The HTTP client -- + + +def test_bearer_token_is_sent_when_configured(api, monkeypatch): + monkeypatch.setenv("CORVUS_TOKEN", "secret-token") + api.on("GET", "/ops/metrics", {}) + run("metrics") + assert api.call("GET", "/ops/metrics").headers.get("authorization") == "Bearer secret-token" + + +def test_no_authorization_header_without_a_token(api): + api.on("GET", "/ops/metrics", {}) + run("metrics") + assert "authorization" not in api.call("GET", "/ops/metrics").headers + + +def test_base_url_from_the_environment_reaches_the_request(api, monkeypatch): + monkeypatch.setenv("CORVUS_URL", "http://corvus.test:9420/") + api.on("GET", "/ops/events/context", {}) + run("context") + assert api.call("GET", "/ops/events/context").url == "http://corvus.test:9420/ops/events/context" + + +# -- The entrypoint -- + + +def test_help_lists_every_command(): + out = run("--help").stdout + for name in ( + "check-target", + "blast-radius", + "dependency-chain", + "context", + "create-change", + "close-change", + "changes", + "emit-event", + "incidents", + "create-incident", + "services", + "service", + "triage", + "metrics", + "gaps", + "signal-quality", + ): + assert name in out, f"{name} missing from --help" + + +def test_module_runs_as_main(monkeypatch): + monkeypatch.setattr(sys, "argv", ["corvus", "--help"]) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", RuntimeWarning) + with pytest.raises(SystemExit) as exc: + runpy.run_module("corvus_cli.main", run_name="__main__") + assert exc.value.code == 0 diff --git a/corvus-hooks/.gitignore b/corvus-hooks/.gitignore new file mode 100644 index 0000000..8b5fb9a --- /dev/null +++ b/corvus-hooks/.gitignore @@ -0,0 +1,5 @@ +# Component-local artifacts (the root .gitignore is owned elsewhere) +.venv/ +__pycache__/ +*.pyc +.ruff_cache/ diff --git a/corvus-hooks/Makefile b/corvus-hooks/Makefile new file mode 100644 index 0000000..4b3f054 --- /dev/null +++ b/corvus-hooks/Makefile @@ -0,0 +1,48 @@ +# The corvus-hooks guard — legacy v1 hooks must lint and import cleanly. +# +# v1 is ARCHIVED (branch v1-archive) and NOT deployed. This is rot protection, not +# a test suite: there are deliberately no tests here. The hooks are stdlib-only +# (json/os/sys/re/subprocess/urllib/platform/typing) so setup installs tooling only. +# +# Most files are scripts with hyphens in the name (corvus-governance.py, ...) and so +# are not importable as modules; corvus_core.py is the one real importable module. + +PYTHON ?= python3.12 +VENV := .venv +PY := $(VENV)/bin/python +PIP := $(VENV)/bin/pip +RUFF := $(VENV)/bin/ruff + +.PHONY: setup lint import fix cycle clean + +# Auto-bootstrap: any target that needs the venv triggers setup exactly once, so +# `make cycle` is genuinely the one command on a fresh clone (this is a real file +# target, not .PHONY — it is a no-op while .venv/bin/ruff exists). +$(VENV)/bin/ruff: + $(MAKE) setup + +setup: ## Bootstrap the pinned 3.12 venv + tooling (the hooks need no third-party deps) + $(PYTHON) -m venv $(VENV) + $(PIP) install --quiet --upgrade pip + $(PIP) install --quiet ruff==0.17.1 pytest pytest-cov + @echo "setup: ok — hooks are stdlib-only, no requirements file to install" + +lint: $(VENV)/bin/ruff ## Ruff over every hook, adapter and the shared core (config in pyproject.toml) + $(RUFF) check . + +import: $(VENV)/bin/ruff ## The real proof: byte-compile every script, then import the shared core + $(PY) -c "import pathlib, py_compile; \ +files = sorted(p for p in pathlib.Path('.').rglob('*.py') if '.venv' not in p.parts); \ +[py_compile.compile(str(p), doraise=True) for p in files]; \ +print(f'import: byte-compiled {len(files)} scripts')" + $(PY) -c "import sys; sys.path.insert(0, '.'); import corvus_core as c; \ +assert all(hasattr(c, n) for n in ('check_target', 'emit_event', 'classify_intent', 'is_destructive_mcp_tool')); \ +print('import: ok — corvus_core imported, public API present')" + +fix: $(VENV)/bin/ruff ## Ruff autofixes (no ruff format — this repo does not use it) + $(RUFF) check --fix . + +cycle: lint import ## The guard, one command + +clean: ## The venv + the lint/byte-compile artifacts + rm -rf $(VENV) .ruff_cache __pycache__ adapters/__pycache__ diff --git a/corvus-hooks/adapters/cline_hooks.py b/corvus-hooks/adapters/cline_hooks.py index 589cdbb..1a154bc 100644 --- a/corvus-hooks/adapters/cline_hooks.py +++ b/corvus-hooks/adapters/cline_hooks.py @@ -45,7 +45,6 @@ import os import sys - sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import corvus_core as core @@ -200,7 +199,7 @@ def main(): else: print(f"Unknown mode: {mode}", file=sys.stderr) sys.exit(1) - except (json.JSONDecodeError, IOError): + except (OSError, json.JSONDecodeError): respond() diff --git a/corvus-hooks/adapters/codex_hooks.py b/corvus-hooks/adapters/codex_hooks.py index 8ca59f2..391ac9b 100644 --- a/corvus-hooks/adapters/codex_hooks.py +++ b/corvus-hooks/adapters/codex_hooks.py @@ -32,7 +32,6 @@ import os import sys - # Add parent dir to path for corvus_core import sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import corvus_core as core @@ -162,7 +161,7 @@ def main(): else: print(f"Unknown mode: {mode}", file=sys.stderr) sys.exit(1) - except (json.JSONDecodeError, IOError): + except (OSError, json.JSONDecodeError): sys.exit(0) diff --git a/corvus-hooks/adapters/windsurf_hooks.py b/corvus-hooks/adapters/windsurf_hooks.py index 94350f2..180018f 100644 --- a/corvus-hooks/adapters/windsurf_hooks.py +++ b/corvus-hooks/adapters/windsurf_hooks.py @@ -44,7 +44,6 @@ import os import sys - sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import corvus_core as core @@ -129,7 +128,7 @@ def cmd_stdin_check(): """Read JSON from stdin (for pipe-based integration).""" try: data = json.loads(sys.stdin.read()) - except (json.JSONDecodeError, IOError): + except (OSError, json.JSONDecodeError): sys.exit(0) # Support both command-style and tool-style input diff --git a/corvus-hooks/corvus-event-emit.py b/corvus-hooks/corvus-event-emit.py index 197bff9..16a8e71 100755 --- a/corvus-hooks/corvus-event-emit.py +++ b/corvus-hooks/corvus-event-emit.py @@ -25,7 +25,7 @@ def main(): try: raw = sys.stdin.read() data = json.loads(raw) - except (json.JSONDecodeError, IOError): + except (OSError, json.JSONDecodeError): sys.exit(0) tool_name = data.get("tool_name", "") diff --git a/corvus-hooks/corvus-governance.py b/corvus-hooks/corvus-governance.py index bea8f28..403881a 100755 --- a/corvus-hooks/corvus-governance.py +++ b/corvus-hooks/corvus-governance.py @@ -27,7 +27,7 @@ def main(): try: raw = sys.stdin.read() data = json.loads(raw) - except (json.JSONDecodeError, IOError): + except (OSError, json.JSONDecodeError): sys.exit(0) tool_name = data.get("tool_name", "") diff --git a/corvus-hooks/corvus-lifecycle.py b/corvus-hooks/corvus-lifecycle.py index 5a88bbc..7fd31a8 100755 --- a/corvus-hooks/corvus-lifecycle.py +++ b/corvus-hooks/corvus-lifecycle.py @@ -92,7 +92,7 @@ def main(): try: raw = sys.stdin.read() data = json.loads(raw) - except (json.JSONDecodeError, IOError): + except (OSError, json.JSONDecodeError): sys.exit(0) prompt = data.get("user_prompt", "") diff --git a/corvus-hooks/corvus_core.py b/corvus-hooks/corvus_core.py index 6ea591c..6e83553 100644 --- a/corvus-hooks/corvus_core.py +++ b/corvus-hooks/corvus_core.py @@ -19,7 +19,7 @@ import subprocess import urllib.error import urllib.request -from typing import Any, Dict, Optional, Tuple +from typing import Any # --------------------------------------------------------------------------- # Configuration @@ -41,7 +41,7 @@ # --------------------------------------------------------------------------- -def get_api_key() -> Optional[str]: +def get_api_key() -> str | None: """Retrieve Corvus API key. Tries macOS keychain, then env var.""" # macOS keychain if platform.system() == "Darwin": @@ -65,7 +65,7 @@ def get_api_key() -> Optional[str]: # --------------------------------------------------------------------------- -def check_target(target: str, api_key: Optional[str] = None) -> Optional[Dict[str, Any]]: +def check_target(target: str, api_key: str | None = None) -> dict[str, Any] | None: """Call Corvus GET /ops/events/targets/{target}/status. Returns dict with keys: target, recommendation (GO/CAUTION/STOP), @@ -95,8 +95,8 @@ def emit_event( target: str, source: str = "ai-assistant", severity: str = "info", - data: Optional[Dict[str, Any]] = None, - api_key: Optional[str] = None, + data: dict[str, Any] | None = None, + api_key: str | None = None, ) -> bool: """POST an event to Corvus. Returns True on success.""" if not api_key: @@ -182,7 +182,7 @@ def emit_event( } -def extract_target_from_command(command: str) -> Optional[str]: +def extract_target_from_command(command: str) -> str | None: """Extract infrastructure target from a shell command. Returns container/volume/network/stack name, or None if not destructive. @@ -216,7 +216,7 @@ def extract_target_from_command(command: str) -> Optional[str]: return None -def extract_action_from_command(command: str) -> Optional[Tuple[str, str]]: +def extract_action_from_command(command: str) -> tuple[str, str] | None: """Extract (action, target) from a shell command for event emission. Returns (action, target) tuple or None. @@ -275,7 +275,7 @@ def normalize_mcp_tool_name(tool_name: str) -> str: return tool_name -def extract_target_from_mcp(tool_name: str, tool_input: Dict[str, Any]) -> Optional[str]: +def extract_target_from_mcp(tool_name: str, tool_input: dict[str, Any]) -> str | None: """Extract target from an MCP tool call's input dict.""" canonical = normalize_mcp_tool_name(tool_name) primary = MCP_TARGET_FIELDS.get(canonical) @@ -295,7 +295,7 @@ def is_destructive_mcp_tool(tool_name: str) -> bool: return canonical in MCP_TARGET_FIELDS -def get_mcp_event_type(tool_name: str) -> Optional[str]: +def get_mcp_event_type(tool_name: str) -> str | None: """Get the Corvus event type for an MCP tool.""" canonical = normalize_mcp_tool_name(tool_name) return MCP_EVENT_MAP.get(canonical) @@ -384,7 +384,7 @@ def get_mcp_event_type(tool_name: str) -> Optional[str]: ] -def classify_intent(prompt: str) -> Optional[str]: +def classify_intent(prompt: str) -> str | None: """Classify user intent: 'incident', 'change', 'design', or None.""" for pattern in INCIDENT_PATTERNS: if pattern.search(prompt): diff --git a/corvus-hooks/pyproject.toml b/corvus-hooks/pyproject.toml new file mode 100644 index 0000000..72a1000 --- /dev/null +++ b/corvus-hooks/pyproject.toml @@ -0,0 +1,25 @@ +# Ruff config for the legacy v1 governance hooks. +# +# v1 is ARCHIVED (branch v1-archive) and not deployed — this file exists so the +# hooks cannot silently rot. The rule set is pinned EXPLICITLY rather than left to +# ruff's defaults, because those drift: ruff 0.15 enabled 59 rules by default and +# ruff 0.17 enables 407, which would flip this guard red on a tool upgrade alone. +# +# The select list matches corvus-server/pyproject.toml so both legacy components +# are held to one standard. It is ruff-only — there is deliberately no [project] +# table, so this file cannot be mistaken for an installable package. +[tool.ruff] +line-length = 120 +target-version = "py312" + +[tool.ruff.lint] +select = ["E", "F", "W", "I", "N", "UP", "S", "B", "A", "C4", "SIM"] +ignore = [ + "S310", # urlopen against the operator-configured Corvus URL is the hook's whole job + "S603", # shelling out to the local git/installer binaries is intentional + "S607", # ditto — the hooks resolve git from PATH by design + "SIM102", # 2 nested ifs in the archived adapters; ruff marks the collapse UNSAFE, so the dead code stays untouched +] + +[tool.ruff.lint.isort] +known-first-party = ["corvus_core"] diff --git a/corvus-sdk/.gitignore b/corvus-sdk/.gitignore new file mode 100644 index 0000000..eb9f454 --- /dev/null +++ b/corvus-sdk/.gitignore @@ -0,0 +1,4 @@ +# Component-local artifacts (the root .gitignore already covers .venv/) +.venv/ +coverage.json +junit.xml diff --git a/corvus-sdk/Makefile b/corvus-sdk/Makefile new file mode 100644 index 0000000..168e532 --- /dev/null +++ b/corvus-sdk/Makefile @@ -0,0 +1,44 @@ +# corvus-sdk dev harness — the build > lint > test loop, one command per step. +# python3.12 is PINNED: bare python3 on this host is 3.15 and breaks parity with CI. + +PYTHON ?= python3.12 +VENV := .venv +PY := $(VENV)/bin/python +PIP := $(VENV)/bin/pip +RUFF := $(VENV)/bin/ruff + +.PHONY: setup build lint test fix cycle clean + +setup: ## Bootstrap the 3.12 venv, the component deps and the pinned tooling + $(PYTHON) -m venv $(VENV) + $(PIP) install --quiet --upgrade pip + $(PIP) install --quiet -e ".[dev]" + $(PIP) install --quiet ruff==0.17.1 pytest pytest-cov + @echo "setup: ok" + +build: ## The editable install plus a real import + client/model proof + $(PIP) install --quiet -e . + $(PIP) check # the resolved dependency set must be internally consistent + $(PY) -c 'import corvus_sdk as s; c = s.CorvusClient("http://corvus:8000/", token="t"); assert c._base_url == "http://corvus:8000", c._base_url; assert s.Event("E-1", "t0", "src", "change.completed", "tg").data == {}; assert issubclass(s.CorvusError, Exception); print("build: ok —", s.__version__)' + @echo "build: ok" + +lint: ## Ruff over the package and the tests + $(RUFF) check src/ tests/ + @echo "lint: ok" + +test: ## The tests with the coverage measurement + $(PY) -m pytest tests/ -q --cov=corvus_sdk --cov-report=term --cov-report=json:coverage.json --junitxml=junit.xml + @echo "test: ok" + +fix: ## Apply the auto-fixable lint findings + $(RUFF) check --fix src/ tests/ + @echo "fix: ok" + +cycle: build lint test ## THE one command: build > lint > test + +# clean only touches artifacts this harness created: the venv and the test reports. +# PREFLIGHT:rollback-written — the rollback is `make setup` (and `make test`), which +# recreates every deleted artifact from pyproject.toml in one command. +clean: ## The venv and the test artifacts (recoverable: make setup + make test) + rm -rf $(VENV) .coverage coverage.json junit.xml .pytest_cache .ruff_cache + @echo "clean: ok" diff --git a/corvus-sdk/pyproject.toml b/corvus-sdk/pyproject.toml index 5bdebda..686e611 100644 --- a/corvus-sdk/pyproject.toml +++ b/corvus-sdk/pyproject.toml @@ -3,10 +3,12 @@ name = "corvus-sdk" version = "0.1.0" requires-python = ">=3.11" description = "Lightweight Python SDK for the Corvus operational governance API" -dependencies = ["httpx>=0.27"] +# Floors are the versions the harness tested (python3.12), capped at the next +# MAJOR so a breaking release fails at install time rather than silently. +dependencies = ["httpx>=0.28,<1.0"] [project.optional-dependencies] -dev = ["pytest>=8", "pytest-asyncio>=1", "respx>=0.22"] +dev = ["pytest>=9,<10", "pytest-asyncio>=1.4,<2", "respx>=0.23,<1"] [build-system] requires = ["hatchling"] diff --git a/corvus-sdk/tests/test_client.py b/corvus-sdk/tests/test_client.py index cbf305f..81a3693 100644 --- a/corvus-sdk/tests/test_client.py +++ b/corvus-sdk/tests/test_client.py @@ -1,5 +1,7 @@ """Tests for CorvusClient — all API operations.""" +import json + import httpx import pytest import respx @@ -514,3 +516,159 @@ async def test_500_error(self, client): async with client as c: with pytest.raises(CorvusError): await c.metrics() + + +# --------------------------------------------------------------------------- +# Step submission + triage continuation +# --------------------------------------------------------------------------- + + +class TestStepFlow: + @respx.mock + async def test_submit_step_posts_its_result(self, client): + route = respx.post(f"{BASE}/ops/runbooks/steps/STEP-1/result").mock( + return_value=httpx.Response(200, json={ + "step_id": "STEP-1", + "status": "submitted", + "triage_id": "TRG-1", + "all_steps_complete": True, + "total_steps": 2, + "pending_steps": 0, + }) + ) + async with client as c: + step = await c.submit_step("STEP-1", output={"stdout": "ok"}) + assert step.all_steps_complete is True + assert json.loads(route.calls.last.request.content) == { + "output": {"stdout": "ok"}, + "error": None, + "success": True, + } + + @respx.mock + async def test_submit_step_reports_a_failed_step(self, client): + route = respx.post(f"{BASE}/ops/runbooks/steps/STEP-2/result").mock( + return_value=httpx.Response(200, json={ + "step_id": "STEP-2", + "status": "submitted", + "triage_id": "TRG-1", + "all_steps_complete": False, + "total_steps": 2, + "pending_steps": 1, + }) + ) + async with client as c: + step = await c.submit_step("STEP-2", error="command timed out", success=False) + assert step.pending_steps == 1 + body = json.loads(route.calls.last.request.content) + assert body["error"] == "command timed out" + assert body["success"] is False + + @respx.mock + async def test_continue_triage(self, client): + respx.post(f"{BASE}/ops/runbooks/steps/triage/TRG-1/continue").mock( + return_value=httpx.Response(200, json={ + "status": "triaged", + "triage_id": "TRG-1", + "target": "caddy", + "service_type": "proxy", + "diagnosis": "config_drift", + "root_cause": "stale Caddyfile", + "confidence": 0.8, + "escalation_required": True, + "restart_safe": False, + }) + ) + async with client as c: + result = await c.continue_triage("TRG-1") + assert result.diagnosis == "config_drift" + assert result.escalation_required is True + assert result.restart_safe is False + + @respx.mock + async def test_pending_steps_filters_by_triage(self, client): + route = respx.get(f"{BASE}/ops/runbooks/steps/pending").mock( + return_value=httpx.Response(200, json=[{"id": "STEP-1", "command": "docker logs caddy"}]) + ) + async with client as c: + assert len(await c.pending_steps()) == 1 + assert dict(route.calls.last.request.url.params) == {} + assert len(await c.pending_steps("TRG-1")) == 1 + assert dict(route.calls.last.request.url.params) == {"triage_id": "TRG-1"} + + +# --------------------------------------------------------------------------- +# List filters — the query params the server filters on +# --------------------------------------------------------------------------- + + +class TestListFilters: + @respx.mock + async def test_list_incidents_sends_its_filters(self, client): + route = respx.get(f"{BASE}/ops/incidents").mock( + return_value=httpx.Response(200, json=[]) + ) + async with client as c: + await c.list_incidents(status="open", target="vllm", severity="warning") + params = route.calls.last.request.url.params + assert (params["status"], params["target"], params["severity"]) == ("open", "vllm", "warning") + + @respx.mock + async def test_list_services_sends_its_filters(self, client): + route = respx.get(f"{BASE}/ops/cmdb").mock( + return_value=httpx.Response(200, json=[]) + ) + async with client as c: + await c.list_services(service_type="proxy", host="host-01", critical=False) + params = route.calls.last.request.url.params + assert params["service_type"] == "proxy" + assert params["host"] == "host-01" + assert params["critical"] == "false" + + @respx.mock + async def test_compliance_audit_sends_its_filters(self, client): + route = respx.get(f"{BASE}/ops/metrics/compliance").mock( + return_value=httpx.Response(200, json={"coverage_rate": 95.0}) + ) + async with client as c: + await c.compliance_audit(since="2026-03-01", source="corvus-sdk") + params = route.calls.last.request.url.params + assert params["since"] == "2026-03-01" + assert params["source"] == "corvus-sdk" + + +# --------------------------------------------------------------------------- +# Dual-shape server fields — JSON-string or native list +# --------------------------------------------------------------------------- + + +class TestDualShapeFields: + @respx.mock + async def test_create_change_accepts_native_list_targets(self, client): + respx.post(f"{BASE}/ops/changes").mock( + return_value=httpx.Response(200, json={ + "id": "CHG-2", + "created_at": "2026-03-31T10:00:00", + "created_by": "corvus-sdk", + "status": "active", + "targets": ["caddy", "litellm"], + "description": "Rolling restart", + }) + ) + async with client as c: + change = await c.create_change(["caddy", "litellm"], "Rolling restart") + assert change.targets == ["caddy", "litellm"] + + @respx.mock + async def test_get_service_accepts_native_list_dependencies(self, client): + respx.get(f"{BASE}/ops/cmdb/litellm").mock( + return_value=httpx.Response(200, json={ + "id": "SVC-2", + "name": "litellm", + "dependencies": ["postgresql"], + }) + ) + async with client as c: + svc = await c.get_service("litellm") + assert svc.dependencies == ["postgresql"] + assert svc.alert_policy == "default" diff --git a/corvus-sdk/tests/test_models.py b/corvus-sdk/tests/test_models.py new file mode 100644 index 0000000..4ef8c4f --- /dev/null +++ b/corvus-sdk/tests/test_models.py @@ -0,0 +1,248 @@ +"""Tests for the response models — required fields, defaults and per-instance state.""" + +import pytest + +import corvus_sdk +from corvus_sdk.models import ( + Change, + Event, + Incident, + Problem, + Service, + StepResult, + TriageResult, +) + + +def test_change_defaults_and_optionals(): + change = Change( + id="CHG-1", + created_at="2026-03-31T10:00:00", + created_by="corvus-sdk", + status="active", + targets=["caddy"], + description="Update Caddyfile", + ) + assert (change.rollback_plan, change.project) == ("", "") + assert change.expires_at is None + assert change.completed_at is None + assert change.outcome is None + + closed = Change( + id="CHG-2", + created_at="2026-03-31T10:00:00", + created_by="ops", + status="completed", + targets=["vllm"], + description="Restart", + rollback_plan="restart --previous", + project="inference", + expires_at="2026-03-31T11:00:00", + completed_at="2026-03-31T10:20:00", + outcome="success", + ) + assert closed.rollback_plan == "restart --previous" + assert closed.completed_at == "2026-03-31T10:20:00" + assert closed.outcome == "success" + + +def test_event_defaults_and_optionals(): + event = Event( + id="EVT-1", timestamp="2026-03-31T10:00:00", source="corvus-sdk", type="change.started", target="caddy" + ) + assert event.severity == "info" + assert event.data == {} + assert event.related_incident_id is None + assert event.related_change_id is None + + linked = Event( + id="EVT-2", + timestamp="2026-03-31T10:00:00", + source="corvus-sdk", + type="incident.opened", + target="vllm", + severity="warning", + data={"reason": "oom"}, + related_incident_id="INC-1", + related_change_id="CHG-1", + ) + assert linked.severity == "warning" + assert linked.data == {"reason": "oom"} + assert linked.related_incident_id == "INC-1" + assert linked.related_change_id == "CHG-1" + + +def test_incident_defaults_and_optionals(): + incident = Incident( + id="INC-1", + created_at="2026-03-31T10:00:00", + detected_by="corvus-sdk", + target="vllm", + status="open", + severity="warning", + title="GPU OOM", + ) + assert incident.description is None + assert incident.root_cause is None + assert incident.remediation_applied is None + assert incident.resolved_at is None + assert incident.resolution_time_minutes is None + + resolved = Incident( + id="INC-2", + created_at="2026-03-31T10:00:00", + detected_by="corvus-sdk", + target="vllm", + status="resolved", + severity="critical", + title="GPU OOM", + description="VRAM exhausted", + root_cause="kv-cache leak", + remediation_applied="restart", + resolved_at="2026-03-31T10:12:00", + resolution_time_minutes=12, + ) + assert resolved.root_cause == "kv-cache leak" + assert resolved.remediation_applied == "restart" + assert resolved.resolution_time_minutes == 12 + + +def test_problem_defaults_and_optionals(): + problem = Problem(id="PRB-1", created_at="2026-03-31T10:00:00", status="identified", title="Recurring GPU OOM") + assert problem.pattern is None + assert problem.root_cause is None + assert problem.workstream is None + assert problem.severity == "medium" + assert problem.correlated_incidents == [] + + tracked = Problem( + id="PRB-2", + created_at="2026-03-31T10:00:00", + status="in_progress", + title="Config drift", + pattern="caddy reloads weekly", + root_cause="out-of-band edit", + severity="high", + correlated_incidents=["INC-1", "INC-2"], + workstream="CI", + ) + assert tracked.correlated_incidents == ["INC-1", "INC-2"] + assert tracked.workstream == "CI" + + +def test_service_defaults_and_optionals(): + service = Service(id="SVC-1", name="caddy") + assert service.host is None + assert service.service_type is None + assert service.critical is False + assert service.dependencies == [] + assert service.baseline_behavior == {} + assert service.alert_policy == "default" + + registered = Service( + id="SVC-2", + name="litellm", + host="host-01", + service_type="proxy", + critical=True, + dependencies=["postgresql"], + baseline_behavior={"p99_ms": 120}, + alert_policy="strict", + ) + assert registered.critical is True + assert registered.dependencies == ["postgresql"] + assert registered.alert_policy == "strict" + + +def test_step_result_carries_all_counters(): + step = StepResult( + step_id="STEP-1", + status="submitted", + triage_id="TRG-1", + all_steps_complete=False, + total_steps=3, + pending_steps=2, + ) + assert step.step_id == "STEP-1" + assert step.all_steps_complete is False + assert (step.total_steps, step.pending_steps) == (3, 2) + + +def test_triage_result_defaults_and_optionals(): + triage = TriageResult(status="pending_steps", triage_id="TRG-1", target="caddy", service_type="proxy") + assert triage.runbook_name is None + assert triage.diagnosis is None + assert triage.root_cause is None + assert triage.confidence == 0.0 + assert triage.escalation_required is False + assert triage.restart_safe is None + assert triage.pending_steps == [] + + completed = TriageResult( + status="triaged", + triage_id="TRG-2", + target="vllm", + service_type="inference", + runbook_name="inference-oom.yaml", + diagnosis="gpu_oom", + root_cause="VRAM exhaustion", + confidence=0.9, + escalation_required=True, + restart_safe=False, + pending_steps=[{"id": "STEP-1"}], + ) + assert completed.runbook_name == "inference-oom.yaml" + assert completed.confidence == 0.9 + assert completed.escalation_required is True + assert completed.restart_safe is False + assert completed.pending_steps == [{"id": "STEP-1"}] + + +def test_required_fields_are_enforced(): + """A missing required field must raise, not silently default.""" + with pytest.raises(TypeError): + Service(id="SVC-1") # name is required + with pytest.raises(TypeError): + StepResult(step_id="STEP-1") # status/triage_id/counters are required + + +def test_mutable_defaults_are_per_instance(): + """field(default_factory=...) must not hand every instance the same object.""" + event_a = Event(id="EVT-1", timestamp="t0", source="s", type="t", target="x") + event_b = Event(id="EVT-2", timestamp="t0", source="s", type="t", target="x") + event_a.data["leak"] = True + assert event_b.data == {} + + service_a = Service(id="SVC-1", name="a") + service_b = Service(id="SVC-2", name="b") + service_a.dependencies.append("dep") + service_a.baseline_behavior["cpu"] = 1 + assert service_b.dependencies == [] + assert service_b.baseline_behavior == {} + + problem_a = Problem(id="PRB-1", created_at="t0", status="open", title="a") + problem_b = Problem(id="PRB-2", created_at="t0", status="open", title="b") + problem_a.correlated_incidents.append("INC-1") + assert problem_b.correlated_incidents == [] + + triage_a = TriageResult(status="s", triage_id="TRG-1", target="x", service_type="y") + triage_b = TriageResult(status="s", triage_id="TRG-2", target="x", service_type="y") + triage_a.pending_steps.append({"id": "STEP-1"}) + assert triage_b.pending_steps == [] + + +def test_public_surface_is_reexported(): + assert set(corvus_sdk.__all__) == { + "CorvusClient", + "CorvusError", + "Change", + "Event", + "Incident", + "Problem", + "Service", + "StepResult", + "TriageResult", + } + assert corvus_sdk.Change is Change + assert corvus_sdk.TriageResult is TriageResult + assert corvus_sdk.__version__ == "0.1.0" diff --git a/corvus-server/.gitignore b/corvus-server/.gitignore new file mode 100644 index 0000000..8b5fb9a --- /dev/null +++ b/corvus-server/.gitignore @@ -0,0 +1,5 @@ +# Component-local artifacts (the root .gitignore is owned elsewhere) +.venv/ +__pycache__/ +*.pyc +.ruff_cache/ diff --git a/corvus-server/Makefile b/corvus-server/Makefile new file mode 100644 index 0000000..936d048 --- /dev/null +++ b/corvus-server/Makefile @@ -0,0 +1,56 @@ +# The corvus-server guard — legacy v1 platform (FastAPI) must lint and import cleanly. +# +# v1 is ARCHIVED (branch v1-archive) and NOT deployed; the live product of record is +# corvus-v2. There are deliberately NO tests here — this is rot protection, not a suite. +# +# LAYOUT NOTE: the repo-root CLAUDE.md documents a src/ layout (src/app.py, routers/, +# middleware/, models/...). That doc is STALE — none of it exists on this branch. The +# real corvus-server is two modules: +# cli/corvus_cli.py the typer CLI (builds app = typer.Typer(...) at import) +# modules/soc2/module.py the SOC 2 APIRouter (builds router = APIRouter() at import) +# There is therefore no package to `pip install -e .` and no app factory to import, so +# this harness has lint + import rather than build + test. See the report for the gap. +# +# python3.12 is PINNED: bare `python3` on this host is 3.15 and would break parity with +# the CI, which runs 3.12. + +PYTHON ?= python3.12 +VENV := .venv +PY := $(VENV)/bin/python +PIP := $(VENV)/bin/pip +RUFF := $(VENV)/bin/ruff + +.PHONY: setup lint import fix cycle clean + +# Auto-bootstrap: any target that needs the venv triggers setup exactly once, so +# `make cycle` is genuinely the one command on a fresh clone (this is a real file +# target, not .PHONY — it is a no-op while .venv/bin/ruff exists). +$(VENV)/bin/ruff: + $(MAKE) setup + +setup: ## Bootstrap the pinned 3.12 venv + the component deps + the tooling + $(PYTHON) -m venv $(VENV) + $(PIP) install --quiet --upgrade pip + $(PIP) install --quiet -r requirements.txt + $(PIP) install --quiet ruff==0.17.1 pytest pytest-cov + @echo "setup: ok" + +lint: $(VENV)/bin/ruff ## Ruff over both modules, using corvus-server/pyproject.toml's own config + $(RUFF) check cli/ modules/ + +import: $(VENV)/bin/ruff ## The real proof: import both modules and assert the objects they build + $(PY) -c "import sys; sys.path.insert(0, '.'); \ +assert sys.version_info[:2] == (3, 12), f'expected 3.12 (CI parity), got {sys.version.split()[0]}'; \ +import modules.soc2.module as soc2; import cli.corvus_cli as cli; \ +from fastapi import APIRouter; import typer; \ +assert isinstance(soc2.router, APIRouter), type(soc2.router); \ +assert isinstance(cli.app, typer.Typer), type(cli.app); \ +print(f'import: ok on {sys.version.split()[0]} — soc2 APIRouter({len(soc2.router.routes)} routes), typer app {cli.app.info.name!r} ({len(cli.app.registered_commands)} commands)')" + +fix: $(VENV)/bin/ruff ## Ruff autofixes (no ruff format — this repo does not use it) + $(RUFF) check --fix cli/ modules/ + +cycle: lint import ## The guard, one command + +clean: ## The venv + the lint artifacts + rm -rf $(VENV) .ruff_cache __pycache__ cli/__pycache__ modules/__pycache__ modules/soc2/__pycache__ diff --git a/corvus-splunk/.gitignore b/corvus-splunk/.gitignore new file mode 100644 index 0000000..d505c5a --- /dev/null +++ b/corvus-splunk/.gitignore @@ -0,0 +1,3 @@ +# Component-local artifacts (the root .gitignore is owned elsewhere) +__pycache__/ +*.pyc diff --git a/corvus-splunk/Makefile b/corvus-splunk/Makefile new file mode 100644 index 0000000..1a14888 --- /dev/null +++ b/corvus-splunk/Makefile @@ -0,0 +1,28 @@ +# The corvus-splunk guard — a config-only Splunk app, so `validate` IS the whole guard. +# +# There is deliberately NO venv: validate.py is stdlib-only (json, csv, xml.etree), so +# there is nothing to install and the VENV/PY/PIP/RUFF variables have no meaning here. +# It still runs on the pinned python3.12 for CI parity — bare `python3` on this host is +# 3.15, and a syntax check under the wrong interpreter proves the wrong thing. +# +# Proves: every default/*.conf parses, every dashboard XML is well-formed, every lookup +# CSV has a consistent column count, the transforms/props/nav cross-references resolve, +# and bin/ compiles. +# Cannot prove: that Splunk accepts the app, that the SPL is valid, or that the +# dashboards render — those need a live Splunk instance. + +PYTHON ?= python3.12 + +.PHONY: setup validate cycle clean + +setup: ## Nothing to install — validate.py is stdlib-only, so there is no venv + @echo "setup: ok — no venv by design: validate.py imports only json/csv/xml.etree" + +validate: ## Parse every conf/XML/CSV, resolve the cross-references, compile bin/ + $(PYTHON) validate.py + +cycle: validate ## The guard, one command + +clean: ## The byte-compile artifacts (validate.py itself writes nothing) + rm -rf __pycache__ bin/__pycache__ .pytest_cache + @echo "clean: ok — validate.py writes no artifacts of its own" diff --git a/corvus-splunk/validate.py b/corvus-splunk/validate.py new file mode 100644 index 0000000..900abc8 --- /dev/null +++ b/corvus-splunk/validate.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""Validate the corvus-splunk Splunk app. + +This app is config-only, so this script IS the whole guard. It proves: + * every default/*.conf parses as a [stanza] / key = value file + * every dashboard/nav XML is well-formed + * every lookup CSV has a header and a consistent column count + * the transforms.conf -> lookups/, props.conf -> transforms.conf and + nav/default.xml -> views/ cross-references all resolve + * every bin/*.py compiles + * any JSON/YAML that appears parses (there are none today) + +It CANNOT prove: that Splunk accepts the app, that the SPL in the saved searches is +valid, or that the dashboards render. Those all need a live Splunk instance. + +Stdlib only by design — no venv, no dependencies. +""" + +from __future__ import annotations + +import csv +import json +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +DEFAULT = ROOT / "default" +LOOKUPS = ROOT / "lookups" +VIEWS = DEFAULT / "data" / "ui" / "views" + +# Views Splunk ships itself: a nav reference to one of these needs no file here. +BUILTIN_VIEWS = {"search", "dashboards", "reports", "alerts", "help", "launcher"} + +REQUIRED = [ + DEFAULT / "app.conf", + DEFAULT / "indexes.conf", + DEFAULT / "macros.conf", + DEFAULT / "props.conf", + DEFAULT / "savedsearches.conf", + DEFAULT / "transforms.conf", + ROOT / "metadata" / "default.meta", + LOOKUPS, +] + +errors: list[str] = [] + + +def check(ok: bool, message: str) -> bool: + if not ok: + errors.append(message) + return ok + + +def parse_conf(path: Path) -> dict[str, dict[str, str]]: + """Parse a Splunk .conf into {stanza: {key: value}}. + + Handles '#' comments, blank lines and backslash line continuations. The + continuations matter: savedsearches.conf splits long SPL across lines, and a + naive one-key-per-line parser false-fails on them. + """ + stanzas: dict[str, dict[str, str]] = {} + current: dict[str, str] | None = None + pending = "" + for lineno, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + line = pending + raw + pending = "" + if line.rstrip().endswith("\\"): + pending = line.rstrip()[:-1] + " " + continue + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + if stripped.startswith("[") and stripped.endswith("]"): + name = stripped[1:-1].strip() + if not name: + errors.append(f"{path.name}:{lineno}: empty stanza name") + elif name in stanzas: + errors.append(f"{path.name}:{lineno}: duplicate stanza [{name}]") + current = stanzas.setdefault(name, {}) + continue + if current is None: + errors.append(f"{path.name}:{lineno}: content before the first [stanza]") + continue + if "=" not in stripped: + errors.append(f"{path.name}:{lineno}: not a 'key = value' line: {stripped[:60]!r}") + continue + key, _, value = stripped.partition("=") + current[key.strip()] = value.strip() + if pending: + errors.append(f"{path.name}: file ends inside a line continuation") + return stanzas + + +def main() -> int: + counts = {"conf": 0, "xml": 0, "csv": 0, "py": 0} + + # 1. the metadata files a Splunk app cannot ship without + for path in REQUIRED: + check(path.exists(), f"missing required path: {path.relative_to(ROOT)}") + + # 2. every .conf parses + confs: dict[str, dict[str, dict[str, str]]] = {} + for path in sorted(DEFAULT.glob("*.conf")): + confs[path.name] = parse_conf(path) + counts["conf"] += 1 + print(f" conf default/{path.name}: {len(confs[path.name])} stanza(s)") + + # 3. every XML is well-formed + for path in sorted(DEFAULT.rglob("*.xml")): + counts["xml"] += 1 + try: + ET.parse(path) + print(f" xml {path.relative_to(ROOT)}: well-formed") + except ET.ParseError as exc: + errors.append(f"{path.relative_to(ROOT)}: malformed XML: {exc}") + + # 4. every lookup CSV has a header and a consistent column count + for path in sorted(LOOKUPS.glob("*.csv")): + counts["csv"] += 1 + rows = list(csv.reader(path.read_text(encoding="utf-8").splitlines())) + if not rows: + errors.append(f"lookups/{path.name}: empty CSV") + continue + width = len(rows[0]) + for lineno, row in enumerate(rows[1:], start=2): + if len(row) != width: + errors.append(f"lookups/{path.name}:{lineno}: {len(row)} columns, header has {width}") + print(f" csv lookups/{path.name}: {len(rows) - 1} row(s) x {width} col(s)") + + # 5. transforms.conf filename = X must exist under lookups/ + transforms = confs.get("transforms.conf", {}) + for stanza, keys in transforms.items(): + filename = keys.get("filename") + if filename: + check((LOOKUPS / filename).exists(), f"transforms.conf [{stanza}] points at missing lookup {filename!r}") + + # 6. props.conf LOOKUP- = ... must resolve + for stanza, keys in confs.get("props.conf", {}).items(): + for key, value in keys.items(): + if not key.upper().startswith("LOOKUP-"): + continue + target = value.split()[0] if value.split() else "" + check(target in transforms, f"props.conf [{stanza}] {key} references unknown transforms stanza {target!r}") + + # 7. nav/default.xml view references must resolve to views/.xml + nav = DEFAULT / "data" / "ui" / "nav" / "default.xml" + if nav.exists(): + for view in ET.parse(nav).getroot().iter("view"): + name = view.get("name") or "" + if name in BUILTIN_VIEWS: + continue + check((VIEWS / f"{name}.xml").exists(), f"nav/default.xml references view {name!r} with no views/{name}.xml") + + # 8. every bin/ script compiles (in memory — writes nothing) + for path in sorted((ROOT / "bin").glob("*.py")): + counts["py"] += 1 + try: + compile(path.read_text(encoding="utf-8"), str(path), "exec") + print(f" py bin/{path.name}: compiles") + except SyntaxError as exc: + errors.append(f"bin/{path.name}: syntax error: {exc}") + + # 9. JSON, and YAML if any appears (there are none today) + for path in sorted(ROOT.rglob("*.json")): + try: + json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + errors.append(f"{path.relative_to(ROOT)}: malformed JSON: {exc}") + yamls = sorted(ROOT.rglob("*.yaml")) + sorted(ROOT.rglob("*.yml")) + if yamls: + try: + import yaml + except ImportError: + errors.append("YAML files present but PyYAML is unavailable — refusing to skip them silently") + else: + for path in yamls: + try: + yaml.safe_load(path.read_text(encoding="utf-8")) + except yaml.YAMLError as exc: + errors.append(f"{path.relative_to(ROOT)}: malformed YAML: {exc}") + + if errors: + print(f"\nvalidate: FAILED — {len(errors)} problem(s)", file=sys.stderr) + for problem in errors: + print(f" - {problem}", file=sys.stderr) + return 1 + + print( + f"\nvalidate: ok — {counts['conf']} conf, {counts['xml']} xml, " + f"{counts['csv']} csv, {counts['py']} py, 0 json, {len(yamls)} yaml" + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/corvus-v2/Makefile b/corvus-v2/Makefile new file mode 100644 index 0000000..26cc406 --- /dev/null +++ b/corvus-v2/Makefile @@ -0,0 +1,82 @@ +# The corvus-v2 dev harness — the Build > Test > Fix cycle, one command per step. +# The engine is the deployed artifact (image corvus-v2:): its harness is the +# gate that decides whether a change is deployable. Every step is idempotent. + +PYTHON ?= python3.12 +VENV := .venv +PY := $(VENV)/bin/python +PIP := $(VENV)/bin/pip +RUFF := $(VENV)/bin/ruff +METRICS := ../docs/canonical-path/dev-metrics.json +SRC := corvus/ tests/ + +.PHONY: setup build lint test fix metrics metrics-check run check cycle clean mutation conformance + +setup: ## Bootstrap the venv + the deps (fresh-clone safe, pinned interpreter) + $(PYTHON) -m venv $(VENV) + $(PIP) install --quiet --upgrade pip + $(PIP) install --quiet -r requirements.txt + $(PIP) install --quiet pytest pytest-cov ruff==0.17.1 httpx + @echo "setup: ok ($$($(PY) --version))" + +build: ## The engine up + the live proof: health, an authenticated read, and the 401 control + $(PIP) check # the resolved dependency set must be internally consistent + $(PY) scripts/dev_check.py + +lint: ## Ruff over the package and the tests + $(RUFF) check $(SRC) + @echo "lint: ok" + +test: ## The tests with the coverage measurement + $(PY) -m pytest tests/ -q \ + --cov=corvus --cov-report=term --cov-report=json:coverage.json \ + --junitxml=junit.xml + @echo "test: ok" + +fix: ## The autofix step — ruff repairs what it can, then lint proves it + $(RUFF) check --fix $(SRC) + @echo "fix: ok (run `make lint` to confirm)" + +metrics: ## Regenerate the dev-metrics artifact from the last test run + $(PY) scripts/gen_metrics.py \ + --coverage coverage.json --junit junit.xml --out $(METRICS) \ + --source "$$(git rev-parse --short HEAD)" \ + --duration "$$($(PY) -c 'import xml.etree.ElementTree as ET; r=ET.parse("junit.xml").getroot(); s=r.find("testsuite") if r.tag=="testsuites" else r; print(round(float(s.get("time",0)),2))')" + @echo "metrics: regenerated" + +metrics-check: ## The drift guard: the committed metrics must match a fresh run + $(PY) scripts/gen_metrics.py \ + --coverage coverage.json --junit junit.xml --out /tmp/metrics-check.json \ + --source "$$(git rev-parse --short HEAD)" + $(PY) scripts/check_metrics.py --expected $(METRICS) --actual /tmp/metrics-check.json + +run: build ## Alias: the live check is the build step + +check: lint test metrics-check ## The full gate set (what CI runs) + +cycle: build lint test metrics-check ## THE Build > Test > Fix loop, one command + +mutation: ## The mutation evidence (the phase-gate target) + $(PY) -c "import sys; sys.path.insert(0, '.'); from corvus.conformance import run_mutation_suite; import json; r = run_mutation_suite(); print(json.dumps(r, indent=1)); exit(0 if r['all_mutants_caught'] else 1)" + +conformance: ## The conformance run against the engine shim (the phase-gate target) + @PORT=$$($(PY) -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()'); \ + $(PY) -m corvus.engine_shim --port $$PORT & SHIM=$$!; sleep 2; \ + $(PY) -c "import sys, json; sys.path.insert(0, '.'); from corvus.conformance import conformance_run; r = conformance_run('http://127.0.0.1:' + sys.argv[1]); print(json.dumps(r, indent=1)); sys.exit(0 if r['failed'] == 0 else 1)" $$PORT; \ + STATUS=$$?; kill $$SHIM 2>/dev/null || true; exit $$STATUS +# Two measured defects this target used to have: +# 1. The shim is killed by PID, never by `pkill -f`: the recipe's own shell carries +# the pattern in its argv, so pkill matched and killed the shell — the run passed +# (2/0) while make reported "Terminated" and exited 2 (CI run 40058979560). +# 2. The port is EPHEMERAL. A fixed 8933 silently tested whatever already held it: +# this host runs a launchd agent (com.themillertribe.corvus-v2-engine) bound to +# 127.0.0.1:8933 from a different worktree, so a local conformance "pass" was +# evidence about that process, not this code. The ephemeral port removes the +# ambiguity; if the shim fails to start, conformance fails loudly. + +# PREFLIGHT:rollback-written — clean deletes only REGENERABLE state (the venv and the +# pytest cache); `make setup` and `make test` recreate both. It deliberately does NOT +# delete coverage.json / junit.xml, which are tracked artifacts feeding dev-metrics.json. +clean: ## The regenerable artifacts only (never the tracked metrics inputs) + rm -rf $(VENV) .pytest_cache + @echo "clean: ok — run `make setup` to rebuild" diff --git a/corvus-v2/coverage.json b/corvus-v2/coverage.json index 4a677e9..9300f5d 100644 --- a/corvus-v2/coverage.json +++ b/corvus-v2/coverage.json @@ -1 +1 @@ -{"meta": {"format": 3, "version": "7.16.2", "timestamp": "2026-10-18T13:37:14.949232", "branch_coverage": false, "show_contexts": false}, "files": {"corvus/__init__.py": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "corvus/app.py": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 78, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 78, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [15, 17, 18, 19, 20, 21, 23, 25, 26, 27, 29, 30, 31, 34, 35, 36, 37, 38, 39, 42, 43, 44, 45, 46, 47, 48, 51, 52, 53, 54, 55, 58, 59, 60, 61, 62, 64, 67, 68, 70, 71, 72, 75, 76, 77, 78, 79, 80, 81, 82, 83, 85, 86, 88, 90, 91, 92, 93, 95, 96, 97, 98, 99, 101, 103, 105, 106, 107, 108, 109, 110, 112, 113, 114, 116, 117, 119, 121], "excluded_lines": [], "functions": {"_auth_key": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 4, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [52, 53, 54, 55], "excluded_lines": [], "start_line": 51}, "create_app": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 21, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 21, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [59, 60, 61, 62, 64, 67, 70, 71, 75, 76, 85, 86, 90, 91, 105, 106, 112, 113, 116, 117, 121], "excluded_lines": [], "start_line": 58}, "create_app.require_key": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [68], "excluded_lines": [], "start_line": 67}, "create_app.health": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [72], "excluded_lines": [], "start_line": 71}, "create_app.ingest": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 7, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 7, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [77, 78, 79, 80, 81, 82, 83], "excluded_lines": [], "start_line": 76}, "create_app.events": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [88], "excluded_lines": [], "start_line": 86}, "create_app.stream": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 4, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [92, 93, 95, 103], "excluded_lines": [], "start_line": 91}, "create_app.stream.gen": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [96, 97, 98, 99, 101], "excluded_lines": [], "start_line": 95}, "create_app.open_change": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 4, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [107, 108, 109, 110], "excluded_lines": [], "start_line": 106}, "create_app.active_changes": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [114], "excluded_lines": [], "start_line": 113}, "create_app.conflict_check": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [119], "excluded_lines": [], "start_line": 117}, "": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 28, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 28, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [15, 17, 18, 19, 20, 21, 23, 25, 26, 27, 29, 30, 31, 34, 35, 36, 37, 38, 39, 42, 43, 44, 45, 46, 47, 48, 51, 58], "excluded_lines": [], "start_line": 1}}, "classes": {"EventIn": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 34}, "ChangeIn": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 42}, "": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 78, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 78, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [15, 17, 18, 19, 20, 21, 23, 25, 26, 27, 29, 30, 31, 34, 35, 36, 37, 38, 39, 42, 43, 44, 45, 46, 47, 48, 51, 52, 53, 54, 55, 58, 59, 60, 61, 62, 64, 67, 68, 70, 71, 72, 75, 76, 77, 78, 79, 80, 81, 82, 83, 85, 86, 88, 90, 91, 92, 93, 95, 96, 97, 98, 99, 101, 103, 105, 106, 107, 108, 109, 110, 112, 113, 114, 116, 117, 119, 121], "excluded_lines": [], "start_line": 1}}}, "corvus/config.py": {"executed_lines": [7, 9, 10, 11, 15, 36, 37, 38, 39, 40, 41, 43, 44, 45, 48], "summary": {"covered_lines": 15, "num_statements": 23, "percent_covered": 65.21739130434783, "percent_covered_display": "65", "missing_lines": 8, "excluded_lines": 0, "percent_statements_covered": 65.21739130434783, "percent_statements_covered_display": "65"}, "missing_lines": [55, 56, 57, 58, 66, 67, 68, 73], "excluded_lines": [], "functions": {"FleetConfig.event_types": {"executed_lines": [45], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 44}, "load_config": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 8, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 8, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [55, 56, 57, 58, 66, 67, 68, 73], "excluded_lines": [], "start_line": 48}, "": {"executed_lines": [7, 9, 10, 11, 15, 36, 37, 38, 39, 40, 41, 43, 44, 48], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"FleetConfig": {"executed_lines": [45], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 37}, "": {"executed_lines": [7, 9, 10, 11, 15, 36, 37, 38, 39, 40, 41, 43, 44, 48], "summary": {"covered_lines": 14, "num_statements": 22, "percent_covered": 63.63636363636363, "percent_covered_display": "64", "missing_lines": 8, "excluded_lines": 0, "percent_statements_covered": 63.63636363636363, "percent_statements_covered_display": "64"}, "missing_lines": [55, 56, 57, 58, 66, 67, 68, 73], "excluded_lines": [], "start_line": 1}}}, "corvus/core.py": {"executed_lines": [9, 11, 12, 13, 14, 15, 17, 18, 19, 22, 23, 26, 29, 30, 33, 34, 35, 36, 37, 41, 42, 43, 44, 45, 46, 47, 48, 49, 60, 61, 66, 69, 82, 83, 84, 85, 87, 88, 96, 97, 101, 105, 107, 108, 113, 117, 121, 122, 130, 134, 136, 137, 138, 139, 140, 141, 142, 143, 144, 148, 149, 150, 151], "summary": {"covered_lines": 63, "num_statements": 70, "percent_covered": 90.0, "percent_covered_display": "90", "missing_lines": 7, "excluded_lines": 0, "percent_statements_covered": 90.0, "percent_statements_covered_display": "90"}, "missing_lines": [70, 71, 76, 78, 86, 146, 147], "excluded_lines": [], "functions": {"_now_iso": {"executed_lines": [23], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 22}, "sign_event": {"executed_lines": [29, 30], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 26}, "CorvusCore.__init__": {"executed_lines": [35, 36, 37], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 34}, "CorvusCore.ingest_event": {"executed_lines": [42, 43, 44, 45, 46, 47, 48, 49, 60, 61, 66], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 41}, "CorvusCore.query_events": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 4, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [70, 71, 76, 78], "excluded_lines": [], "start_line": 69}, "CorvusCore.open_change": {"executed_lines": [83, 84, 85, 87, 88, 96, 97, 101, 105], "summary": {"covered_lines": 9, "num_statements": 10, "percent_covered": 90.0, "percent_covered_display": "90", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 90.0, "percent_statements_covered_display": "90"}, "missing_lines": [86], "excluded_lines": [], "start_line": 82}, "CorvusCore.active_changes_for": {"executed_lines": [108, 113], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 107}, "CorvusCore.conflict_check": {"executed_lines": [121, 122, 130, 134, 136, 137, 138, 139, 140, 141, 142, 143, 144, 148, 149, 150, 151], "summary": {"covered_lines": 17, "num_statements": 19, "percent_covered": 89.47368421052632, "percent_covered_display": "89", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 89.47368421052632, "percent_statements_covered_display": "89"}, "missing_lines": [146, 147], "excluded_lines": [], "start_line": 117}, "": {"executed_lines": [9, 11, 12, 13, 14, 15, 17, 18, 19, 22, 26, 33, 34, 41, 69, 82, 107, 117], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"CorvusCore": {"executed_lines": [35, 36, 37, 42, 43, 44, 45, 46, 47, 48, 49, 60, 61, 66, 83, 84, 85, 87, 88, 96, 97, 101, 105, 108, 113, 121, 122, 130, 134, 136, 137, 138, 139, 140, 141, 142, 143, 144, 148, 149, 150, 151], "summary": {"covered_lines": 42, "num_statements": 49, "percent_covered": 85.71428571428571, "percent_covered_display": "86", "missing_lines": 7, "excluded_lines": 0, "percent_statements_covered": 85.71428571428571, "percent_statements_covered_display": "86"}, "missing_lines": [70, 71, 76, 78, 86, 146, 147], "excluded_lines": [], "start_line": 33}, "": {"executed_lines": [9, 11, 12, 13, 14, 15, 17, 18, 19, 22, 23, 26, 29, 30, 33, 34, 41, 69, 82, 107, 117], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "corvus/hlc.py": {"executed_lines": [8, 9, 10, 13, 14, 19, 20, 21, 26, 27, 30, 31, 32, 33, 34, 35, 37, 38, 39, 40, 41, 42, 45, 47, 50, 51, 52, 53, 61], "summary": {"covered_lines": 29, "num_statements": 39, "percent_covered": 74.35897435897436, "percent_covered_display": "74", "missing_lines": 10, "excluded_lines": 0, "percent_statements_covered": 74.35897435897436, "percent_statements_covered_display": "74"}, "missing_lines": [22, 23, 24, 44, 54, 55, 56, 57, 59, 60], "excluded_lines": [], "functions": {"HLCTimestamp.__lt__": {"executed_lines": [20, 21], "summary": {"covered_lines": 2, "num_statements": 5, "percent_covered": 40.0, "percent_covered_display": "40", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 40.0, "percent_statements_covered_display": "40"}, "missing_lines": [22, 23, 24], "excluded_lines": [], "start_line": 19}, "HLCTimestamp.to_string": {"executed_lines": [27], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 26}, "HLC.__init__": {"executed_lines": [32, 33, 34, 35], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 31}, "HLC.now": {"executed_lines": [38, 39, 40, 41, 42, 45], "summary": {"covered_lines": 6, "num_statements": 7, "percent_covered": 85.71428571428571, "percent_covered_display": "86", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 85.71428571428571, "percent_statements_covered_display": "86"}, "missing_lines": [44], "excluded_lines": [], "start_line": 37}, "HLC.merge": {"executed_lines": [50, 51, 52, 53, 61], "summary": {"covered_lines": 5, "num_statements": 11, "percent_covered": 45.45454545454545, "percent_covered_display": "45", "missing_lines": 6, "excluded_lines": 0, "percent_statements_covered": 45.45454545454545, "percent_statements_covered_display": "45"}, "missing_lines": [54, 55, 56, 57, 59, 60], "excluded_lines": [], "start_line": 47}, "": {"executed_lines": [8, 9, 10, 13, 14, 19, 26, 30, 31, 37, 47], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"HLCTimestamp": {"executed_lines": [20, 21, 27], "summary": {"covered_lines": 3, "num_statements": 6, "percent_covered": 50.0, "percent_covered_display": "50", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 50.0, "percent_statements_covered_display": "50"}, "missing_lines": [22, 23, 24], "excluded_lines": [], "start_line": 14}, "HLC": {"executed_lines": [32, 33, 34, 35, 38, 39, 40, 41, 42, 45, 50, 51, 52, 53, 61], "summary": {"covered_lines": 15, "num_statements": 22, "percent_covered": 68.18181818181819, "percent_covered_display": "68", "missing_lines": 7, "excluded_lines": 0, "percent_statements_covered": 68.18181818181819, "percent_statements_covered_display": "68"}, "missing_lines": [44, 54, 55, 56, 57, 59, 60], "excluded_lines": [], "start_line": 30}, "": {"executed_lines": [8, 9, 10, 13, 14, 19, 26, 30, 31, 37, 47], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "corvus/state.py": {"executed_lines": [8, 10, 11, 12, 14, 59, 62, 63, 64, 65, 66, 67, 68, 70, 71, 72, 73, 74, 75], "summary": {"covered_lines": 19, "num_statements": 19, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"State.__init__": {"executed_lines": [63, 64, 65, 66, 67, 68], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 62}, "State.execute": {"executed_lines": [71, 72, 73, 74, 75], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 70}, "": {"executed_lines": [8, 10, 11, 12, 14, 59, 62, 70], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"State": {"executed_lines": [63, 64, 65, 66, 67, 68, 71, 72, 73, 74, 75], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 59}, "": {"executed_lines": [8, 10, 11, 12, 14, 59, 62, 70], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}}, "totals": {"covered_lines": 126, "num_statements": 229, "percent_covered": 55.02183406113537, "percent_covered_display": "55", "missing_lines": 103, "excluded_lines": 0, "percent_statements_covered": 55.02183406113537, "percent_statements_covered_display": "55"}} \ No newline at end of file +{"meta": {"format": 3, "version": "7.17.0", "timestamp": "2026-10-23T20:37:05.987337", "branch_coverage": false, "show_contexts": false}, "files": {"corvus/__init__.py": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "corvus/adapter.py": {"executed_lines": [17, 19, 20, 21, 22, 23, 26, 31, 32, 36, 39, 41, 42, 43, 44, 45, 47, 49, 50, 51, 52, 55, 57, 58, 59, 60, 62, 63, 64, 65, 66, 70, 71, 72, 74, 75, 79, 80, 81, 85, 86, 91, 95, 98, 100, 101, 102, 104, 106, 108, 109, 110, 111, 112, 114], "summary": {"covered_lines": 55, "num_statements": 58, "percent_covered": 94.82758620689656, "percent_covered_display": "95", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 94.82758620689656, "percent_statements_covered_display": "95"}, "missing_lines": [69, 82, 113], "excluded_lines": [], "functions": {"Adapter.solve": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 32}, "ClefFlashAdapter.__init__": {"executed_lines": [41, 42, 43, 44, 45], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 39}, "ClefFlashAdapter.solve": {"executed_lines": [49, 50, 51, 52, 55, 57, 58, 59, 60, 62, 63, 64, 65, 66, 70, 71, 72, 74, 75, 79, 80, 81, 85, 86, 91], "summary": {"covered_lines": 25, "num_statements": 27, "percent_covered": 92.5925925925926, "percent_covered_display": "93", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 92.5925925925926, "percent_statements_covered_display": "93"}, "missing_lines": [69, 82], "excluded_lines": [], "start_line": 47}, "StubAdapter.__init__": {"executed_lines": [100, 101, 102], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 98}, "StubAdapter.solve": {"executed_lines": [106, 108, 109, 110, 111, 112, 114], "summary": {"covered_lines": 7, "num_statements": 8, "percent_covered": 87.5, "percent_covered_display": "88", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 87.5, "percent_statements_covered_display": "88"}, "missing_lines": [113], "excluded_lines": [], "start_line": 104}, "": {"executed_lines": [17, 19, 20, 21, 22, 23, 26, 31, 32, 36, 39, 47, 95, 98, 104], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"AdapterUnavailable": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 26}, "Adapter": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 31}, "ClefFlashAdapter": {"executed_lines": [41, 42, 43, 44, 45, 49, 50, 51, 52, 55, 57, 58, 59, 60, 62, 63, 64, 65, 66, 70, 71, 72, 74, 75, 79, 80, 81, 85, 86, 91], "summary": {"covered_lines": 30, "num_statements": 32, "percent_covered": 93.75, "percent_covered_display": "94", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 93.75, "percent_statements_covered_display": "94"}, "missing_lines": [69, 82], "excluded_lines": [], "start_line": 36}, "StubAdapter": {"executed_lines": [100, 101, 102, 106, 108, 109, 110, 111, 112, 114], "summary": {"covered_lines": 10, "num_statements": 11, "percent_covered": 90.9090909090909, "percent_covered_display": "91", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 90.9090909090909, "percent_statements_covered_display": "91"}, "missing_lines": [113], "excluded_lines": [], "start_line": 95}, "": {"executed_lines": [17, 19, 20, 21, 22, 23, 26, 31, 32, 36, 39, 47, 95, 98, 104], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "corvus/app.py": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 78, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 78, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [15, 17, 18, 19, 20, 21, 23, 25, 26, 27, 29, 30, 31, 34, 35, 36, 37, 38, 39, 42, 43, 44, 45, 46, 47, 48, 51, 52, 53, 54, 55, 58, 59, 60, 61, 62, 64, 67, 68, 70, 71, 72, 75, 76, 77, 78, 79, 80, 81, 82, 83, 85, 86, 88, 90, 91, 92, 93, 95, 96, 97, 98, 99, 101, 103, 105, 106, 107, 108, 109, 110, 112, 113, 114, 116, 117, 119, 121], "excluded_lines": [], "functions": {"_auth_key": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 4, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [52, 53, 54, 55], "excluded_lines": [], "start_line": 51}, "create_app": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 21, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 21, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [59, 60, 61, 62, 64, 67, 70, 71, 75, 76, 85, 86, 90, 91, 105, 106, 112, 113, 116, 117, 121], "excluded_lines": [], "start_line": 58}, "create_app.require_key": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [68], "excluded_lines": [], "start_line": 67}, "create_app.health": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [72], "excluded_lines": [], "start_line": 71}, "create_app.ingest": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 7, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 7, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [77, 78, 79, 80, 81, 82, 83], "excluded_lines": [], "start_line": 76}, "create_app.events": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [88], "excluded_lines": [], "start_line": 86}, "create_app.stream": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 4, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [92, 93, 95, 103], "excluded_lines": [], "start_line": 91}, "create_app.stream.gen": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [96, 97, 98, 99, 101], "excluded_lines": [], "start_line": 95}, "create_app.open_change": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 4, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [107, 108, 109, 110], "excluded_lines": [], "start_line": 106}, "create_app.active_changes": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [114], "excluded_lines": [], "start_line": 113}, "create_app.conflict_check": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [119], "excluded_lines": [], "start_line": 117}, "": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 28, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 28, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [15, 17, 18, 19, 20, 21, 23, 25, 26, 27, 29, 30, 31, 34, 35, 36, 37, 38, 39, 42, 43, 44, 45, 46, 47, 48, 51, 58], "excluded_lines": [], "start_line": 1}}, "classes": {"EventIn": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 34}, "ChangeIn": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 42}, "": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 78, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 78, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [15, 17, 18, 19, 20, 21, 23, 25, 26, 27, 29, 30, 31, 34, 35, 36, 37, 38, 39, 42, 43, 44, 45, 46, 47, 48, 51, 52, 53, 54, 55, 58, 59, 60, 61, 62, 64, 67, 68, 70, 71, 72, 75, 76, 77, 78, 79, 80, 81, 82, 83, 85, 86, 88, 90, 91, 92, 93, 95, 96, 97, 98, 99, 101, 103, 105, 106, 107, 108, 109, 110, 112, 113, 114, 116, 117, 119, 121], "excluded_lines": [], "start_line": 1}}}, "corvus/attested.py": {"executed_lines": [22, 24, 25, 26, 27, 28, 30, 32, 33, 36, 37, 38, 39, 40, 41, 50, 51, 53, 56, 58, 59, 61, 62, 63, 64, 67, 71, 72, 76, 77, 78, 84, 87, 91, 96, 97, 98, 99, 102, 103, 104, 107, 109, 110, 112, 113, 115, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 132, 133, 134, 135, 137, 138, 141], "summary": {"covered_lines": 64, "num_statements": 68, "percent_covered": 94.11764705882354, "percent_covered_display": "94", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 94.11764705882354, "percent_statements_covered_display": "94"}, "missing_lines": [79, 105, 129, 131], "excluded_lines": [], "functions": {"AttestedStore.__init__": {"executed_lines": [38, 39, 40, 41, 50, 51], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 37}, "AttestedStore.append": {"executed_lines": [56, 58, 59, 61, 62, 63, 64, 67, 71, 72], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 53}, "AttestedStore.chain": {"executed_lines": [77, 78, 84, 87], "summary": {"covered_lines": 4, "num_statements": 5, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80"}, "missing_lines": [79], "excluded_lines": [], "start_line": 76}, "AttestedStore.verify": {"executed_lines": [96, 97, 98, 99, 102, 103, 104, 107, 109, 110, 112, 113], "summary": {"covered_lines": 12, "num_statements": 13, "percent_covered": 92.3076923076923, "percent_covered_display": "92", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 92.3076923076923, "percent_statements_covered_display": "92"}, "missing_lines": [105], "excluded_lines": [], "start_line": 91}, "AttestedStore.lifecycle_walk": {"executed_lines": [119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 132, 133, 134, 135, 137, 138, 141], "summary": {"covered_lines": 17, "num_statements": 19, "percent_covered": 89.47368421052632, "percent_covered_display": "89", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 89.47368421052632, "percent_statements_covered_display": "89"}, "missing_lines": [129, 131], "excluded_lines": [], "start_line": 115}, "": {"executed_lines": [22, 24, 25, 26, 27, 28, 30, 32, 33, 36, 37, 53, 76, 91, 115], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"AttestedStore": {"executed_lines": [38, 39, 40, 41, 50, 51, 56, 58, 59, 61, 62, 63, 64, 67, 71, 72, 77, 78, 84, 87, 96, 97, 98, 99, 102, 103, 104, 107, 109, 110, 112, 113, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 132, 133, 134, 135, 137, 138, 141], "summary": {"covered_lines": 49, "num_statements": 53, "percent_covered": 92.45283018867924, "percent_covered_display": "92", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 92.45283018867924, "percent_statements_covered_display": "92"}, "missing_lines": [79, 105, 129, 131], "excluded_lines": [], "start_line": 36}, "": {"executed_lines": [22, 24, 25, 26, 27, 28, 30, 32, 33, 36, 37, 53, 76, 91, 115], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "corvus/canonical.py": {"executed_lines": [10, 12, 13, 14, 17, 19, 20, 21, 22, 23, 25, 28, 30, 31, 34, 35, 38, 42, 43], "summary": {"covered_lines": 19, "num_statements": 21, "percent_covered": 90.47619047619048, "percent_covered_display": "90", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 90.47619047619048, "percent_statements_covered_display": "90"}, "missing_lines": [24, 29], "excluded_lines": [], "functions": {"canonicalize": {"executed_lines": [19, 20, 21, 22, 23, 25, 28, 30, 31], "summary": {"covered_lines": 9, "num_statements": 11, "percent_covered": 81.81818181818181, "percent_covered_display": "82", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 81.81818181818181, "percent_statements_covered_display": "82"}, "missing_lines": [24, 29], "excluded_lines": [], "start_line": 17}, "canonical_json": {"executed_lines": [35], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 34}, "request_hash": {"executed_lines": [42, 43], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 38}, "": {"executed_lines": [10, 12, 13, 14, 17, 34, 38], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [10, 12, 13, 14, 17, 19, 20, 21, 22, 23, 25, 28, 30, 31, 34, 35, 38, 42, 43], "summary": {"covered_lines": 19, "num_statements": 21, "percent_covered": 90.47619047619048, "percent_covered_display": "90", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 90.47619047619048, "percent_statements_covered_display": "90"}, "missing_lines": [24, 29], "excluded_lines": [], "start_line": 1}}}, "corvus/capture.py": {"executed_lines": [22, 24, 25, 26, 27, 30, 31, 32, 33, 34, 35, 36, 37, 38, 45, 48, 50, 54, 55, 56, 58, 59, 60, 63, 64, 69, 70, 72, 73, 74, 77, 79, 80, 81, 83, 85, 88, 89, 92, 94, 95, 96, 97], "summary": {"covered_lines": 43, "num_statements": 43, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"CaptureStore.__init__": {"executed_lines": [32, 33, 34, 35, 36, 37, 38, 45, 48], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 31}, "CaptureStore.enqueue": {"executed_lines": [54, 55, 56, 58, 59, 60, 63, 64, 69, 70], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 50}, "CaptureStore.replay_put": {"executed_lines": [73, 74, 77], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 72}, "CaptureStore.replay_lookup": {"executed_lines": [80, 81, 83], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 79}, "CaptureStore.by_request_hash": {"executed_lines": [88, 89, 92], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 85}, "CaptureStore.stats": {"executed_lines": [95, 96, 97], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 94}, "": {"executed_lines": [22, 24, 25, 26, 27, 30, 31, 50, 72, 79, 85, 94], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"CaptureStore": {"executed_lines": [32, 33, 34, 35, 36, 37, 38, 45, 48, 54, 55, 56, 58, 59, 60, 63, 64, 69, 70, 73, 74, 77, 80, 81, 83, 88, 89, 92, 95, 96, 97], "summary": {"covered_lines": 31, "num_statements": 31, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 30}, "": {"executed_lines": [22, 24, 25, 26, 27, 30, 31, 50, 72, 79, 85, 94], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "corvus/cc_adapter.py": {"executed_lines": [26, 28, 29, 30, 31, 34, 35, 41, 42, 43, 44, 47, 49, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 68, 69, 70, 71, 76, 79, 80, 84, 89, 90, 93, 96, 97, 98, 101, 102, 109, 112, 113, 114, 116, 117, 119, 121, 123, 124, 128, 137], "summary": {"covered_lines": 51, "num_statements": 58, "percent_covered": 87.93103448275862, "percent_covered_display": "88", "missing_lines": 7, "excluded_lines": 0, "percent_statements_covered": 87.93103448275862, "percent_statements_covered_display": "88"}, "missing_lines": [105, 106, 122, 125, 133, 134, 138], "excluded_lines": [], "functions": {"CCAdapter.__init__": {"executed_lines": [41, 42, 43, 44, 47], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 35}, "CCAdapter.pre_tool_use": {"executed_lines": [52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 68, 69, 70, 71, 76, 79, 80, 84, 89, 90, 93, 96, 97, 98, 101, 102], "summary": {"covered_lines": 26, "num_statements": 28, "percent_covered": 92.85714285714286, "percent_covered_display": "93", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 92.85714285714286, "percent_statements_covered_display": "93"}, "missing_lines": [105, 106], "excluded_lines": [], "start_line": 49}, "CCAdapter.post_tool_use": {"executed_lines": [112, 113, 114, 116, 117], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 109}, "CCAdapter.handle": {"executed_lines": [121, 123, 124], "summary": {"covered_lines": 3, "num_statements": 5, "percent_covered": 60.0, "percent_covered_display": "60", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 60.0, "percent_statements_covered_display": "60"}, "missing_lines": [122, 125], "excluded_lines": [], "start_line": 119}, "main": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [133, 134], "excluded_lines": [], "start_line": 128}, "": {"executed_lines": [26, 28, 29, 30, 31, 34, 35, 49, 109, 119, 128, 137], "summary": {"covered_lines": 12, "num_statements": 13, "percent_covered": 92.3076923076923, "percent_covered_display": "92", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 92.3076923076923, "percent_statements_covered_display": "92"}, "missing_lines": [138], "excluded_lines": [], "start_line": 1}}, "classes": {"CCAdapter": {"executed_lines": [41, 42, 43, 44, 47, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 68, 69, 70, 71, 76, 79, 80, 84, 89, 90, 93, 96, 97, 98, 101, 102, 112, 113, 114, 116, 117, 121, 123, 124], "summary": {"covered_lines": 39, "num_statements": 43, "percent_covered": 90.69767441860465, "percent_covered_display": "91", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 90.69767441860465, "percent_statements_covered_display": "91"}, "missing_lines": [105, 106, 122, 125], "excluded_lines": [], "start_line": 34}, "": {"executed_lines": [26, 28, 29, 30, 31, 34, 35, 49, 109, 119, 128, 137], "summary": {"covered_lines": 12, "num_statements": 15, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80"}, "missing_lines": [133, 134, 138], "excluded_lines": [], "start_line": 1}}}, "corvus/compiler.py": {"executed_lines": [17, 19, 20, 21, 26, 29, 33, 43, 46, 47, 48, 49, 50, 52, 55, 62, 63, 64, 66, 69, 70, 71, 77, 78, 79, 80, 81, 83, 88, 89, 90, 94, 95, 96, 100, 101, 102, 103, 104, 106, 109, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 124, 125, 126, 127, 129, 130, 131, 132, 134, 135, 153, 156, 160, 161, 163, 164, 165, 167, 168, 169, 170, 173, 176, 177, 179, 185, 186, 187, 189, 190, 195, 198, 199, 201, 202, 203, 204, 206, 209, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 227, 228, 231], "summary": {"covered_lines": 104, "num_statements": 122, "percent_covered": 85.24590163934427, "percent_covered_display": "85", "missing_lines": 18, "excluded_lines": 0, "percent_statements_covered": 85.24590163934427, "percent_statements_covered_display": "85"}, "missing_lines": [36, 37, 38, 39, 40, 72, 171, 172, 174, 175, 181, 191, 192, 194, 200, 205, 229, 230], "excluded_lines": [], "functions": {"_fail_answer_index": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [36, 37, 38, 39, 40], "excluded_lines": [], "start_line": 33}, "validate_register": {"executed_lines": [46, 47, 48, 49, 50, 52, 55, 62, 63, 64, 66, 69, 70, 71, 77, 78, 79, 80, 81, 83, 88, 89, 90, 94, 95, 96, 100, 101, 102, 103, 104, 106], "summary": {"covered_lines": 32, "num_statements": 33, "percent_covered": 96.96969696969697, "percent_covered_display": "97", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 96.96969696969697, "percent_statements_covered_display": "97"}, "missing_lines": [72], "excluded_lines": [], "start_line": 43}, "_coverage": {"executed_lines": [112, 113, 114, 115, 116, 117, 118, 119, 120, 121], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 109}, "compile_catalog": {"executed_lines": [125, 126, 127, 129, 130, 131, 132, 134, 135, 153], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 124}, "diff_classify": {"executed_lines": [160, 161, 163, 164, 165, 167, 168, 169, 170, 173, 176, 177, 179, 185, 186, 187, 189, 190, 195, 198, 199, 201, 202, 203, 204, 206], "summary": {"covered_lines": 26, "num_statements": 36, "percent_covered": 72.22222222222223, "percent_covered_display": "72", "missing_lines": 10, "excluded_lines": 0, "percent_statements_covered": 72.22222222222223, "percent_statements_covered_display": "72"}, "missing_lines": [171, 172, 174, 175, 181, 191, 192, 194, 200, 205], "excluded_lines": [], "start_line": 156}, "build": {"executed_lines": [212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 227, 228, 231], "summary": {"covered_lines": 14, "num_statements": 16, "percent_covered": 87.5, "percent_covered_display": "88", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 87.5, "percent_statements_covered_display": "88"}, "missing_lines": [229, 230], "excluded_lines": [], "start_line": 209}, "": {"executed_lines": [17, 19, 20, 21, 26, 29, 33, 43, 109, 124, 156, 209], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"CompilerError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 29}, "": {"executed_lines": [17, 19, 20, 21, 26, 29, 33, 43, 46, 47, 48, 49, 50, 52, 55, 62, 63, 64, 66, 69, 70, 71, 77, 78, 79, 80, 81, 83, 88, 89, 90, 94, 95, 96, 100, 101, 102, 103, 104, 106, 109, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 124, 125, 126, 127, 129, 130, 131, 132, 134, 135, 153, 156, 160, 161, 163, 164, 165, 167, 168, 169, 170, 173, 176, 177, 179, 185, 186, 187, 189, 190, 195, 198, 199, 201, 202, 203, 204, 206, 209, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 227, 228, 231], "summary": {"covered_lines": 104, "num_statements": 122, "percent_covered": 85.24590163934427, "percent_covered_display": "85", "missing_lines": 18, "excluded_lines": 0, "percent_statements_covered": 85.24590163934427, "percent_statements_covered_display": "85"}, "missing_lines": [36, 37, 38, 39, 40, 72, 171, 172, 174, 175, 181, 191, 192, 194, 200, 205, 229, 230], "excluded_lines": [], "start_line": 1}}}, "corvus/config.py": {"executed_lines": [7, 9, 10, 11, 15, 36, 37, 38, 39, 40, 41, 43, 44, 45, 48], "summary": {"covered_lines": 15, "num_statements": 23, "percent_covered": 65.21739130434783, "percent_covered_display": "65", "missing_lines": 8, "excluded_lines": 0, "percent_statements_covered": 65.21739130434783, "percent_statements_covered_display": "65"}, "missing_lines": [55, 56, 57, 58, 66, 67, 68, 73], "excluded_lines": [], "functions": {"FleetConfig.event_types": {"executed_lines": [45], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 44}, "load_config": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 8, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 8, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [55, 56, 57, 58, 66, 67, 68, 73], "excluded_lines": [], "start_line": 48}, "": {"executed_lines": [7, 9, 10, 11, 15, 36, 37, 38, 39, 40, 41, 43, 44, 48], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"FleetConfig": {"executed_lines": [45], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 37}, "": {"executed_lines": [7, 9, 10, 11, 15, 36, 37, 38, 39, 40, 41, 43, 44, 48], "summary": {"covered_lines": 14, "num_statements": 22, "percent_covered": 63.63636363636363, "percent_covered_display": "64", "missing_lines": 8, "excluded_lines": 0, "percent_statements_covered": 63.63636363636363, "percent_statements_covered_display": "64"}, "missing_lines": [55, 56, 57, 58, 66, 67, 68, 73], "excluded_lines": [], "start_line": 1}}}, "corvus/config_loader.py": {"executed_lines": [14, 16, 17, 18, 21, 22, 23, 25, 29, 30, 31, 32, 34, 38, 39, 41, 42], "summary": {"covered_lines": 17, "num_statements": 18, "percent_covered": 94.44444444444444, "percent_covered_display": "94", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 94.44444444444444, "percent_statements_covered_display": "94"}, "missing_lines": [40], "excluded_lines": [], "functions": {"ConfigLoader.__init__": {"executed_lines": [23], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 22}, "ConfigLoader.load_fleet_config": {"executed_lines": [29, 30, 31, 32], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 25}, "ConfigLoader.load_register": {"executed_lines": [38, 39, 41, 42], "summary": {"covered_lines": 4, "num_statements": 5, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80"}, "missing_lines": [40], "excluded_lines": [], "start_line": 34}, "": {"executed_lines": [14, 16, 17, 18, 21, 22, 25, 34], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"ConfigLoader": {"executed_lines": [23, 29, 30, 31, 32, 38, 39, 41, 42], "summary": {"covered_lines": 9, "num_statements": 10, "percent_covered": 90.0, "percent_covered_display": "90", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 90.0, "percent_statements_covered_display": "90"}, "missing_lines": [40], "excluded_lines": [], "start_line": 21}, "": {"executed_lines": [14, 16, 17, 18, 21, 22, 25, 34], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "corvus/conformance.py": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 66, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 66, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [31, 33, 34, 35, 36, 38, 39, 42, 66, 75, 77, 78, 79, 80, 81, 82, 85, 86, 87, 88, 92, 93, 94, 98, 99, 100, 104, 105, 106, 107, 108, 111, 116, 117, 118, 120, 121, 122, 123, 124, 125, 127, 128, 131, 132, 137, 141, 142, 143, 144, 146, 147, 148, 149, 150, 154, 155, 156, 157, 158, 159, 160, 162, 163, 164, 168], "excluded_lines": [], "functions": {"run_mutation_suite": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 17, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 17, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [75, 77, 78, 79, 80, 81, 82, 85, 86, 87, 88, 92, 93, 94, 98, 99, 100], "excluded_lines": [], "start_line": 66}, "_extract_red_test": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 4, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [105, 106, 107, 108], "excluded_lines": [], "start_line": 104}, "conformance_run": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 10, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 10, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [116, 117, 118, 120, 131, 146, 162, 163, 164, 168], "excluded_lines": [], "start_line": 111}, "conformance_run.check": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 7, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 7, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [121, 122, 123, 124, 125, 127, 128], "excluded_lines": [], "start_line": 120}, "conformance_run.happy_path": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 6, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 6, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [132, 137, 141, 142, 143, 144], "excluded_lines": [], "start_line": 131}, "conformance_run.existence_hiding": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 11, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 11, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [147, 148, 149, 150, 154, 155, 156, 157, 158, 159, 160], "excluded_lines": [], "start_line": 146}, "": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 11, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 11, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [31, 33, 34, 35, 36, 38, 39, 42, 66, 104, 111], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 66, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 66, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [31, 33, 34, 35, 36, 38, 39, 42, 66, 75, 77, 78, 79, 80, 81, 82, 85, 86, 87, 88, 92, 93, 94, 98, 99, 100, 104, 105, 106, 107, 108, 111, 116, 117, 118, 120, 121, 122, 123, 124, 125, 127, 128, 131, 132, 137, 141, 142, 143, 144, 146, 147, 148, 149, 150, 154, 155, 156, 157, 158, 159, 160, 162, 163, 164, 168], "excluded_lines": [], "start_line": 1}}}, "corvus/contract_codes.py": {"executed_lines": [8], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [8], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [8], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "corvus/core.py": {"executed_lines": [9, 11, 12, 13, 14, 15, 16, 18, 19, 20, 23, 24, 27, 30, 31, 34, 35, 36, 37, 38, 39, 43, 44, 45, 46, 47, 48, 49, 50, 51, 62, 63, 68, 71, 84, 85, 86, 87, 89, 90, 98, 99, 103, 107, 109, 110, 115, 119, 123, 124, 132, 136, 142, 143, 144, 145, 146, 147, 148, 150, 151, 152, 153, 193, 194, 195, 196, 197, 198, 199, 203, 204, 205, 206], "summary": {"covered_lines": 74, "num_statements": 98, "percent_covered": 75.51020408163265, "percent_covered_display": "76", "missing_lines": 24, "excluded_lines": 0, "percent_statements_covered": 75.51020408163265, "percent_statements_covered_display": "76"}, "missing_lines": [72, 73, 78, 80, 88, 154, 163, 169, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 186, 188, 189, 201, 202], "excluded_lines": [], "functions": {"_now_iso": {"executed_lines": [24], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 23}, "sign_event": {"executed_lines": [30, 31], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 27}, "CorvusCore.__init__": {"executed_lines": [36, 37, 38, 39], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 35}, "CorvusCore.ingest_event": {"executed_lines": [44, 45, 46, 47, 48, 49, 50, 51, 62, 63, 68], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 43}, "CorvusCore.query_events": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 4, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [72, 73, 78, 80], "excluded_lines": [], "start_line": 71}, "CorvusCore.open_change": {"executed_lines": [85, 86, 87, 89, 90, 98, 99, 103, 107], "summary": {"covered_lines": 9, "num_statements": 10, "percent_covered": 90.0, "percent_covered_display": "90", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 90.0, "percent_statements_covered_display": "90"}, "missing_lines": [88], "excluded_lines": [], "start_line": 84}, "CorvusCore.active_changes_for": {"executed_lines": [110, 115], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 109}, "CorvusCore.conflict_check": {"executed_lines": [123, 124, 132, 136, 142, 143, 144, 145, 146, 147, 148, 150, 151, 152, 153, 193, 194, 195, 196, 197, 198, 199, 203, 204, 205, 206], "summary": {"covered_lines": 26, "num_statements": 45, "percent_covered": 57.77777777777778, "percent_covered_display": "58", "missing_lines": 19, "excluded_lines": 0, "percent_statements_covered": 57.77777777777778, "percent_statements_covered_display": "58"}, "missing_lines": [154, 163, 169, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 186, 188, 189, 201, 202], "excluded_lines": [], "start_line": 119}, "": {"executed_lines": [9, 11, 12, 13, 14, 15, 16, 18, 19, 20, 23, 27, 34, 35, 43, 71, 84, 109, 119], "summary": {"covered_lines": 19, "num_statements": 19, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"CorvusCore": {"executed_lines": [36, 37, 38, 39, 44, 45, 46, 47, 48, 49, 50, 51, 62, 63, 68, 85, 86, 87, 89, 90, 98, 99, 103, 107, 110, 115, 123, 124, 132, 136, 142, 143, 144, 145, 146, 147, 148, 150, 151, 152, 153, 193, 194, 195, 196, 197, 198, 199, 203, 204, 205, 206], "summary": {"covered_lines": 52, "num_statements": 76, "percent_covered": 68.42105263157895, "percent_covered_display": "68", "missing_lines": 24, "excluded_lines": 0, "percent_statements_covered": 68.42105263157895, "percent_statements_covered_display": "68"}, "missing_lines": [72, 73, 78, 80, 88, 154, 163, 169, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 186, 188, 189, 201, 202], "excluded_lines": [], "start_line": 34}, "": {"executed_lines": [9, 11, 12, 13, 14, 15, 16, 18, 19, 20, 23, 24, 27, 30, 31, 34, 35, 43, 71, 84, 109, 119], "summary": {"covered_lines": 22, "num_statements": 22, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "corvus/decision_core.py": {"executed_lines": [20, 22, 23, 24, 27, 28, 31, 34, 37, 43, 44, 45, 48, 50, 51, 52, 53, 54, 55, 58, 59, 60, 64, 65, 66, 67, 68, 69, 71, 72, 74, 75, 77, 80, 82, 83, 84, 85, 87, 88, 91, 92, 93, 95, 97, 98, 99, 103, 104, 106, 107, 111, 112, 114, 127, 129, 130, 131, 133, 134, 135, 136, 138, 140, 142, 144, 145, 146, 149, 150, 152, 154, 155, 156, 158, 162, 163, 164, 167, 168, 169, 171, 172, 173, 174, 175, 176], "summary": {"covered_lines": 87, "num_statements": 100, "percent_covered": 87.0, "percent_covered_display": "87", "missing_lines": 13, "excluded_lines": 0, "percent_statements_covered": 87.0, "percent_statements_covered_display": "87"}, "missing_lines": [86, 89, 116, 117, 118, 119, 120, 122, 123, 124, 125, 141, 143], "excluded_lines": [], "functions": {"DecisionCore.__init__": {"executed_lines": [43, 44, 45], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 37}, "DecisionCore.solve": {"executed_lines": [50, 51, 52, 53, 54, 55, 58, 59, 60, 64, 65, 66, 67, 68, 69, 71, 72, 74, 75, 77], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 48}, "DecisionCore._dispatch": {"executed_lines": [82, 83, 84, 85, 87, 88], "summary": {"covered_lines": 6, "num_statements": 8, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75"}, "missing_lines": [86, 89], "excluded_lines": [], "start_line": 80}, "DecisionCore._check_deadline": {"executed_lines": [92, 93], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 91}, "DecisionCore._dispatch_rule": {"executed_lines": [97, 98, 99, 103, 104, 106, 107, 111, 112], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 95}, "DecisionCore._dispatch_lookup": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 9, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 9, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [116, 117, 118, 119, 120, 122, 123, 124, 125], "excluded_lines": [], "start_line": 114}, "DecisionCore._dispatch_model": {"executed_lines": [129, 130, 131, 133, 134, 135, 136, 138, 140, 142, 144, 145, 146], "summary": {"covered_lines": 13, "num_statements": 15, "percent_covered": 86.66666666666667, "percent_covered_display": "87", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 86.66666666666667, "percent_statements_covered_display": "87"}, "missing_lines": [141, 143], "excluded_lines": [], "start_line": 127}, "DecisionCore._full": {"executed_lines": [152, 154, 155, 156], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 150}, "DecisionCore._fail_cell": {"executed_lines": [162, 163, 164, 167, 168, 169], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 158}, "DecisionCore._failed_dependency": {"executed_lines": [173, 174, 175, 176], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 172}, "": {"executed_lines": [20, 22, 23, 24, 27, 28, 31, 34, 37, 48, 80, 91, 95, 114, 127, 149, 150, 158, 171, 172], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"SliceExceeded": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 27}, "DecisionCore": {"executed_lines": [43, 44, 45, 50, 51, 52, 53, 54, 55, 58, 59, 60, 64, 65, 66, 67, 68, 69, 71, 72, 74, 75, 77, 82, 83, 84, 85, 87, 88, 92, 93, 97, 98, 99, 103, 104, 106, 107, 111, 112, 129, 130, 131, 133, 134, 135, 136, 138, 140, 142, 144, 145, 146, 152, 154, 155, 156, 162, 163, 164, 167, 168, 169, 173, 174, 175, 176], "summary": {"covered_lines": 67, "num_statements": 80, "percent_covered": 83.75, "percent_covered_display": "84", "missing_lines": 13, "excluded_lines": 0, "percent_statements_covered": 83.75, "percent_statements_covered_display": "84"}, "missing_lines": [86, 89, 116, 117, 118, 119, 120, 122, 123, 124, 125, 141, 143], "excluded_lines": [], "start_line": 34}, "": {"executed_lines": [20, 22, 23, 24, 27, 28, 31, 34, 37, 48, 80, 91, 95, 114, 127, 149, 150, 158, 171, 172], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "corvus/decisions.py": {"executed_lines": [9, 11, 12, 13, 14, 17, 18, 19, 20, 21, 23, 33, 34, 36, 41, 42, 43, 48, 49, 57, 58, 60, 61, 62, 63, 64, 65, 72], "summary": {"covered_lines": 28, "num_statements": 28, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"DecisionClient.__init__": {"executed_lines": [19, 20, 21], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 18}, "DecisionClient.decide": {"executed_lines": [33, 34, 36, 41, 42, 43, 48, 49, 57, 58, 60, 61, 62, 63, 64, 65, 72], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 23}, "": {"executed_lines": [9, 11, 12, 13, 14, 17, 18, 23], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"DecisionClient": {"executed_lines": [19, 20, 21, 33, 34, 36, 41, 42, 43, 48, 49, 57, 58, 60, 61, 62, 63, 64, 65, 72], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 17}, "": {"executed_lines": [9, 11, 12, 13, 14, 17, 18, 23], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "corvus/delivery.py": {"executed_lines": [13, 15, 18, 19, 25, 26, 27, 28, 29, 31, 35, 37, 38, 41, 43, 47, 48, 50, 51, 52, 53, 54, 55, 56, 60, 61, 63, 64, 66, 70, 71, 72, 73, 74, 75, 76, 77, 80, 81, 82, 83, 84, 89, 90, 92, 96, 97, 99, 103, 105, 106], "summary": {"covered_lines": 51, "num_statements": 55, "percent_covered": 92.72727272727273, "percent_covered_display": "93", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 92.72727272727273, "percent_statements_covered_display": "93"}, "missing_lines": [49, 78, 79, 104], "excluded_lines": [], "functions": {"DeliveryChain.__init__": {"executed_lines": [25, 26, 27, 28, 29], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 19}, "DeliveryChain.submit": {"executed_lines": [35, 37, 38, 41], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 31}, "DeliveryChain.deliver": {"executed_lines": [47, 48, 50, 51, 52, 53, 54, 55, 56, 60, 61, 63, 64], "summary": {"covered_lines": 13, "num_statements": 14, "percent_covered": 92.85714285714286, "percent_covered_display": "93", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 92.85714285714286, "percent_statements_covered_display": "93"}, "missing_lines": [49], "excluded_lines": [], "start_line": 43}, "DeliveryChain.deliver_on_recovery": {"executed_lines": [70, 71, 72, 73, 74, 75, 76, 77, 80, 81, 82, 83, 84, 89, 90, 92, 96, 97], "summary": {"covered_lines": 18, "num_statements": 20, "percent_covered": 90.0, "percent_covered_display": "90", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 90.0, "percent_statements_covered_display": "90"}, "missing_lines": [78, 79], "excluded_lines": [], "start_line": 66}, "DeliveryChain.route_invalid": {"executed_lines": [103, 105, 106], "summary": {"covered_lines": 3, "num_statements": 4, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75"}, "missing_lines": [104], "excluded_lines": [], "start_line": 99}, "": {"executed_lines": [13, 15, 18, 19, 31, 43, 66, 99], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"DeliveryChain": {"executed_lines": [25, 26, 27, 28, 29, 35, 37, 38, 41, 47, 48, 50, 51, 52, 53, 54, 55, 56, 60, 61, 63, 64, 70, 71, 72, 73, 74, 75, 76, 77, 80, 81, 82, 83, 84, 89, 90, 92, 96, 97, 103, 105, 106], "summary": {"covered_lines": 43, "num_statements": 47, "percent_covered": 91.48936170212765, "percent_covered_display": "91", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 91.48936170212765, "percent_statements_covered_display": "91"}, "missing_lines": [49, 78, 79, 104], "excluded_lines": [], "start_line": 18}, "": {"executed_lines": [13, 15, 18, 19, 31, 43, 66, 99], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "corvus/engine_shim.py": {"executed_lines": [15, 17, 18, 19, 20, 22, 24, 25, 27, 28, 29, 31, 33, 41, 43, 44, 53, 54, 57, 66, 67, 68, 69, 71, 72, 73, 75, 76, 77, 78, 79, 80, 83, 84, 92, 94, 97, 103, 104, 106, 107, 108, 111, 112, 113, 117, 118, 119, 125, 126, 127, 128, 129, 130, 131, 133, 134, 135, 139, 140, 141, 142, 143, 144, 146, 147, 149, 153, 157, 158, 159, 160, 161, 163, 164, 165, 166, 177, 178, 179, 180, 181, 182, 183, 184, 185, 189, 195, 196, 197, 198, 199, 200, 201, 204, 205, 207, 208, 209, 211, 212, 217, 218, 219, 220, 221, 222, 223, 224, 226, 227, 230, 234, 236, 237, 238, 239, 264, 274, 281, 282, 283, 284, 287, 288, 289, 292], "summary": {"covered_lines": 127, "num_statements": 147, "percent_covered": 86.39455782312925, "percent_covered_display": "86", "missing_lines": 20, "excluded_lines": 0, "percent_statements_covered": 86.39455782312925, "percent_statements_covered_display": "86"}, "missing_lines": [70, 105, 145, 162, 225, 229, 231, 245, 246, 247, 248, 249, 256, 257, 293, 294, 295, 296, 297, 298], "excluded_lines": [], "functions": {"err": {"executed_lines": [43, 44], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 41}, "_register_candidates": {"executed_lines": [67, 68, 69, 71, 72, 73], "summary": {"covered_lines": 6, "num_statements": 7, "percent_covered": 85.71428571428571, "percent_covered_display": "86", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 85.71428571428571, "percent_statements_covered_display": "86"}, "missing_lines": [70], "excluded_lines": [], "start_line": 66}, "_site_resolution": {"executed_lines": [84], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 83}, "_quantize": {"executed_lines": [103, 104, 106, 107, 108], "summary": {"covered_lines": 5, "num_statements": 6, "percent_covered": 83.33333333333333, "percent_covered_display": "83", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 83.33333333333333, "percent_statements_covered_display": "83"}, "missing_lines": [105], "excluded_lines": [], "start_line": 97}, "decide": {"executed_lines": [113, 117, 118, 119, 125, 126, 127, 128, 129, 130, 131, 133, 134, 135, 139, 140, 141, 142, 143, 144, 146, 147, 149, 153, 157, 158, 159, 160, 161, 163, 164, 165, 166, 177, 178, 179, 180, 181, 182, 183, 184, 185, 189, 195, 196, 197, 198, 199, 200, 201, 204, 205, 207, 208, 209, 211, 212, 217, 218, 219, 220, 221, 222, 223, 224, 226, 227, 230, 234, 236, 237, 238, 239, 264, 274, 281, 282, 283, 284], "summary": {"covered_lines": 79, "num_statements": 91, "percent_covered": 86.81318681318682, "percent_covered_display": "87", "missing_lines": 12, "excluded_lines": 0, "percent_statements_covered": 86.81318681318682, "percent_statements_covered_display": "87"}, "missing_lines": [145, 162, 225, 229, 231, 245, 246, 247, 248, 249, 256, 257], "excluded_lines": [], "start_line": 112}, "hashlib_sha": {"executed_lines": [288, 289], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 287}, "": {"executed_lines": [15, 17, 18, 19, 20, 22, 24, 25, 27, 28, 29, 31, 33, 41, 53, 54, 57, 66, 75, 76, 77, 78, 79, 80, 83, 92, 94, 97, 111, 112, 287, 292], "summary": {"covered_lines": 32, "num_statements": 38, "percent_covered": 84.21052631578948, "percent_covered_display": "84", "missing_lines": 6, "excluded_lines": 0, "percent_statements_covered": 84.21052631578948, "percent_statements_covered_display": "84"}, "missing_lines": [293, 294, 295, 296, 297, 298], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [15, 17, 18, 19, 20, 22, 24, 25, 27, 28, 29, 31, 33, 41, 43, 44, 53, 54, 57, 66, 67, 68, 69, 71, 72, 73, 75, 76, 77, 78, 79, 80, 83, 84, 92, 94, 97, 103, 104, 106, 107, 108, 111, 112, 113, 117, 118, 119, 125, 126, 127, 128, 129, 130, 131, 133, 134, 135, 139, 140, 141, 142, 143, 144, 146, 147, 149, 153, 157, 158, 159, 160, 161, 163, 164, 165, 166, 177, 178, 179, 180, 181, 182, 183, 184, 185, 189, 195, 196, 197, 198, 199, 200, 201, 204, 205, 207, 208, 209, 211, 212, 217, 218, 219, 220, 221, 222, 223, 224, 226, 227, 230, 234, 236, 237, 238, 239, 264, 274, 281, 282, 283, 284, 287, 288, 289, 292], "summary": {"covered_lines": 127, "num_statements": 147, "percent_covered": 86.39455782312925, "percent_covered_display": "86", "missing_lines": 20, "excluded_lines": 0, "percent_statements_covered": 86.39455782312925, "percent_statements_covered_display": "86"}, "missing_lines": [70, 105, 145, 162, 225, 229, 231, 245, 246, 247, 248, 249, 256, 257, 293, 294, 295, 296, 297, 298], "excluded_lines": [], "start_line": 1}}}, "corvus/facts.py": {"executed_lines": [27, 29, 30, 33, 38, 41, 42, 46, 47, 49, 52, 53, 54, 55, 56, 57, 59, 60, 61, 62, 63, 68, 69, 70, 71, 72, 73, 74, 75, 76, 79, 81, 85, 87, 88, 89, 90, 91, 93, 94, 101, 104, 105, 106, 109, 110, 111, 122], "summary": {"covered_lines": 48, "num_statements": 51, "percent_covered": 94.11764705882354, "percent_covered_display": "94", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 94.11764705882354, "percent_statements_covered_display": "94"}, "missing_lines": [114, 117, 118], "excluded_lines": [], "functions": {"FactComposer.__init__": {"executed_lines": [46, 47], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 42}, "FactComposer.compose": {"executed_lines": [52, 53, 54, 55, 56, 57, 59, 60, 61, 62, 63, 68, 69, 70, 71, 72, 73, 74, 75, 76, 79], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 49}, "FactComposer.compose_status_view": {"executed_lines": [85, 87, 88, 89, 90, 91, 93, 94], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 81}, "change_window_source": {"executed_lines": [104, 122], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 101}, "change_window_source.source": {"executed_lines": [105, 106, 109, 110, 111], "summary": {"covered_lines": 5, "num_statements": 8, "percent_covered": 62.5, "percent_covered_display": "62", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 62.5, "percent_statements_covered_display": "62"}, "missing_lines": [114, 117, 118], "excluded_lines": [], "start_line": 104}, "": {"executed_lines": [27, 29, 30, 33, 38, 41, 42, 49, 81, 101], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"FactComposer": {"executed_lines": [46, 47, 52, 53, 54, 55, 56, 57, 59, 60, 61, 62, 63, 68, 69, 70, 71, 72, 73, 74, 75, 76, 79, 85, 87, 88, 89, 90, 91, 93, 94], "summary": {"covered_lines": 31, "num_statements": 31, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 41}, "": {"executed_lines": [27, 29, 30, 33, 38, 41, 42, 49, 81, 101, 104, 105, 106, 109, 110, 111, 122], "summary": {"covered_lines": 17, "num_statements": 20, "percent_covered": 85.0, "percent_covered_display": "85", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 85.0, "percent_statements_covered_display": "85"}, "missing_lines": [114, 117, 118], "excluded_lines": [], "start_line": 1}}}, "corvus/hlc.py": {"executed_lines": [8, 9, 10, 13, 14, 15, 16, 17, 19, 20, 21, 26, 27, 30, 31, 32, 33, 34, 35, 37, 38, 39, 40, 41, 42, 45, 47, 50, 51, 52, 53, 61], "summary": {"covered_lines": 32, "num_statements": 42, "percent_covered": 76.19047619047619, "percent_covered_display": "76", "missing_lines": 10, "excluded_lines": 0, "percent_statements_covered": 76.19047619047619, "percent_statements_covered_display": "76"}, "missing_lines": [22, 23, 24, 44, 54, 55, 56, 57, 59, 60], "excluded_lines": [], "functions": {"HLCTimestamp.__lt__": {"executed_lines": [20, 21], "summary": {"covered_lines": 2, "num_statements": 5, "percent_covered": 40.0, "percent_covered_display": "40", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 40.0, "percent_statements_covered_display": "40"}, "missing_lines": [22, 23, 24], "excluded_lines": [], "start_line": 19}, "HLCTimestamp.to_string": {"executed_lines": [27], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 26}, "HLC.__init__": {"executed_lines": [32, 33, 34, 35], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 31}, "HLC.now": {"executed_lines": [38, 39, 40, 41, 42, 45], "summary": {"covered_lines": 6, "num_statements": 7, "percent_covered": 85.71428571428571, "percent_covered_display": "86", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 85.71428571428571, "percent_statements_covered_display": "86"}, "missing_lines": [44], "excluded_lines": [], "start_line": 37}, "HLC.merge": {"executed_lines": [50, 51, 52, 53, 61], "summary": {"covered_lines": 5, "num_statements": 11, "percent_covered": 45.45454545454545, "percent_covered_display": "45", "missing_lines": 6, "excluded_lines": 0, "percent_statements_covered": 45.45454545454545, "percent_statements_covered_display": "45"}, "missing_lines": [54, 55, 56, 57, 59, 60], "excluded_lines": [], "start_line": 47}, "": {"executed_lines": [8, 9, 10, 13, 14, 15, 16, 17, 19, 26, 30, 31, 37, 47], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"HLCTimestamp": {"executed_lines": [20, 21, 27], "summary": {"covered_lines": 3, "num_statements": 6, "percent_covered": 50.0, "percent_covered_display": "50", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 50.0, "percent_statements_covered_display": "50"}, "missing_lines": [22, 23, 24], "excluded_lines": [], "start_line": 14}, "HLC": {"executed_lines": [32, 33, 34, 35, 38, 39, 40, 41, 42, 45, 50, 51, 52, 53, 61], "summary": {"covered_lines": 15, "num_statements": 22, "percent_covered": 68.18181818181819, "percent_covered_display": "68", "missing_lines": 7, "excluded_lines": 0, "percent_statements_covered": 68.18181818181819, "percent_statements_covered_display": "68"}, "missing_lines": [44, 54, 55, 56, 57, 59, 60], "excluded_lines": [], "start_line": 30}, "": {"executed_lines": [8, 9, 10, 13, 14, 15, 16, 17, 19, 26, 30, 31, 37, 47], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "corvus/observe.py": {"executed_lines": [21, 23, 26, 27, 31, 32, 33, 35, 41, 42, 46, 47, 49, 53, 54, 55, 58, 60, 63, 66, 70], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"ObserveMode.__init__": {"executed_lines": [31, 32, 33], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 27}, "ObserveMode.record_attempt": {"executed_lines": [41, 42, 46, 47, 49, 53, 54, 55, 58], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 35}, "ObserveMode.coverage": {"executed_lines": [63], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 60}, "ObserveMode.token_issuance_allowed": {"executed_lines": [70], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 66}, "": {"executed_lines": [21, 23, 26, 27, 35, 60, 66], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"ObserveMode": {"executed_lines": [31, 32, 33, 41, 42, 46, 47, 49, 53, 54, 55, 58, 63, 70], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 26}, "": {"executed_lines": [21, 23, 26, 27, 35, 60, 66], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "corvus/ops.py": {"executed_lines": [13, 15, 18, 21, 26, 28, 32, 33, 34, 37, 41, 42, 43, 44, 46, 47, 48, 49, 51, 52, 53, 54, 56, 57, 58, 61, 70, 71, 72, 74, 75, 76, 79, 80, 81, 82, 83, 84, 85, 87], "summary": {"covered_lines": 40, "num_statements": 41, "percent_covered": 97.5609756097561, "percent_covered_display": "98", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 97.5609756097561, "percent_statements_covered_display": "98"}, "missing_lines": [77], "excluded_lines": [], "functions": {"FleetScopeResolver.__init__": {"executed_lines": [26], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 21}, "FleetScopeResolver.resolve": {"executed_lines": [32, 33, 34], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 28}, "Metrics.__init__": {"executed_lines": [42, 43, 44], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 41}, "Metrics.inc": {"executed_lines": [47, 48, 49], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 46}, "Metrics.set_gauge": {"executed_lines": [52, 53, 54], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 51}, "Metrics.snapshot": {"executed_lines": [57, 58], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 56}, "Health.__init__": {"executed_lines": [71, 72], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 70}, "Health.record_latency": {"executed_lines": [75, 76], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67"}, "missing_lines": [77], "excluded_lines": [], "start_line": 74}, "Health.state": {"executed_lines": [80, 81, 82, 83, 84, 85, 87], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 79}, "": {"executed_lines": [13, 15, 18, 21, 28, 37, 41, 46, 51, 56, 61, 70, 74, 79], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"FleetScopeResolver": {"executed_lines": [26, 32, 33, 34], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 18}, "Metrics": {"executed_lines": [42, 43, 44, 47, 48, 49, 52, 53, 54, 57, 58], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 37}, "Health": {"executed_lines": [71, 72, 75, 76, 80, 81, 82, 83, 84, 85, 87], "summary": {"covered_lines": 11, "num_statements": 12, "percent_covered": 91.66666666666667, "percent_covered_display": "92", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 91.66666666666667, "percent_statements_covered_display": "92"}, "missing_lines": [77], "excluded_lines": [], "start_line": 61}, "": {"executed_lines": [13, 15, 18, 21, 28, 37, 41, 46, 51, 56, 61, 70, 74, 79], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "corvus/state.py": {"executed_lines": [8, 10, 11, 12, 14, 59, 62, 63, 64, 65, 66, 67, 68, 70, 71, 72, 73, 74, 75], "summary": {"covered_lines": 19, "num_statements": 19, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"State.__init__": {"executed_lines": [63, 64, 65, 66, 67, 68], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 62}, "State.execute": {"executed_lines": [71, 72, 73, 74, 75], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 70}, "": {"executed_lines": [8, 10, 11, 12, 14, 59, 62, 70], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"State": {"executed_lines": [63, 64, 65, 66, 67, 68, 71, 72, 73, 74, 75], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 59}, "": {"executed_lines": [8, 10, 11, 12, 14, 59, 62, 70], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "corvus/tokens.py": {"executed_lines": [31, 33, 34, 35, 38, 39, 44, 45, 48, 49, 53, 54, 55, 56, 57, 58, 60, 67, 68, 72, 80, 81, 83, 87, 91, 93, 94, 96, 100, 103, 108, 109, 114, 117, 140, 145], "summary": {"covered_lines": 36, "num_statements": 58, "percent_covered": 62.06896551724138, "percent_covered_display": "62", "missing_lines": 22, "excluded_lines": 0, "percent_statements_covered": 62.06896551724138, "percent_statements_covered_display": "62"}, "missing_lines": [40, 41, 69, 92, 95, 97, 98, 99, 101, 102, 104, 127, 128, 129, 130, 131, 132, 133, 134, 138, 141, 148], "excluded_lines": [], "functions": {"TokenRefused.__init__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [40, 41], "excluded_lines": [], "start_line": 39}, "VerdictTokenIssuer.__init__": {"executed_lines": [53, 54, 55, 56, 57, 58], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 49}, "VerdictTokenIssuer.issue": {"executed_lines": [67, 68, 72, 80, 81], "summary": {"covered_lines": 5, "num_statements": 6, "percent_covered": 83.33333333333333, "percent_covered_display": "83", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 83.33333333333333, "percent_statements_covered_display": "83"}, "missing_lines": [69], "excluded_lines": [], "start_line": 60}, "VerdictTokenIssuer.consume": {"executed_lines": [87, 91, 93, 94, 96, 100, 103, 108, 109, 114], "summary": {"covered_lines": 10, "num_statements": 18, "percent_covered": 55.55555555555556, "percent_covered_display": "56", "missing_lines": 8, "excluded_lines": 0, "percent_statements_covered": 55.55555555555556, "percent_statements_covered_display": "56"}, "missing_lines": [92, 95, 97, 98, 99, 101, 102, 104], "excluded_lines": [], "start_line": 83}, "VerdictTokenIssuer.revoke": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 9, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 9, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [127, 128, 129, 130, 131, 132, 133, 134, 138], "excluded_lines": [], "start_line": 117}, "VerdictTokenIssuer.outstanding_for": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [141], "excluded_lines": [], "start_line": 140}, "action_fingerprint": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [148], "excluded_lines": [], "start_line": 145}, "": {"executed_lines": [31, 33, 34, 35, 38, 39, 44, 45, 48, 49, 60, 83, 117, 140, 145], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"TokenRefused": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [40, 41], "excluded_lines": [], "start_line": 38}, "EvidenceNotDurable": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 44}, "VerdictTokenIssuer": {"executed_lines": [53, 54, 55, 56, 57, 58, 67, 68, 72, 80, 81, 87, 91, 93, 94, 96, 100, 103, 108, 109, 114], "summary": {"covered_lines": 21, "num_statements": 40, "percent_covered": 52.5, "percent_covered_display": "52", "missing_lines": 19, "excluded_lines": 0, "percent_statements_covered": 52.5, "percent_statements_covered_display": "52"}, "missing_lines": [69, 92, 95, 97, 98, 99, 101, 102, 104, 127, 128, 129, 130, 131, 132, 133, 134, 138, 141], "excluded_lines": [], "start_line": 48}, "": {"executed_lines": [31, 33, 34, 35, 38, 39, 44, 45, 48, 49, 60, 83, 117, 140, 145], "summary": {"covered_lines": 15, "num_statements": 16, "percent_covered": 93.75, "percent_covered_display": "94", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 93.75, "percent_statements_covered_display": "94"}, "missing_lines": [148], "excluded_lines": [], "start_line": 1}}}, "corvus/trust.py": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 61, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 61, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [36, 38, 41, 44, 52, 53, 56, 57, 58, 59, 60, 62, 66, 67, 68, 70, 77, 78, 79, 80, 83, 84, 85, 87, 94, 95, 96, 97, 98, 102, 108, 109, 110, 111, 112, 114, 115, 117, 118, 120, 122, 125, 126, 127, 128, 129, 130, 133, 134, 135, 138, 140, 141, 147, 154, 155, 161, 164, 166, 171, 172], "excluded_lines": [], "functions": {"TrustLedger.__init__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [56, 57, 58, 59, 60], "excluded_lines": [], "start_line": 53}, "TrustLedger.tier": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [66, 67, 68], "excluded_lines": [], "start_line": 62}, "TrustLedger.record_outcome": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 7, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 7, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [77, 78, 79, 80, 83, 84, 85], "excluded_lines": [], "start_line": 70}, "TrustLedger.due_missing": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [94, 95, 96, 97, 98], "excluded_lines": [], "start_line": 87}, "TrustLedger.evaluate_promotion": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 20, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 20, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [108, 109, 110, 111, 112, 114, 115, 117, 118, 120, 122, 125, 126, 127, 128, 129, 130, 133, 134, 135], "excluded_lines": [], "start_line": 102}, "TrustLedger.promote": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [140, 141], "excluded_lines": [], "start_line": 138}, "TrustLedger.demote": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [154, 155], "excluded_lines": [], "start_line": 147}, "TrustLedger.invalidate_on_drift": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [164], "excluded_lines": [], "start_line": 161}, "TrustLedger.quarantine": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [171, 172], "excluded_lines": [], "start_line": 166}, "": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 14, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 14, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [36, 38, 41, 44, 52, 53, 62, 70, 87, 102, 138, 147, 161, 166], "excluded_lines": [], "start_line": 1}}, "classes": {"TrustLedger": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 47, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 47, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [56, 57, 58, 59, 60, 66, 67, 68, 77, 78, 79, 80, 83, 84, 85, 94, 95, 96, 97, 98, 108, 109, 110, 111, 112, 114, 115, 117, 118, 120, 122, 125, 126, 127, 128, 129, 130, 133, 134, 135, 140, 141, 154, 155, 164, 171, 172], "excluded_lines": [], "start_line": 52}, "": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 14, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 14, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [36, 38, 41, 44, 52, 53, 62, 70, 87, 102, 138, 147, 161, 166], "excluded_lines": [], "start_line": 1}}}}, "totals": {"covered_lines": 932, "num_statements": 1277, "percent_covered": 72.98355520751763, "percent_covered_display": "73", "missing_lines": 345, "excluded_lines": 0, "percent_statements_covered": 72.98355520751763, "percent_statements_covered_display": "73"}} \ No newline at end of file diff --git a/corvus-v2/junit.xml b/corvus-v2/junit.xml index a9ac60e..c12f6d3 100644 --- a/corvus-v2/junit.xml +++ b/corvus-v2/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/corvus-v2/requirements.txt b/corvus-v2/requirements.txt index d86765f..75b52d0 100644 --- a/corvus-v2/requirements.txt +++ b/corvus-v2/requirements.txt @@ -1,4 +1,7 @@ -fastapi>=0.115 -uvicorn>=0.30 -pyyaml>=6.0 -pydantic>=2.0 +# Floors are the versions the harness actually tested (python3.12), capped at the +# next MAJOR: a future breaking major must fail loudly at install time rather than +# be silently absorbed by a loose floor. Re-run `make cycle` before raising. +fastapi>=0.162,<1.0 +uvicorn>=0.54,<1.0 +pyyaml>=6.0.3,<7.0 +pydantic>=2.14,<3.0 diff --git a/corvus-v2/scripts/check_metrics.py b/corvus-v2/scripts/check_metrics.py index 9f6d9e1..4653466 100644 --- a/corvus-v2/scripts/check_metrics.py +++ b/corvus-v2/scripts/check_metrics.py @@ -1,47 +1,76 @@ #!/usr/bin/env python3 -"""The metrics drift guard: the committed dev-metrics.json must match a fresh -run. A stale file fails the gate — the mutation-provable control that keeps -the metrics honest (the mutation: stale the file → the gate must red).""" +"""The metrics drift guard: the committed dev-metrics.json must match a fresh run. + +A stale file fails the gate — the mutation-provable control that keeps the +metrics honest (the mutation: stale the file -> the gate must red). + +WHAT IS GATED AND WHY THE TOLERANCES ARE WHAT THEY ARE +The interpreter is PINNED to python3.12 in the Makefile and in CI, so the +interpreter-variance that forced a 7.0-point coverage tolerance is gone. The +measured history, kept because it explains the numbers: + 2026-10-19: the same suite on 3.15 = 79.1% vs 3.12 = 72.9% -> 6.2 points apart. + 2026-10-24: pinned 3.12.15 = 73.0%, 1277 statements, 90 tests. +The 108-tests / 79.1% / 1274-lines file committed on 2026-10-19 was therefore +stale, and the OLD guard passed it: it compared only coverage percent and +lines_total, so a test-count drift of 108-vs-90 was invisible. The test count +is now gated exactly. That is the whole point of a drift guard — it must be +able to fail on the drift that actually happened. +""" import argparse import json import sys from pathlib import Path +# The interpreter is pinned (python3.12) on both sides, so these are tight. +COVERAGE_TOLERANCE = 1.0 # percentage points — rounding only, not interpreter variance +LINES_TOLERANCE = 0 # statements are deterministic for a fixed interpreter + source + def main(): ap = argparse.ArgumentParser() - ap.add_argument("--expected", required=True) - ap.add_argument("--actual", required=True) - ap.add_argument("--tolerance", type=float, default=7.0, - help="the coverage percent tolerance — CALIBRATED TO THE MEASURED " - "interpreter variance (2026-10-19: the same suite on 3.15 = 79.1%% " - "vs 3.12 = 72.9%% → 6.2 points apart; the tolerance 7.0 covers it " - "while the stale file (the 227-era seed) still fails by 50+ points)") + ap.add_argument("--expected", required=True, help="the committed dev-metrics.json") + ap.add_argument("--actual", required=True, help="the metrics from a fresh run") + ap.add_argument("--coverage-tolerance", type=float, default=COVERAGE_TOLERANCE) + ap.add_argument("--lines-tolerance", type=int, default=LINES_TOLERANCE) args = ap.parse_args() expected = json.loads(Path(args.expected).read_text()) actual = json.loads(Path(args.actual).read_text()) - # The source_commit is INFORMATIONAL (every commit changes the HEAD sha, - # so gating on it is structurally unsatisfiable — caught live by this - # guard's own first CI run). The MEASURABLE facts gate. + # source_commit is INFORMATIONAL: every commit changes the HEAD sha, so gating + # on it is structurally unsatisfiable (caught live by this guard's first CI run). + # The MEASURABLE facts gate. drifts = [] + ec = expected.get("coverage", {}).get("percent") ac = actual.get("coverage", {}).get("percent") - if ec is None or ac is None or abs((ac or 0) - (ec or 0)) > args.tolerance: - drifts.append(f"coverage percent: {ec} != {ac}") + if ec is None or ac is None or abs((ac or 0) - (ec or 0)) > args.coverage_tolerance: + drifts.append(f"coverage percent: committed {ec} != fresh {ac} (tolerance {args.coverage_tolerance})") + et = expected.get("coverage", {}).get("lines_total") at = actual.get("coverage", {}).get("lines_total") - if et is not None and at is not None and abs(at - et) > 6: - drifts.append(f"lines_total: {et} != {at} (beyond the interpreter variance)") + if et is None or at is None or abs(at - et) > args.lines_tolerance: + drifts.append(f"lines_total: committed {et} != fresh {at} (tolerance {args.lines_tolerance})") + + # The test count is exact: it cannot vary with the interpreter, and its drift is + # exactly the failure the old guard could not see. + ett = expected.get("tests", {}).get("total") + att = actual.get("tests", {}).get("total") + if ett is None or att is None or ett != att: + drifts.append(f"tests total: committed {ett} != fresh {att} (exact — no tolerance)") + + af = actual.get("tests", {}).get("failed") + if af is not None and af > 0: + drifts.append(f"the fresh run has {af} failing test(s)") if drifts: print("METRICS DRIFT — the committed dev-metrics.json is stale:") for d in drifts: print(" ", d) - print("Run `make metrics` and commit the refreshed file.") + print("Run 'make test && make metrics' and commit the refreshed file.") sys.exit(1) - print("metrics: fresh (no drift)") + + print(f"metrics: fresh (no drift) — {att} tests, {ac}% coverage, {at} statements") if __name__ == "__main__": diff --git a/corvus-v2/scripts/dev_check.py b/corvus-v2/scripts/dev_check.py new file mode 100644 index 0000000..c9370a0 --- /dev/null +++ b/corvus-v2/scripts/dev_check.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""The corvus-v2 build proof: the engine actually serves. + +This is the harness's "build" step, and it proves three things a unit test +cannot: the app object constructs from a real config, it serves over a real +socket, and the auth boundary both admits a valid key and rejects an invalid +one. The 401 control matters — a check that only ever sees 200 would pass on +an app whose auth gate was deleted. + +Exit 0 on success, non-zero with the reason on failure. +""" +import json +import socket +import sys +import threading +import time +import urllib.error +import urllib.request + +sys.path.insert(0, ".") +from corvus.app import create_app # noqa: E402 +from corvus.config import FleetConfig # noqa: E402 + +API_KEY = "dev-local-key" + + +def free_port() -> int: + with socket.socket() as s: + s.bind(("127.0.0.1", 0)) + return s.getsockname()[1] + + +def main() -> int: + port = free_port() + cfg = FleetConfig(name="dev-fleet", node_id="dev-1", api_key=API_KEY) + app = create_app(config=cfg, db_path=":memory:") + import uvicorn + + t = threading.Thread( + target=lambda: uvicorn.run(app, host="127.0.0.1", port=port, log_level="error"), + daemon=True, + ) + t.start() + time.sleep(1.5) + + def get(path: str, key: str): + req = urllib.request.Request(f"http://127.0.0.1:{port}{path}", headers={"X-Corvus-Key": key}) + return urllib.request.urlopen(req, timeout=5) + + health = json.loads(get("/health", "unused").read()) + assert health["status"] == "ok", f"health not ok: {health}" + + authed = json.loads(get("/ops/events", API_KEY).read()) + assert authed is not None, "the authenticated read returned nothing" + + try: + get("/ops/events", "wrong-key") + except urllib.error.HTTPError as e: + assert e.code == 401, f"expected 401 for a bad key, got {e.code}" + else: + return fail("the auth gate did NOT reject a wrong key") + + print(f"build: ok — {health['fleet']} / health + authenticated read + 401 control") + return 0 + + +def fail(msg: str) -> int: + print(f"build: FAILED — {msg}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/DEV-READINESS-REVIEW.md b/docs/DEV-READINESS-REVIEW.md new file mode 100644 index 0000000..cf574cc --- /dev/null +++ b/docs/DEV-READINESS-REVIEW.md @@ -0,0 +1,226 @@ +# Dev Readiness Review — the plan against what the harness can prove + +**Baseline**: worktree `corvus-dev`, branch `harness/per-component`, HEAD `2191155` (= `origin/main`). +**Interpreter**: `python3 --version` → 3.15.0; `python3.12 --version` → 3.12.15. Every component +harness pins 3.12; CI uses 3.12. +**Method**: every current-state claim names its instrument — a `file:line`, a command with its real +output, or a live probe. Claims I could not verify are listed in §8, not softened into the prose. +Numbers below are from my own runs on this worktree unless a different instrument is named. The +repo's README and CLAUDE.md are not instruments: they describe v1 and are stale (§7.12). + +## 1. Component inventory + +| Component | What it is | Status | Current state — instrument | +|---|---|---|---| +| `corvus-v2` | The decision engine + the CCB spine (the single binary) | **LIVE** — deployed | 24 modules, 1,277 statements; 90 tests pass at 73.0% coverage (`make cycle`). No `pyproject.toml` (`ls corvus-v2/pyproject.toml` → absent; requirements.txt only), so `pip install -e .` is impossible. Three modules at 0% (`coverage.json` per-file list): `app.py` (78 stmts), `conformance.py` (66), `trust.py` (61). | +| `corvus-sdk` | Async Python client | **LIVE PACKAGE, v1 protocol** | 220 statements; 54 tests, 100% coverage, all against mocked HTTP (`respx`; `make cycle`). 30 `/ops` paths, **3** served by v2. | +| `corvus-cli` | typer CLI | **LIVE PACKAGE, v1 protocol** | 214 statements; 50 tests (added on this branch), 100% coverage, `MockTransport` (`make cycle`); 15 `/ops` paths, **3** served by v2; no evidence-export command. | +| `corvus-server` | v1 platform (FastAPI, CMDB, runbooks, Neo4j) | **LEGACY** — archived at `v1-archive`, not deployed | `src/` absent on this branch (`ls corvus-server/src` → No such file); `Dockerfile:35` still `COPY src/ src/`; two modules remain; 15 runbooks; 0 tests. | +| `corvus-hooks` | v1 governance hooks (CC PreToolUse/PostToolUse, 3 adapters, installer) | **LEGACY** — not deployed | stdlib scripts; default URL `http://localhost:9420` and v1 route `/ops/events/targets/{target}/status` (`corvus_core.py:28,80`); 0 tests; v1 copy archived at `~/.claude/hooks/_archive-corvus-decom-2026-05-28`. | +| `corvus-splunk` | Splunk app (4 dashboards, 11 searches, 3 lookups) | **LEGACY config** — not deployed | expects `index=corvus sourcetype="corvus:ocsf"` + `class_uid` (`default/macros.conf:4,13-29`); app version 0.1.0; 0 tests; no Splunk instance locally. | + +The `Status` column carries the distinction that matters: a component can be **live as a package** +(builds, lints, tests, CI-gated) while speaking a protocol nothing serves. `corvus-cli` and +`corvus-sdk` are exactly that (§3.1). + +## 2. What "live" actually is: the deployed surface + +- **One workload, one container.** `kubectl -n corvus get deploy,svc,pods -o wide` → `corvus-engine` + 1/1 ready, image `harbor.themillertribe-int.org/homelab/corvus-v2:2191155`, Service + `10.43.206.222:8933`, pod on `tmtdockp01` (1 restart 2d14h ago). `kubectl get deploy,sts,ds -A | + grep -i corvus` returns that single line — no v1 server, no second deployment. +- **The deployed artifact is the decision engine, not the CCB.** The container sets no + `command`/`args` (`kubectl … -o jsonpath` → `||8933`), so the image CMD runs: + `python -m corvus.engine_shim --port 8933` (`corvus-v2/Dockerfile:21`). The shim declares exactly + one route, `POST /v1/fleets/{fleet_id}/decide` (`corvus/engine_shim.py:111`); there is no GET + status route, and liveness/readiness are TCP probes on 8933. +- **The CCB spine is not deployed.** `corvus/app.py` defines the event ingest, change windows and + the conflict check (`app.py:70-119`, six paths) but no workload in this cluster serves them and + nothing exposes a port other than 8933. +- **The register is byte-identical to HEAD.** `shasum -a 256 decision-register.yaml` → + `7ede7637…6e368`; `kubectl -n corvus get cm corvus-register -o jsonpath='{.data.decision-register\.yaml}' + | shasum -a 256` → the same hash. The register is 19 sites + 10 facts + 3 composition policies + (`python3.12` + yaml: `len(sites)=19`, `len(fact_layer.facts)=10`), while its own header comment + (`decision-register.yaml:5`) says "18 sites + 9 fact citizens". +- **The live capture store is not durable.** The deployment sets `CORVUS_CAPTURE_DB=/data/capture.db` + on an `emptyDir` volume (`kubectl -n corvus get deploy corvus-engine -o yaml`), so the decision + records and the replay index die with the pod. The local default (`:memory:`, + `engine_shim.py:92`) is no better; only an explicitly configured file path persists. +- **Public reachability is real but the path is not visible from the cluster.** + `curl -sS -o /dev/null -w '%{http_code}' https://corvus.themillertribe-int.org/` → `404` + (the app answers); `dig +short corvus.themillertribe-int.org` → `192.168.20.230`; + `kubectl get ingress -A` → 0 corvus matches. The hop from that address to the ClusterIP is + outside the cluster's Ingress surface and I did not verify it. + +## 3. The two seams that are broken + +### 3.1 The clients speak v1; the engine serves v2 + +Exact string comparison of the clients' called paths against `app.py`'s route decorators +(`python3.12`, regex over the three files): + +``` +v2 routes: ['/health', '/ops/changes', '/ops/changes/active', '/ops/conflict-check', + '/ops/events', '/ops/events/stream'] +sdk: 30 distinct paths; exact v2 matches: 3 -> ['/ops/changes', '/ops/changes/active', '/ops/events'] +cli: 15 distinct paths; exact v2 matches: 3 -> ['/ops/changes', '/ops/changes/active', '/ops/events'] +``` + +So **27 of 30 SDK paths and 12 of 15 CLI paths are not served by v2** — `/ops/cmdb`, +`/ops/incidents`, `/ops/runbooks/*`, `/ops/graph/*`, `/ops/trust`, `/ops/gaps`, `/ops/metrics`, +`/ops/knowledge/*`, `/ops/discovery/*`, `/ops/problems`, `/ops/events/context`, +`/ops/events/targets/{t}/status`, `/ops/changes/{id}`. The same v1 routes are hard-coded in the +hooks (`corvus_core.py:80,107`) against `http://localhost:9420` (`:28`) — the v1 server's port. + +Their 100% coverage is real but scoped: the SDK tests mock with `respx`, and the CLI fixture states +"No test ever opens a socket" (`corvus-cli/tests/conftest.py:3`). **No test in the repo starts a v2 +server and runs a client against it.** Green here proves the client's own logic, not parity. + +### 3.2 The v2 binary exercises 4 of its 24 modules + +Static import reachability from the two entrypoints (`python3.12` + `ast` over `corvus/*.py`): + +``` +engine_shim reachable: ['canonical', 'capture', 'contract_codes', 'engine_shim'] +app reachable: ['app', 'config', 'core', 'hlc', 'state'] +NOT reachable from either entrypoint: __init__, adapter, attested, cc_adapter, compiler, + config_loader, conformance, decision_core, decisions, delivery, facts, observe, ops, tokens, trust +``` + +15 modules exist only for the tests or for the out-of-repo adapter. The load-bearing ones: + +- **The trust ledger is unwired.** `TrustLedger` (`trust.py:52`) has no importer; nothing writes the + `ops_trust` table (`state.py:49`) — `conflict_check` only reads it (`core.py:133`), so every live + answer carries the default `trust_tier: "ESCALATE"`. `test_spine.py:65-68` pins that default; no + test promotes a tier and observes it in a conflict answer. Capability 3 (graded autonomy) is not + on the deployed path. +- **The observe mode, delivery chain, verdict tokens, decision client and conformance runner are + not reachable from the deployed binary** — `observe.py`, `delivery.py`, `tokens.py`, + `decisions.py`, `conformance.py`. They are tested as units (`test_observe_delivery.py`, + `test_decision_contract.py`), never as the deployed surface. +- **`app.py` is 0% under pytest** even though it is the CCB's whole HTTP surface. The harness build + step exercises `/health` and the auth boundary over a socket (`scripts/dev_check.py`), which is + why the build target is meaningful — but no pytest test covers the app. +- **The P1 conformance gate is not gated.** At HEAD, `ci-v2.yaml` ran lint, tests, the metrics + guard and a smoke job — no mutation, no conformance run. The branch's CI matrix runs each + component's `make cycle`, which still does not include the `mutation`/`conformance` targets. + The P1 capability gate ("the conformance suite runs green in CI with the mutation evidence") is + therefore a manual target, not a gate. + +## 4. What the dev harness proves today + +Each component owns `/Makefile`; the root `Makefile` runs every component's `cycle`. + +| Component | Targets | Evidence from my run | What it cannot prove | +|---|---|---|---| +| `corvus-v2` | setup build lint test fix metrics metrics-check run check cycle mutation conformance clean | `make cycle`: "build: ok — dev-fleet / health + authenticated read + 401 control"; "All checks passed!"; "90 passed, 13 warnings in 15.94s"; "TOTAL 1277 345 73%"; "metrics: fresh (no drift) — 90 tests, 73.0% coverage, 1277 statements" | the deployed decision path; `app.py` under pytest; the mutation/conformance gates (manual, not in cycle/CI) | +| `corvus-sdk` | setup build lint test fix cycle clean | `make cycle`: "build: ok — 0.1.0"; "All checks passed!"; "54 passed in 0.33s"; "TOTAL 220 0 100%" | parity with v2 — every request is mocked | +| `corvus-cli` | setup build lint test fix cycle clean | `make cycle`: "import: ok"; "`corvus --help`" and "`corvus check-target --help`" exit 0; "All checks passed!"; "50 passed in 0.72s"; "TOTAL 214 0 100%" | parity with v2; any command that reads decisions/capture/audit evidence | +| `corvus-server` | setup lint import fix cycle clean | `make cycle`: "All checks passed!"; "import: ok on 3.12.15 — soc2 APIRouter(3 routes), typer app 'corvus' (11 commands)" | anything behavioral: no tests, no runnable app | +| `corvus-hooks` | setup lint import fix cycle clean | `make cycle`: "All checks passed!"; "import: byte-compiled 8 scripts"; "import: ok — corvus_core imported, public API present" | that the hooks work against any server (they target v1 routes) | +| `corvus-splunk` | setup validate cycle clean | `make cycle`: "validate: ok — 6 conf, 5 xml, 3 csv, 1 py" | that Splunk accepts the app, that the SPL is valid, that the dashboards render | + +Two contract deviations, stated plainly: `corvus-v2` has no `pyproject.toml`, so its `build` is a +live-socket proof instead of `pip install -e .`; and the three legacy components replace +`build`/`test` with `import`/`validate` because there is no installable package and no suite. + +Consequence for the umbrella, and its fix: the root's `build`/`lint`/`test`/`fix` first looped +over all six components and aborted at the first one lacking the target +(`make -C corvus-hooks test` → "No rule to make target 'test'"; `make -C corvus-splunk build` → +same, and splunk has no `lint` or `fix` either). The root Makefile now declares per-target +component lists — `LIVE` (the three with build/test), `PY` (the five that lint and autofix), and +`ALL` (setup/cycle/clean) — so every root target resolves. Verified on this worktree: `make lint`, +`make build`, `make test` and `make fix` all exit 0, and `make fix` on the clean tree leaves +`git status` byte-identical (it is idempotent). `make check` is an alias for `make cycle`. + +## 5. Plan mapping — the phase that needs each component next + +| Component | Phase | What the plan needs | What the harness proves | The gap | +|---|---|---|---|---| +| `corvus-v2` | **P1** (then P2/P3) | C1 answers live traffic in observe mode; the capture store records full-schema decisions; the conformance suite runs green in CI with mutation evidence | the engine contract (15 wire tests), 90 unit tests, a live health/auth proof | trust ledger unwired; observe/delivery/tokens not on the deployed path; capture ephemeral live; conformance/mutation not in CI; `app.py` 0% under pytest | +| `corvus-sdk` | **P3** | "the SDK reduced to the documented HTTP contract (any language)" | 54 mocked tests at 100% | v1 protocol: 27/30 paths unserved; no real-server test | +| `corvus-cli` | **P5** | "the audit CLI's evidence export" | 50 mocked tests at 100%, entrypoint dispatch | v1 protocol; no decision/audit/evidence commands exist | +| `corvus-server` | **P2** (+P4) | the runbook schema + validation gate; the knowledge base; blind-spot detection "ported from the old implementation where the concept survives review" | lint + import of the two remaining modules | `src/` is in `v1-archive`; 15 runbooks with no schema gate; 78 archived test files / 746 test functions that cannot run on this branch | +| `corvus-hooks` | **P1** | ADR-004's adapter contract, its conformance suite, and the first two real adapters | lint + byte-compile + `corvus_core` import | v1 routes; no adapter-conformance fixture; the one real v2 adapter is outside the repo (§7.4) | +| `corvus-splunk` | **P3** | "The Splunk app validated against the rebuilt OCSF events" | config/XML/CSV parse + cross-reference validation | v2 emits no OCSF and forwards nothing to a SIEM (§7.6) | + +**THE acceptance gate is untestable today.** "A new harness reaches full governance parity by +shipping only an adapter" needs a fixture that runs an adapter against the engine and checks the +four capabilities. The repo has the engine's wire conformance (`conformance.py:111`) and unit tests +for the CC adapter with a stub client (`test_cc_adapter.py`), but no adapter-conformance harness. + +## 6. The metrics guard: the drift it could not see + +``` +committed at HEAD (git show HEAD:docs/canonical-path/dev-metrics.json): 108 tests, 79.1%, 1274 lines, source dabbda4 +my fresh run (make cycle): 90 tests, 73.0%, 1277 lines, source 2191155 +``` + +At HEAD the guard compared only coverage percent (tolerance 7.0) and `lines_total` (tolerance 6) +(`git diff corvus-v2/scripts/check_metrics.py` shows the removed code); 108-vs-90 tests was +invisible to it. On this branch the guard gates the test count exactly and coverage to 1.0 point +with 0-statement tolerance — my `make cycle` above passes the tightened gate. + +## 7. Blind spots — what a developer cannot verify locally + +1. **The deployed decision path has no test.** `app.py` is 0% under pytest; the deployed shim's only + route is `POST /decide`, so even a live smoke check writes to the capture store. +2. **The live capture store is ephemeral** (`emptyDir`): the audit trail does not survive a pod + restart, and no local run can prove otherwise for the deployment. +3. **Client↔engine parity is unproven**: no test starts a v2 server and drives a client at it. +4. **The only real adapter is out of repo.** `~/.claude/hooks/corvus-v2-observe.py` (wired at + `~/.claude/settings.json:119`) imports `corvus` from a different worktree + (`/Users/tmiller/worktrees/corvus-canonical/corvus-v2`) and defaults to the deployed URL. Its + chain DB `~/.corvus-v2/chain.db` has mtime 2026-10-19T15:17 (12,288 bytes) — the local evidence + store has not recorded since. Nothing in the repo exercises it. +5. **The acceptance gate has no fixture** (§5), so "parity by adapter alone" is a claim, not a check. +6. **OCSF/SIEM is absent from v2.** `grep -rniE 'splunk|\bsiem\b|ocsf' corvus-v2/corvus/ --include=*.py` + → one docstring line (`core.py:5`). PRODUCT capability 2's "OCSF 1.3.0" stream and the Splunk + app's consumer side cannot be validated locally. +7. **No lockfiles anywhere** (`find . -maxdepth 3 \( -name 'poetry.lock' -o -name 'uv.lock' -o -name + 'requirements*.lock' -o -name 'Pipfile.lock' -o -name 'pdm.lock' \) -not -path '*/.venv/*'` → none). + Dependency floors are `>=`; a fresh resolve today equals the current latest (Lead's PyPI query, + 2026-10-24T03:30Z, corroborated by `.venv` `pip list`: fastapi 0.162.0, uvicorn 0.54.0, + pydantic 2.14.1, pyyaml 6.0.3), but nothing pins that resolve — the only pin is the toolchain + (`ruff==0.17.1`, deliberate; latest is 0.17.2). Reproducibility rests on CI going red after a + break, not on a pinned set. The legacy server's floors are majors behind + (`corvus-server/requirements.txt`: `neo4j>=5.20.0`, `mcp>=1.0.0`; latest 6.4.1 and 2.3.0 per the + Lead's query), and it caps `pytest-asyncio>=0.25.0,<1.0.0`. +8. **The mutation runner rewrites the working tree.** `run_mutation_suite` writes the mutant and + restores it (`conformance.py:87,98`); a kill between the two leaves a mutated repo. It is not in + CI and is unsafe to run concurrently with anyone editing. +9. **The contract tests share a fixed port.** Each `shim` fixture starts uvicorn on 8934 and never + stops it; my run emits repeated `PytestUnhandledThreadExceptionWarning` (Thread-2…6, uvicorn + startup `SystemExit: 3`) and still passes because an earlier server is listening. Parallel runs + or a busy port collide. The `conformance` make target had the same class of defect and it bit + CI: it bound a fixed 8933 and cleaned up with `pkill -f "engine_shim --port 8933"`, which also + matched the recipe's own shell, so the run passed 2/0 while make reported "Terminated" and + exited 2 (CI run 40058979560). It now binds an ephemeral port and kills the shim by PID. +10. **This host runs a launchd-managed engine on the conformance port.** `launchctl list` shows + `com.themillertribe.corvus-v2-engine` (PID 66272, `-m corvus.engine_shim --port 8933 --host + 127.0.0.1`, cwd `/Users/tmiller/worktrees/corvus-canonical/corvus-v2`, python 3.15) holding + 127.0.0.1:8933. It restarts when killed (parent PID 1). A local `make conformance` against a + fixed 8933 was therefore testing THAT process, not this worktree's code — a green local result + that proved nothing. The ephemeral port fixes the harness; the agent itself is outside this + repo and was not touched. +10. **The deployed register load is inferred, not observed.** The mount is proven (hash match), and + `engine_shim.py:66-80` is the path ladder, but I could not read the running process's loaded + register: `kubectl exec` is blocked by the session's guardrails (a DDR is required for that + target), and I did not author one. +11. **The public route is unverified** (§2): DNS resolves to a LAN address; there is no Ingress. +12. **The repo's own docs are stale instruments.** `CLAUDE.md` documents `corvus-server/src/app.py`, + `routers/`, `middleware/` (absent: `ls corvus-server/src` fails), a 12-runbook count (15 on + disk: `ls corvus-server/runbooks/*.yaml | wc -l`), and a `docker run … ghcr.io/…/corvus:latest` + deployment (the live image is `harbor…/corvus-v2:2191155` on 8933). README describes the v1 box. + +## 8. Claims I could not verify + +- That the deployed engine actually **loaded** the mounted register (mount hash proven; load not). +- The live capture DB's contents, row count, or persistence across a restart (`kubectl exec` blocked). +- SDK/CLI behavior against a **real** v2 server (not attempted; static route comparison is the + instrument used instead). +- The v1 hooks' runtime behavior (no tests, not deployed, and their target routes do not exist in v2). +- The Splunk app in a real Splunk (no instance available): acceptance, SPL validity, rendering. +- The deployed engine's latency, availability, or behavior under load (no load instrument here). +- Whether the out-of-repo CC adapter currently works end-to-end (its chain DB is stale; running it + would POST a decision to the live engine, which I did not do). diff --git a/docs/canonical-path/dev-metrics.json b/docs/canonical-path/dev-metrics.json index 947d0cb..9797082 100644 --- a/docs/canonical-path/dev-metrics.json +++ b/docs/canonical-path/dev-metrics.json @@ -1,16 +1,16 @@ { "schema": 1, - "source_commit": "dabbda4", - "generated_at": "2026-10-19T21:30:43.414112+00:00", + "source_commit": "2191155", + "generated_at": "2026-10-24T03:24:33.605193+00:00", "tests": { - "total": 108, - "passed": 108, + "total": 90, + "passed": 90, "failed": 0 }, "coverage": { - "percent": 79.1, - "lines_covered": 1008, - "lines_total": 1274 + "percent": 73.0, + "lines_covered": 932, + "lines_total": 1277 }, - "duration_seconds": 0.0 + "duration_seconds": 16.38 }