# Test Intelligence Ingest a coverage report and repowise can answer two questions your CI cannot: which files are risky *and* untested, and which tests a given diff actually exercises. That second one turns a 4,000-test suite into the 40 tests that guard the change you just made. Without a coverage report there is still an answer, a weaker one. The dependency graph records which test files import which source files, so repowise can say *something reaches this* even where nothing measured it. That inferred tier needs no setup and is always labelled as inferred. See [The inferred tier](#the-inferred-tier-no-coverage-report-needed). Everything here is an index lookup: no LLM, no network. ## Quick start ```bash # 1. Produce a report. Any of these work. pytest --cov --cov-report=lcov:coverage.lcov coverage run --contexts=test -m pytest # also builds the per-test map # 2. Ingest it. repowise coverage add coverage.lcov repowise coverage add .coverage # per-file coverage + per-test map repowise coverage status # 3. Use it. repowise health # untested hotspots now light up repowise impacted-tests main..HEAD # the tests guarding this branch repowise impacted-tests main..HEAD --format list | xargs pytest ``` Longer walkthrough: [examples/health-coverage/](../../examples/health-coverage/). ``` repowise coverage status Coverage (lcov) Files: 412 Lines: 71.4% Branch: 63.9% Test-to-code map (coverage.py) Tests: 1,204 Files: 388 Records: 19,551 ``` ## Two dimensions, one command `repowise coverage add` stores two different things, and the difference matters for everything below. | Dimension | What a row says | Powers | |-----------|-----------------|--------| | **Per-file aggregate** | This file is 71% covered, merged across every test. | `untested_hotspot`, `coverage_gap`, `coverage_gradient` in [code health](CODE_HEALTH.md), the coverage dashboard | | **Per-test map** | Test `tests/test_auth.py::test_login` covered lines 40-58 of `src/auth/service.py`. | `repowise impacted-tests`, `get_change_risk`'s `impacted_tests`, `get_risk`'s `tests_to_run` | | **Inferred map** (no ingest) | `tests/test_round_trips.py` imports `src/auth/service.py`, so it reaches it. | The fallback under every row above, always labelled `inferred` | The aggregate always gets stored. The map is only built when the report carries per-test contexts. A report without contexts still ingests fine, it just skips the map. The inferred map is not stored at all: it is read off the graph when asked. Both are point-in-time: each ingest replaces the previous rows rather than appending history. Ingest at the same commit you intend to query, so line numbers line up. ## Supported formats | Format | Detected by | Per-test map | |--------|-------------|--------------| | **LCOV** | Leading `TN:` / `SF:`, or any `TN\|SF\|DA\|BRDA\|LF\|LH\|BRF\|BRH:` line | Yes, when each record carries a non-blank `TN:` test name | | **Cobertura** XML | `Reachability walks EXECUTION_EDGE_TYPES, which is the reachability view minus references and reads: those record a mention rather than a transfer of control. Dead code asks "is this used", which a mention answers; this asks "is this run", which it does not. Both depths are keyword arguments on the walks. ### Nothing is stored The inferred map is read off `graph_edges` when asked and never written to `test_coverage`. Two reasons, and the first is the one that matters: 1. **A consumer cannot mistake it for measured data.** Sharing the table behind a marker column would make every existing reader work immediately and would make every existing reader silently start returning inferred rows the day someone forgot to check the marker. That exact ambiguity, "no data" read as "not loaded", is the shape of [#1739](https://github.com/repowise-dev/repowise/issues/1739). 2. **It would be a transitive closure.** On this repository tests reach 1,630 of 2,509 production files; materialising that is O(tests x sources) rows that go stale the moment the graph moves, to answer a query that is a bounded breadth-first search over rows already in the database. The cost of deriving it is what makes that affordable. Indexing gains nothing: the health pass computes the whole-repo answer in one multi-source walk, measured at **27 ms** on this 3,700-file repository, once per run and cached. The per-change walk is one `IN` query at depth 1, over the same edge table `pr_blast` already reads. ## Empty means unknown, not "no tests" This is the contract that makes the whole layer safe to act on, and it is worth stating plainly: **an empty test list never means the change is untested.** Both tools carry an explicit discriminator alongside the list: - `get_change_risk` sets `status` to `"map_present"` only when a map exists. With no map ingested it returns `status: "inferred"` when the graph can name candidate test files, and `status: "no_map"`, `map_present: false` and a summary that says "run the full suite" when it cannot. Other degraded statuses are `no_index` (nothing indexed yet), `unknown` (the git read failed), and `no_source_line_changes`. `basis` carries the same distinction in one word. - `get_risk` and the REST blast-radius route consume one canonical `test_impact` population. The directive lifts capped typed recommendations and keeps `tests_to_run` as a non-contradictory compatibility projection. Coverage availability, freshness, and measured-map presence remain explicit at `pr_blast_radius.test_impact.coverage`; the legacy `guarding_tests` block is a projection of the same rows rather than a separate derivation. Only `status: "map_present"` with an empty `tests` list means "the map exists and nothing in it covers this change". That is a real finding. `status: "inferred"` is not: it is a candidate list from the import graph, and passing everything in it does not clear a change. Everything else is an absence of evidence, and repowise says so rather than implying a clean bill of health. The same rule runs through the CLI ("unknown, run the full suite"), the `no_coverage_data` bucket, and the coverage lookup helpers, which document absence as unknown at every layer. ## Configuration The `coverage:` block in `.repowise/config.yaml`: ```yaml coverage: auto_discover: true artifacts: # override the discovery globs - "coverage/lcov.info" format: lcov # skip format sniffing strip_prefix: "/build/src/" # trim an absolute prefix from report paths reingest_on_update: false ``` Coverage is also auto-discovered and ingested during `init` and `update`, and `repowise init --coverage-report ` takes explicit reports (repeatable). Note that `--coverage-report` is test coverage, while `--coverage` controls *documentation* breadth. Two different things, similarly named. ## CLI reference | Command | What it does | |---------|--------------| | `repowise coverage add [PATHS...]` | Ingest reports. Auto-discovers when no path is given, merges multiple, builds the per-test map when contexts are present. Flags: `--path`, `--format`, `--verbose` | | `repowise coverage status` | Coverage summary plus test-to-code map counts. Flag: `--path` | | `repowise impacted-tests [REVSPEC]` | The tests a change exercises. Flags: `--path`, `--staged`, `--format` | Full reference: [CLI_REFERENCE.md](../reference/CLI_REFERENCE.md#repowise-coverage). ## See also - [CODE_HEALTH.md](CODE_HEALTH.md): the coverage markers and how they deduct from the score. - [CHANGE_RISK.md](CHANGE_RISK.md): the authoritative review percentile and supporting diff-shape score that `impacted_tests` rides alongside. - [MCP_TOOLS.md](../agent/MCP_TOOLS.md#get_change_risk): full parameter and response reference. - [CONFIG.md](../reference/CONFIG.md): the `coverage:` block.