What evidence exists that a given change was owned, tested and checked, and
what happened to it afterwards? Repowise rebuilds every merged change from
git as a Change Record and attaches a checklist of that evidence, each item
stating how it is known. Where the evidence is missing, the item says
unknown, never ok.
Records are built during repowise init and extended by every
repowise update. No forge API is called, no model runs, and nothing leaves
the machine.
repowise changes list # merged changes, newest first
repowise changes show pr-123 # one record with its checklist
repowise changes show pr-123 --format in-toto # an in-toto Statement v1
repowise changes export --since 2026-07-01 --until 2026-10-01 --format csv
repowise suspects --symptom "TypeError: total is undefined"How changes are reconstructed
The first-parent chain of HEAD is the sequence of things that landed. Each
first-parent commit becomes one change.
| What landed | Shape | Commits in the change | Files |
|---|---|---|---|
| A merge commit | merge | The commits its branch brought in | The merge against its first parent |
| A squash commit naming a PR or MR | squash | The squash commit | That commit's diff |
| Any other commit | commit | The commit itself | That commit's diff |
- A maintainer merge that names no change of its own (a release branch merged back) gives up the PR merges and squash commits inside it as their own changes.
- Rebase merges leave no grouping in git, so each rebased commit is its own change.
- The window is the repository's
commit_limit(5,000 first-parent commits by default). An update reads only what landed after the newest stored change, and rebuilds after a rebase or branch switch.
Each change gets the first id that applies, and keeps it across updates:
| Order | Source | Id |
|---|---|---|
| 1 | A PR or MR number the message names, read per forge (GitHub, GitLab, Bitbucket, Azure DevOps) | pr-123 |
| 2 | A Gerrit Change-Id: trailer | change-I... |
| 3 | The merge commit | merge-<sha> |
| 4 | The commit | commit-<sha> |
changes show accepts an id, the forge ref changes list prints, or any
commit sha the change landed.
The evidence checklist
Every record carries the same eleven items, in this order. Each has a
status, a basis and the source it was read from, and reads as one plain
sentence in the CLI and web UI.
| Item | What it answers |
|---|---|
components_touched | Which components the change touched, with criticality, tags and bus factor |
owners | The declared owner teams of those components |
suggested_reviewers | Who has recently worked on these files (counted, never named in reports) |
reaching_tests | Per changed source file, the tests that reach it, measured from coverage or inferred from the graph |
contracts | Contracts the change touched, and whether any change is breaking |
cross_repo_consumers | Consumers of those contracts in other workspace repositories |
decisions_crossed | Accepted decisions governing the changed files |
health_delta | Health findings the change introduced or resolved |
provenance | Three-state AI attribution per commit, with coverage |
risk_score | Where the diff size ranks among recent commits (size, not a bug predictor) |
outcomes | What later history records about the change (below) |
status | Meaning |
|---|---|
ok | The evidence is present and raises nothing |
attention | The evidence is present and a reviewer should look |
unknown | The evidence is not available; never read as a pass |
basis | Meaning |
|---|---|
measured | Read directly: coverage, git, the index |
inferred | Derived from the graph, such as a test that reaches a file through imports |
declared | Stated by someone: a catalog owner, an accepted decision, a trailer |
unresolved | Missing; the item says why |
The checklist is built on first read and kept on the record; --refresh
rebuilds it. reaching_tests asks for attention only on a measured absence:
when nothing knows whether a test reaches a file, the item is unknown.
Outcomes
outcomes reads later history from explicit links only, and is read again
whenever HEAD moves:
- Reverts that name the change.
- Fix references: a later commit that names the change as the cause of
what it fixes in its own words (a
Fixes: <sha>trailer, "regression in", "caused by", "broken by"), quoted, and only from commits the change is an ancestor of. - File fix rate: how many of the change's files received bug-fix commits
within 30 days (
changes.outcome_fix_days). This says where fixes landed, never that the change caused them.
There is no inferred blame: no commit is named as the cause of a bug unless a later commit says so.
In-toto export
repowise changes show REF --format in-toto wraps the record as the
predicate of an in-toto Statement v1
with predicate type https://repowise.dev/ChangeRecord/v1. Its subjects are
the change's commits, keyed by gitCommit digest. It claims no SLSA level and
is not signed by repowise; sign it with your own tooling if you need an
attestation.
The record itself is one versioned JSON shape (schema_version: 1) on every
surface: id, shape, forge reference, title, merge time, head sha, author,
commits, files with added and deleted lines, category, provenance and
metadata. A record keeps up to 1,000 files; files_changed carries the full
count.
Suspects for a symptom
repowise suspects takes what you saw (issue text, an error message, a stack
trace file or a list of files) and ranks the changes merged in a window
against it. Five signals are combined by reciprocal rank fusion, and each row
shows which fired and why:
| Signal | Evidence |
|---|---|
| Text match | Words from the symptom in the change's title, message and files |
| Graph distance | Import distance from the files the symptom names |
| Co-change | Files that historically change with those files |
| Recency | How recently the change merged |
| Stack trace | Frames that land in the change's files |
repowise suspects --symptom "$(cat issue.txt)" --since 2026-09-01
repowise suspects --trace crash.log --files src/billing/invoice.pyMeasured on 43 real regressions in django and node, the change a later revert or fix named was in the top 5 for 67% of issues, against 28% for ranking by shared files alone. Text match carries most of that ranking.
The order is where to start reading, not a cause. To confirm a suspect, let git find the first bad commit with a check that fails on the bug:
git bisect start <bad-sha> <good-sha>
git bisect run python -m pytest tests/test_invoices.py -x
git bisect resetThe default window is the 14 days before --until. Most regressions surface
later than that, so a change missing from the list is not cleared: widen
--since.
Where it shows
| Surface | Use |
|---|---|
| CLI | repowise changes list, changes show, changes export, repowise suspects |
| REST | GET /api/repos/{id}/changes, GET /api/repos/{id}/changes/summary, GET /api/repos/{id}/changes/{change_id}, GET /api/repos/{id}/wins, POST /api/repos/{id}/suspects |
| MCP | get_context(targets=["change:<id>"]) or a commit sha, get_change_risk(include=["evidence"]), get_context(include=["suspects"]), get_risk(include=["changes"]), get_overview(include=["changes"]) |
| Web | Commits page, History switch: By commit, By change (category, provenance, an Evidence column and a Needs attention filter) and Events; a page per change with its checklist and outcomes |
Every change row leads with health: its commits' stored health deltas summed
(introduced, worsened, resolved, files worsened) and a kind of improved,
worsened, mixed or neutral, with null when no commit was scanned.
/changes/summary counts a period's changes by kind, improvements first, and
/wins lists resolved findings, files whose score rose and refactorings that
landed.
changes list --needs-attention reads checklists already built and builds
none, so a change nobody has opened or exported is not listed.
Accuracy
A change counts as correct when its file set matches the forge's, on the last ~100 merged pull requests of each repository:
| Repository | Forge | Correct |
|---|---|---|
| repowise | GitHub | 100% |
| express | GitHub | 98.7% |
| gitleaks | GitHub | 97.9% |
| flask | GitHub | 99.0% |
| fd | GitHub | 98.9% |
| gitlab-dangerfiles | GitLab | 100% |
Limits
- Review data. Git does not record reviewers, approvals or labels. The
default metadata source reads
Approved-by:trailers (written by GitLab on merge commits) and closing keywords; everything else isnull, meaning unknown. A plugin can supply forge metadata through thechange-metadataseam. - Partial clones (
--filter) get no Change Records, because reading file stats would fetch blobs. - The window. Changes before the
commit_limitwindow are not read. An existing index keeps thecommit_limititsconfig.yamlrecords; setcommit_limit: 5000there to read the default window. - Building checklists costs a few seconds per change the first time;
changes exportandreport changesprint how many they will build first.