Where a change goes
ritsu impact --base <revision> compares two revisions of a project and says, in one report, whether
any answer moved and who the change concerns. With --route <file.rule>, a rule of your own says where
the change goes: straight to merge, to the owner to read, or nowhere until it is fixed. Who wrote the
change makes no difference to it.
ritsu impact --base <revision> [--head <revision>] [--route <file.rule>]
[--format text|markdown|json] [--budget <n>] [--root <dir>] [--lang ja|en]
The head is the working tree unless --head names a revision. The root is found as ritsu check finds
it, and the project needs no ritsu.mod. ritsu impact runs no claim and writes nothing: one side of
the change need not be on the disk, and a report should not start a service or change a journal.
What it says
Every part of the report is what a language already answers. ritsu impact gathers them; it does not
say anything of its own about what a file means.
- The files: each changed file, added, removed, renamed or changed, as a file of a language (a
spec), a generated file (its first lines say a generator wrote it), code written by hand (by its
extension), prose (
.md,.markdown,.rst,.adoc, outside a hidden directory, with no name in capitals but README, CHANGELOG, CHANGES and CONTRIBUTING) or something else. The lines of code written by hand are counted. - The check:
ritsu checkof both sides, with.geasfiles read and their claims not run, and the errors and warnings only the head has. A diagnostic whose line moved is not new. Acontracta.geasfile names is read too, so a call its document does not have counts as a new error. - What moved: what
ritsu mod diffsays of the two sides, so every input whose answer differs in a rule, every date that differs in a dates file, and every combination a gate decides differently. That includes what a file reads: a gate reading a calendar moves when the calendar does, and a gate with a date condition shows nothing when an unrelated file changes. - The owners:
yuen affectedof the change: the requirements it reaches, their owners, and the changes no requirement reaches. A project with no.reqis told as one. - The claims:
geas affectedof the change: the claims it touches, and the changed lines no claim runs. Without a record that fits the code (.geas/<name>.map.jsonl), the report says it does not know, and every changed line of code counts as one no claim runs. - The generated files:
ritsu gen --checkwhenritsu.modhas atargetline and the head is the working tree. Otherwise the report says they were not checked. - What it cannot compare:
.flow,.kn,.ctx,.geasand.protofiles have no difference of behavior to show, so they are counted, not read. Anything else that could not be compared or decided is listed with why.
The first sentence of the report is the conclusion. In the example in
crates/ritsu/examples/impact,
moving a band of refund_band.rule from 20 items to 30 (and recording the review with yuen review)
gives:
ritsu impact: base → the working tree
The answers moved (rule_changes 1, allow_gained 1, policies_moved 2); route: route = owner_reads, person_reads = true; owners: finance.
files: 6 changed (2 spec, 0 generated, 0 code, 0 doc, 4 other); 0 changed lines of code written by hand
changed: refund_band.rule (spec, rulec)
changed: refunds.req (spec, yuen)
and, further down, the region of the rule and the combination of the gate that reads it:
allow gained (deny → allow): 1 combination
deciding policies: none → clerks_refund_small_amounts
- User holding clerk, Order, size: small, in_period: yes (in the old version: size: large)
The rule that says where it goes
The inputs of the rule are named after the facts below, by their names or their aliases, and it passes the same checks as any other rule. Its outputs are all given. This is the example's:
define moved : number = rule_changes + date_changes + allow_gained + allow_lost + policies_moved + book_changes + interface_changes + interface_breaks + requirements_moved
define unread : number = undecided + generated_unchecked + opaque_specs + unclaimed_lines + other_files + new_warnings
table route
policy unique
| generated_stale | new_errors | moved | unread | -> route : destination |
| true | - | - | - | stop |
| false | >=1 | - | - | stop |
| false | 0 | >=1 | - | owner_reads |
| false | 0 | 0 | >=1 | owner_reads |
| false | 0 | 0 | 0 | merge |
table answers
policy unique
| moved | -> answers_moved : bool |
| >=1 | true |
| 0 | false |
table who_reads
policy unique
| answers_moved | route | -> person_reads : bool |
| true | owner_reads, stop | true |
| false | owner_reads, stop | true |
| false | merge | false |
The last table holds a promise: it has no row for a change that moved an answer and goes to merge, so
a route table that sends one there stops ritsu check with E101. Because the check covers every input,
that holds for every change, not for the examples alone.
What this rule lets through to merge is what the report can vouch for: prose, a spec whose answers are
shown to be the same, and code that reaches no requirement and whose every added line a claim runs.
Whatever the report knows nothing of goes to a person: a contract a spec reads, a build file, a workflow
(other_files), a generated file nothing checked (generated_unchecked). So does code a claim runs when
a requirement rests on that claim (requirements_moved), because a claim running a line does not say the
claim would notice the line going wrong.
A rule that cannot take the facts stops the run with exit 2, before anything is compared: an input named after no fact (every name is listed), an input of another type than its fact. A value outside the range of an input is said, with its name, its value and the range, and is not rounded: the report still comes out, with no route, and the exit is 2.
The facts
Counts are number inputs, 0 or more, and the rest are bool.
| Fact | What it is |
|---|---|
spec_files, generated_files, code_files, doc_files, other_files |
the changed files of each kind; a rename counts once |
code_lines |
the changed lines of code written by hand, blank lines left out; one line edited is one, and a file whose diff has no line (a rename) is one |
new_errors, new_warnings |
the diagnostics only the head has |
rule_changes |
the regions of input where a rule answers differently, and the rules added or removed |
date_changes |
the dates and calendar days that differ, and the dates files added or removed |
allow_gained, allow_lost |
the combinations a gate now allows or denies that it did not |
policies_moved |
the combinations with the same answer from other policies, what only one version of a gate has, and the gates added or removed |
book_changes |
the changes to what a .book offers, and the books added or removed |
interface_changes, interface_breaks |
the changes to what files offer the code that uses them, and those that can break it. A fed by line added, dropped or moved counts as a change here, because dropping one loosens what the check holds |
undecided |
what could not be compared or decided |
opaque_specs |
the changed .flow, .kn, .ctx, .geas and .proto files, whose contents are not compared |
requirements_moved, requirements_unknown |
the requirements that moved, and the changed specs and code no requirement reaches |
claims_touched, unclaimed_lines |
the claims the change touches, and the changed lines of code no claim is known to run: those it does not run, an added line the record holds no statement on (a table, a constant; a comment on its own line is left out), and a file whose diff has no line |
claims_known |
whether every .geas has a record that fits the change |
generated_stale, generated_checked |
whether a generated file is stale, and whether that was checked at all |
generated_unchecked |
the changed generated files ritsu gen --check did not hold, its head being all that says a generator wrote them |
The exit, and the JSON
- 1 when the head has an error the base does not.
- 2 when that is not so and something could not be compared or decided, or the rule could not take the facts. A wrong argument and a rule that does not pass its check are also 2.
- 0 otherwise. Where the rule sends the change does not change the exit.
The JSON comes to its end in every case but a wrong argument or a rule that cannot be read:
{ritsu, base, head, files, check, behavior, interface, requirements, claims, generated, undecided,
facts: {<name>: <value>}, route: {file, outputs} | null, markdown}
route.outputs holds each output by its public name, and an enum's value by its public name too, so a
rule with names in Japanese gives a pipeline the same words. markdown is the report for a comment, at
most 60,000 characters, with every @ in code.
In a pull request
One run gives both the comment and the route, which jq reads:
# .github/workflows/ritsu-impact.yml
name: ritsu impact
on:
pull_request:
permissions:
contents: read
pull-requests: write
jobs:
impact:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: i2y/ritsu@v0.50.0
- name: Say what the change moved and where it goes
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
run: |
base="origin/$GITHUB_BASE_REF"
git show "$base:route.rule" > "$RUNNER_TEMP/route.rule"
code=0
ritsu impact --base "$base" --route "$RUNNER_TEMP/route.rule" --format json > impact.json || code=$?
jq -r .markdown impact.json > impact.md
gh pr comment "$PR" --body-file impact.md
route=$(jq -r '.route.outputs.route // empty' impact.json)
if [ -z "$route" ]; then exit "$code"; fi
if [ "$route" = owner_reads ]; then gh pr edit "$PR" --add-label "owner reads"; fi
if [ "$route" = stop ]; then exit 1; fi
- The rule is read from the base, so a pull request cannot change where it sends itself; a change to
route.ruleis a change like any other, and shows asrule_changeswhenroute.ruleis in the project. Merge the rule before adding the workflow. fetch-depth: 0fetches the history, so thatorigin/<base>can be read.- With a
ritsu.mod,ritsu impactfetches the modules both locks pin, asritsu mod reportdoes. In a repository that holds several modules, give the directory of each to--root: a change inside a module of its own is listed as not compared when the root is above it. - The label has to exist in the repository. A pull request from a fork gets a token that cannot write.
- For a record of the claims to count, the job has to run
geasfirst and leave.geas/<name>.map.jsonlof the code it is given. mergesays no person has to read the change, andritsu impactruns no claim: run the tests and the claims in the job as well, and merge on both.- The workflow that runs is the one in the pull request, so a change to it can change this step. It counts
in
other_files; have the repository's settings ask for a person's review of.github/as well. --lang jawrites the report in Japanese.