Skip to content

Latest commit

 

History

History
619 lines (518 loc) · 36.4 KB

File metadata and controls

619 lines (518 loc) · 36.4 KB

Contributing

Before you push

Two commands. They are the ones the gate runs, character for character, so a green run here and a green run there are the same run:

cargo build --locked --all-targets
cargo test --locked

A fresh clone needs a Rust toolchain and a network. The manifest declares dependencies, so the first build fetches before it compiles, and --locked is what makes that restore refuse to rewrite Cargo.lock rather than doing it quietly. It is in both commands for that reason. What is declared, and what it reaches, is read rather than written here:

cargo tree -e normal

THIS PARAGRAPH SAID A FRESH CLONE NEEDED THE TOOLCHAIN AND NOTHING ELSE, ON THE GROUND THAT THE MANIFEST DECLARED NO DEPENDENCIES. It declares some, and the sentence saying what --locked would be for when that changed was left standing above the sentence it was about, so this file went on promising that nothing is fetched. README.md carried the correction already and this one did not, which is the same drift the section below names for the two commands, one paragraph earlier and in the file that names it.

THE COUNT THAT REPLACED THE NONE WENT STALE THE WEEK AFTER IT LANDED, AND THERE IS NO NUMBER HERE NOW. It said one. A second entry arrived on 2026-09-04 with docs/decisions/0292-the-means-the-core-speaks-http-with.md, in a change about how the core speaks HTTP that had no reason to open this paragraph, and both this file and README.md went on saying one. A number here is read by nothing that would have said so, so the count is deleted rather than corrected a second time, and the command above is what answers it.

The toolchain version is pinned in rust-toolchain.toml, in one place, and no workflow carries a copy of the number. The toolchain manager reads that file itself, so a fresh clone compiles with the pinned version without being told to install anything. Where a compiler arrived some other way, the build check says which version this tree expects and which one ran, by number:

bash .github/toolchain/toolchain.sh check

The gate still prints the compiler it used on every run, so a verdict can be read against a version rather than against a promise about one.

README.md carries the same two commands and the arrangement of the tree. This document is the one that says what happens around them.

No work without an issue

Every change starts as an issue and lands as a pull request. Direct pushes to main are refused.

An issue says what is wrong, what the evidence is, and what "done" means. If the evidence is a number, it carries the command that produced it. A sentence with a number and no command is a claim, and the difference between a claim and evidence is one line.

The same holds in a pull request body. Every number in it carries the command that produced it, run at the commit being pushed rather than in a working tree that has something else in it. Where a claim cannot be backed by a command, write it as a claim and say so.

A negative statement stays negative. If something was not done, not measured, or not covered by any run here, say so plainly and do not soften it in a later edit.

Sign your work

Every commit carries a Signed-off-by trailer matching its author, and the check named DCO sign-off refuses a pull request where one does not:

git commit -s

Retroactively, for a branch that already has commits:

git rebase --signoff <base>

What the trailer refers to is the developer certificate of origin in the file named DCO at the root of this repository. Signing off is a statement about the provenance of what you wrote, not a formality.

What the gate runs, and what each thing refuses

The list below goes stale the day a check is added. Derive it rather than trusting it:

gh api repos/Flowfin/core/commits/main/check-runs --jq '.check_runs[].name' | sort

build compiles the tree with every compiler warning an error, using the first command above. Before it compiles anything it compares the compiler that ran against the version rust-toolchain.toml pins, and refuses a mismatch by number rather than letting it arrive as a compile error. .github/toolchain/toolchain.sh holds that comparison and the fixtures that prove it. .github/workflows/build.yml.

That command is not a build of everything, so the check does not stop there. --all-targets selects the test targets carrying test = true, and a second step compiles the ones this manifest declares with test = false, without running any of them. The set is derived from Cargo.toml rather than written down, and a disagreement between the names read and the occurrences of the setting they came from is refused, because a block reader that stopped matching reports an empty set and reads exactly like a manifest with nothing to compile. .github/excluded-targets/excluded-targets.sh holds the rules and the fixtures that prove them.

It is also a build for one triple, which is the runner's, and that is what targets is beside it rather than inside it.

targets compiles the library once for every target triple a client links it on, which is 0113's set. It is a compile and never a run: nothing in the suite executes on any of those triples, and the check prints that bound beside its own verdict on every run. The set is data in .github/targets/targets, one triple per line with the reason it is there, so adding a platform is editing a register and the argument for the platform sits on the same line as the platform. An entry carrying a triple and no reason is itself refused, so is a triple this compiler does not know, and so is a register naming nothing, because an empty register compiles nothing, exits zero and prints a page indistinguishable from a run that compiled every platform. The derived set is counted against the raw lines it came from for the same reason the manifest reader above is. .github/targets/targets.sh holds the rules and the fixtures that prove them.

test runs the suite with the second command above, in the environment #20 requires and described below, and refuses a run that collected nothing. A harness that ran no test exits zero and prints a page that reads like a clean run, so the count is read out of the run, printed, and refused when it is zero; a non-zero filtered-out total is refused for the same reason. The number of tests executed is written to the step summary. .github/test/test.sh holds the counting and the fixtures that prove it.

lint runs the analyser the language ships, denying its default, pedantic and manifest sets, with every remaining warning an error. The lints it does not refuse are in .github/lint/excluded-lints, one per line with the reason, and a line carrying a name and no reason is itself refused. .github/lint/lint.sh holds the settings and the fixtures that prove them.

format runs the formatter the language ships over every tracked source file, in --check mode, so a carriage return is refused rather than converted. The subject is git ls-files rather than the crate, because cargo fmt walks the module graph and never opens a tracked file no mod declares. The files it does not ask about are in .github/format/unformatted-paths, one per line with the reason, and a line carrying a path and no reason, or a path the tree no longer carries, is itself refused. .github/format/format.sh holds the settings and the fixtures that prove them.

invariants applies a rule set held as data in .github/invariants/rules to the tracked files each rule names, one extended regular expression per rule, and refuses a line that matches. Each rule carries the record it comes from and the failure it prevents, and the loader refuses a rule that carries neither. Each also carries a line that violates it and a line that nearly does, and every run judges both against the whole rule set before it judges the tree, so a rule proves it bites and proves it bites alone. .github/invariants/invariants.sh is the loader.

Five of the rules read a list of names somebody wrote down, and what they are is worth reading before treating a green run as the property held. Three are #77's, grounded in the boundary record: one refuses a windowing, drawing or user-interface package in the committed lockfile, which is the resolved dependency graph rather than the manifest; one refuses the words a view layer thinks in; one refuses a type of this core carrying its own wording for a person. The fourth is #73's, grounded in the data-locality record, and refuses a telemetry, analytics or crash-reporting package in the same lockfile. The fifth is #266's, grounded in the certificate record, and refuses a logger registered behind the logging facade that record admits; it reads five registration call names, so a registration written through a name nobody listed walks past it and the ground the narrowing in 0243 rests on is gone with a green run beside it.

THIS PARAGRAPH SAID FOUR UNTIL #368, AND THE FIFTH RULE LANDED ON 2026-09-01, in a change about a logger that had no reason to open it. The number is a hand count over the blocks of .github/invariants/rules and no command derives it: two of the five say in their own prevents line that they hold a name list rather than a purpose test and three do not, so a reader who wants the set reads the blocks. It is a hand count in the same sense as the count of runs that report and refuse nothing, further down this section.

Each of the five holds what somebody has named rather than a purpose test, so a crossing written in a name nobody listed passes all of them. The boundary record's own sentence that the forbidden side cannot be expressed as data is unchanged by the three, and the run prints that bound beside its verdict.

DCO sign-off refuses a commit whose trailer does not match its author.

Deterministic PR-hygiene checks refuses a pull request that names no issue and one whose body says nothing the template did not. Where an issue it names declares a Scope: line at column zero, the changed paths are compared against it; where none does, the run prints that the comparison was not made. .github/pr-hygiene/hygiene.sh.

hygiene / Deterministic PR hygiene is a second hygiene run on every pull request, and its rules are not in this tree. .github/workflows/shared-hygiene.yml calls a workflow pinned by hash in another repository; that file is where the argument for calling it beside the local gate lives, and it states what the two have in common - that a pull request body has to say something and name the issue it belongs to. What this board holds on top of that is the entry above, and the scope comparison there has no equivalent in the shared check at all. The two names differ by a hyphen and a word, so a refusal reported under this one is not answered by reading .github/pr-hygiene/hygiene.sh, and a refusal reported under the name above is not answered by reading the called workflow.

Documents name paths that resolve refuses a Markdown link target or a code span that names a path not tracked in this tree. .github/doc-paths/doc-paths.sh carries what it reads and, on every run, the list of what it does not.

A narrowing is named in both directions refuses a Narrows: or Narrowed-by: field in a decision record that names a record which does not exist, that names a record and no clause, that sits below the first heading, or that the record it names does not name back. Both fields are 0267's, which is where the shape and its reasons are; .github/decision-records/decision-records.sh holds the rules and proves every one of them against its own violating record and its own near miss before it judges anything. What it cannot reach is printed on every run: whether the clause a field names is the clause that actually moved is a judgement no reading of the text makes, and a later record that narrows an earlier one and writes no field at all is silent to every rule in it.

Every scanner exclusion has a statement validates security/statements.json, which says publicly why a finding this repository does not fix does not apply, and compares it against the exclusion registers the tree carries. A malformed document is refused before any rule reads it, an entry missing a field or carrying one blank is refused, and the comparison runs in both directions: a directive a register excludes with no statement, and a statement for a directive no register excludes any more. The registers are derived rather than listed, so one added tomorrow is read on the day it lands. .github/statements/statements.sh proves every rule against its own violating document and its own near miss before it judges anything, and prints what it cannot reach: whether a statement is true, an exclusion that is not a directive in a register, and a finding dismissed on the code-scanning surface, which is a state on the repository rather than a byte in the tree.

Analyse the shell the gate runs (shellcheck) analyses every tracked shell file. The rules it does not refuse are in .github/shell-analysis/excluded-rules with the reason for each.

fixture-scrub refuses a value in a recorded fixture that a scrubbed recording does not carry: a session token, a password, a server address, a device identity or a server-supplied identifier. It asks about membership of a declared set rather than about a shape, because a synthetic identifier has the shape of a real one and a rule written on the shape alone refuses every recording or none. The set is data, in .github/fixture-scrub/values, one block per rule with the record it comes from and the failure it prevents on it, and .github/fixture-scrub/fixture-scrub.sh proves every rule against its own violating line and its own near miss before it judges anything. A refusal names the file, the line and the kind, and never the value. What it cannot reach is printed on every run: the personal data list in docs/decisions/0068-the-data-locality-position.md is closed by a question a contributor answers, so a title, an account name and a viewing history walk past every rule, and nothing here reads the history. tests/recorded/README.md is the procedure that produces a recording which passes, and it is written before the first recording rather than after it, for the reason #99 already gives about line endings.

Analyze (rust) runs the code-scanning surface's own semantic analysis over src/, which asks a question a pattern cannot: whether a value that arrived from outside reaches a place that trusts it, across the calls in between. The verdict is this repository's rather than the action's, since the action uploads findings and fails no build on one: .github/codeql/codeql.sh reads the file the analysis wrote and refuses a finding the register in .github/codeql/excluded-rules does not excuse, an identifier written there with no reason is itself refused, and a file carrying no analysis run or no loaded rule is refused too, because a query set that never loaded reports nothing and reads exactly like a clean tree. The findings reach the code-scanning tab as well as the job log, and the upload is skipped on a pull request from a fork, where the token cannot write there and the gate still refuses.

Audit workflows (zizmor) audits the workflow files themselves.

Reject Trojan Source Unicode refuses bidirectional and invisible Unicode control characters in tracked text.

thread-detector runs the suite a second time under a thread detector and refuses a report against it, which is a claim about a schedule rather than about a shape and is the one that tests/thread_statements.rs cannot make. It then runs tests/a_race_the_detector_must_catch.rs, which holds a data race written on purpose, and refuses a run of that target which reports nothing: a detector that was never switched on reports nothing and prints a page indistinguishable from a clean tree. The verdict is read out of the detector's report and never out of an exit code. A finding it does not refuse is written in .github/thread-detector/suppressions with the reason, an entry there carrying no reason is itself refused, and the run derives the detector's own suppression file from that register so there is no second file to keep in step. .github/thread-detector/thread-detector.sh holds the rules and the fixtures that prove them. It is the one leg whose compiler is not the pinned one, for the reason rust-toolchain.toml gives, and it names its own toolchain and target and prints both on every run.

coverage measures line coverage with the compiler's own instrumentation and judges it on the surface .github/coverage/pinned-surface names. That file is a register of the kind .github/invariants/rules is - a kind, a path and the reason on one line - and a line missing any of the three is refused, as is an area naming nothing tracked and a tracked source file under an area that no module line lists. The bar is on the modules the register names rather than on the whole tree, and the number is in the script with the measurement it was set from rather than here: a whole-repository percentage is met by covering the easy half and missed by adding a large trivial module, so a bar over it cannot do what #84 asks of one. The whole-tree figure is measured and printed on every run and gates nothing, which is the half that is tracked rather than enforced. It reads the counters with the tools in the pinned toolchain's sysroot rather than whatever LLVM answers on the path, because a system LLVM of another version answers differently and a verdict that depends on who ran it is not a verdict. .github/coverage/coverage.sh holds the rules and the fixtures that prove them.

mutation changes the code under the suite and reports which changes the suite failed to notice, which is the question neither test nor coverage can ask: a test that reaches a line and asserts nothing about the answer is green in both. It runs weekly and on request rather than in front of a merge, because every mutant is a build and a test run of its own, and IT GATES NOTHING. A survivor obliges an issue on this board carrying the mutant in the analyser's own words, the module it is in, and what would have caught it - not a re-run and not a quiet acceptance, because a number produced on a schedule that nobody must answer is a number nobody reads. There is deliberately no suppression register beside it: excusing a mutant raises the published score with no test written, which moves the number in the one direction that looks like progress and is not, so a mutant nobody can kill is written into its issue with that reason and the issue closed on it. Its scope is the area lines of the same register coverage pins its bar to, so a source file added under one is mutated whether or not anybody remembered it. .github/mutation/mutation.sh holds the rules and the fixtures that prove them.

dependencies restores in locked mode, so a restore that would rewrite Cargo.lock fails rather than proceeding quietly, and scans the graph the committed lockfile declares against the advisory database. It reads back how many packages the scanner says it examined and refuses a disagreement with what the lockfile declares, because a scanner pointed at a moved file exits zero and prints a page that reads exactly like a clean scan. An entry carrying no source line is not matched by that scanner, so the run also reports how many of the packages carry one. .github/dependencies/dependencies.sh holds the rules and the fixtures that prove them, and the fixture lockfiles beside it are one-change neighbours of each other.

dependency-review reads the dependency diff of a pull request against the advisory database.

branch-health is the one check here whose subject is not this tree. It walks the workflow registry, reads each workflow's most recent runs on the default branch, and reports any whose latest conclusion is not success, opening an issue per such workflow rather than sending a notification, because a notification is read once and an issue survives being missed. Every other check reports on a pull request, where somebody is waiting on it; a run that concludes only after a merge or on a schedule has nobody waiting on it and can stay red for weeks with every pull request green beside it. What it refuses is being unable to produce a report at all - a listing it could not read, rows in a shape it does not parse, a registry naming nothing - and never a red workflow, so its own run does not go red for what it found and cannot end up reporting itself forever. It prints the registry entry by entry with what it read of each, so a workflow with no run on the default branch is named as one this run did NOT examine rather than counted as healthy, and it prints how many of each workflow's runs it looked at against how many exist. .github/branch-health/branch-health.sh holds the rules and the fixtures that prove them. The passage below walks the runs that report and refuse nothing; what this one refuses is written above, and reading the two together is what says whether a green tick here is a merge condition.

THIS SENTENCE COUNTED THAT PASSAGE AND THE COUNT WENT STALE. It said this leg would be a third member of a pair below, written on 2026-09-02 when the passage held two entries, and the passage has held four since 2026-09-16. A size written here is a second copy of the one the passage already carries, no route in this tree reads either, and the next leg to arrive would make a third copy, so the number is deleted rather than corrected. The ground it separated this leg on was not this leg's alone either: cross-toolchain below refuses a survey that cannot be read, which is the same shape as refusing a run that produced no report. Whether this leg belongs in that passage is open and is not answered here.

cross-toolchain is the one check here whose subject is the runner image rather than this tree. It compiles one C translation unit for every triple in 0113's set, using the invocations named in .github/cross-toolchain/probes, and reports which of them this image can compile C for. 0243 validates a certificate through rustls, every crypto provider rustls offers is C, and 0291 chose to wait rather than to buy a cross-toolchain or to drop a client platform - and said in its own text that it made that choice without this measurement, every reading behind the question having been taken on a contributor's Windows machine. This leg is that measurement, and it is re-taken on every run because an image gains and loses a toolchain on somebody else's schedule. The set is not read twice: the triples come from the targets leg's own reader through bash .github/targets/targets.sh triples. .github/cross-toolchain/cross-toolchain.sh holds the rules and the fixtures that prove them.

FOUR RUNS REPORT AND REFUSE NOTHING, which is deliberate rather than an oversight, AND THIS PARAGRAPH SAID TWO AND THEN THREE. Each move was a leg arriving rather than a decision changing, and this one is mutation above, which stood in the tree for twenty days before this section named it at all. The number is a hand count over the entries in this section and no command derives it, so it is right about what is described here and says nothing about what the tree carries. External addresses in documents requests the addresses documents name and prints what answered; an address outside this repository that is down for an hour is not a defect here, and a gate that reddens for it teaches people that red means nothing. Scorecard analysis scores the repository and writes to the code-scanning surface. The third is cross-toolchain above, and what it declines to refuse is narrower than either: it refuses a survey that cannot be read - a register block missing a field, a triple in 0113's set no block covers, a probe for a triple that set does not name - and never the finding that a triple has no compiler here, because that is the state 0291 already decided to wait through and a red tick for it would redden every pull request for a decision already taken.

Which of these is a gate, and which is a sentence

Read this before treating a green tick as a merge condition.

No check is required to merge. The ruleset on main requires a pull request, requires a verified signature, and refuses a deletion and a rewrite. It names no status check at all:

gh api repos/Flowfin/core/rulesets/20572113 --jq '{enforcement, bypass: .bypass_actors, required: [.rules[].type]}'
{"bypass":[],"enforcement":"active","required":["deletion","non_fast_forward","pull_request","required_signatures"]}

So a red build blocks nothing today. #26 is where the names are written into that ruleset, and what that issue waits on is written on it rather than restated here. Until then, whether a red check stops a merge is a person's judgement, and the rule is that it does.

THE PASTE ABOVE CARRIED THREE RULES, AND THE FOURTH IS THE ONE THAT REFUSES A MERGE WITH EVERY CHECK GREEN. The sentence under it named an issue that has since closed as what #26 was waiting on, which is why neither is stated here any more. Enforcement is active and the bypass list is empty, so one commit without a verified signature anywhere in a branch's history refuses the merge. That is not the Signed-off-by trailer the section above is about: a trailer is text in a message and DCO sign-off reads it, a signature is a cryptographic object over the commit and no check in this tree reads one. Both are required and neither stands in for the other, and the only thing that says a signature is missing is the merge at the end of the line. So a signing failure is fixed rather than pushed past: the way around it is one flag, and taking it moves the refusal to that point, after the review has already been done.

The two commands above are prose. Nothing compares what a workflow invokes against what this document and README.md say a contributor runs. The three are kept in step by whoever edits one of them.

Formatting is checked, and cargo fmt is not what checks it. Run cargo fmt before pushing and the gate will agree with you for every file the module graph reaches. It reaches every source file in this tree today, and it is not what the format check runs: that reads git ls-files, so a source file added without a mod declaring it is judged here and would pass cargo fmt --all --check untouched. bash .github/format/format.sh check is the run the gate makes, and it needs no network.

A test that needs real hardware or a real server does not run here. It has a harness of its own, tests/needs_a_real_server_or_real_hardware.rs, declared in Cargo.toml with test = false so that cargo test --locked never invokes it and nothing it needs can make the headless suite conditional. Running it is deliberate:

cargo test --locked --test needs_a_real_server_or_real_hardware

What it covers that the headless suite cannot: a genuine TLS handshake against a genuine certificate chain, a real server's behaviour under a real sign-in, and image decoding at real sizes on real hardware. A fake proves the core's reaction to a shape; it cannot prove that the shape is what a real server sends or that real hardware decodes what was handed to it.

It refuses rather than skipping. An absent prerequisite prints every missing variable by name and exits non-zero, and so does a run that carries no case, because a skip exits zero and cannot be told from a run that proved something.

It carries no case today, so every path in the list above is untested rather than tested elsewhere, and a pull request touching one says so in its own body. The first case needs behaviour that does not exist yet; #27 is where the earliest of it arrives.

A second target sits outside cargo test --locked for a different reason. tests/a_race_the_detector_must_catch.rs holds a data race written on purpose, so that the thread-detector leg has something it can be seen reporting. What keeps it out of the ordinary command is not a prerequisite it needs but what its body is: a contributor running the suite must not execute undefined behaviour. The leg that reads it builds and runs it on every pull request and refuses a run of it that reports no race, so unlike the harness above it cannot rot unnoticed. It is the one place in this repository that writes unsound code, src/lib.rs forbids unsafe code and this file is outside it, and the file says both at the top of itself.

Every test in this repository runs headless, which is #20's rule. Every test runs with no display server present and as a non-elevated user. A test that needs either is a defect in the test rather than a step to document.

The reason it is a rule rather than a preference is that it cannot be added later. A suite that grew up assuming a display, a keychain prompt or an administrator is not made headless afterwards without rewriting the tests that matter most, and by then somebody will argue that the ones needing a display are the important ones.

One concrete case, written here so nobody rediscovers it. Binding a socket to a machine's own interface address rather than to loopback raises a firewall consent dialog on Windows. The dialog is answered by an administrator, and its subject is the executable's full path, so answering it settles nothing for the next build directory. A test that needs that bind belongs in the separate harness above.

The gate enforces the environment, and it does not enforce the rule. Those are two different things and the difference is worth reading.

The test check runs the suite inside a network namespace carrying only loopback, under the runner's own unprivileged user, with new privileges refused by the kernel, and on an image with no display server. .github/headless/enter.sh creates that environment and .github/headless/headless.sh reads it back and refuses the run when any of the three is absent, because a sandbox nobody checks is a sandbox that silently stops being one. So a test that opens a display finds none, a test that asks for elevation is refused by the kernel rather than granted by a passwordless sudo, and a test that binds a socket to the machine's own interface address cannot, because that address does not exist in there.

What is still not refused: a bind to the wildcard address. It succeeds, it reaches nothing from inside that namespace, and nothing here fails it. And none of this reaches your own machine - a test that needs a display passes on a laptop that has one, and the gate is where it stops.

Nothing reads the prose of an issue, a commit message or a pull request body. Scope: at column zero is the only line any route takes out of an issue. Whether a body says what changed, what failure it prevents, or what was not covered is read by a person.

Fixtures

A fixture exists to prove an exact sequence of bytes, so nothing under tests/fixtures/ is translated on the way into the tree or out of it. .gitattributes carries that rule, and tests/fixture_bytes.rs is what goes red when the rule is removed.

Read a fixture as bytes. Every convenience for reading lines treats a carriage return and a line feed as the same thing, which is exactly the difference a fixture in that directory exists to hold.

Decisions

A decision that shapes this repository is a file under docs/decisions/, in the shape docs/decisions/0001-decision-records.md fixes: what was decided in one sentence, what would have gone wrong without the record, the alternatives with what each one cost, and a reversal condition somebody can check against the world. A record is added or superseded and never edited in place.

The number of a record is the number of the issue whose decision it records, and docs/decisions/README.md lists every record. Adding a record without its index line leaves the index wrong and nothing here refuses that.

A record written from 2026-09-05 on states what it decides and does not explain why the record exists or speak about itself. "The failure this record exists to prevent" and "what this section exists for" are the shapes it does not use: a sentence about the record's own presence tells a reader nothing that the decision, the failure and the reversal condition do not already say. Records that predate that date keep their wording deliberately. Rewording is not one of the in-place edits the register allows, and how many of them there are is not written here: 0001 lists the ones it permits, and its own header names every record that has narrowed it since, one of which added a fourth and one of which reshaped the third without adding one.

git grep -nE '^Narrowed-by: |^Three edits are not supersessions' \
  -- docs/decisions/0001-decision-records.md

So the sentences already landed stay as they are, and which records carry one is derived rather than listed here:

git grep -nE "this (record|section) exists" -- docs/decisions/

Nothing refuses the shape in a new record. The rule is prose, and the review of the pull request that adds the record is where it is caught.

Adding a check to the gate

Where a check needs logic rather than one command, the logic goes in a script beside the workflow rather than in steps inside it, and the script carries fixtures proving each rule bites. Each of those scripts runs its own fixtures before it judges anything, so a rule cannot pass its fixture and refuse something else in the gate.

WHICH SCRIPTS THOSE ARE IS DERIVED RATHER THAN LISTED, AND THIS PARAGRAPH USED TO LIST THEM. The list named eight, and the tree it described carried twelve at a22c8bd027e47eba9a732a1ad51c81589cb534b1, so a reader taking it for the set was four short. The sentence beside it already told them to derive the count, and now the set goes the same way. Neither line below has a number under it, because the answer is whatever the tree holds when you run it:

git ls-tree -r --name-only origin/main -- .github | grep '\.sh$' | wc -l
git ls-files -- '.github/**/*.sh'

THE OUTPUT PASTED HERE SAID TWELVE AND THE TREE CARRIED SEVENTEEN, which is this paragraph's own subject arriving in this paragraph. It records a list that went four short and hands the reader a command instead, and the output beside that command then went five short. The reading that recorded seventeen was taken at 74066f7b9ac3972b779ed1ef60e1d48a6dd74965, and it is pinned to that commit here rather than left standing under a command that asks the mainline:

git ls-tree -r --name-only 74066f7b9ac3972b779ed1ef60e1d48a6dd74965 -- .github \
  | grep '\.sh$' | wc -l
17

SEVENTEEN STOOD UNDER THE UNPINNED COMMAND UNTIL THIS EDIT, AND THAT COMMAND HAS STOPPED ANSWERING IT. At 4e95353399ef7eec5aad2c782d8a861ac39de66a it answers twenty-two, five more than the paste beside it, which is the same distance this paragraph already records twice. The sentence calling seventeen a reading rather than a fact about the tree tomorrow was right and is kept; what was wrong is that the commit it was read at lived only in the prose, so a reader running the line as written got a number the paste contradicted and nothing on the line said which of the two was theirs. The reading is pinned rather than refreshed, because a fresh number under a moving reference is this same defect one merge later. It was found by running the command as it was written.

A rule that is turned off is turned off in a register beside the script, one entry per line with the reason on the same line, and the run refuses an entry that carries no reason. An exclusion is a debt rather than a dispensation, so the reason says what would retire it.

A rule that is turned ON can live in a register too, and where the rules are the thing that changes rather than the logic that applies them, it should. .github/invariants/rules is the one that does: a rule there is a block of fields naming the record it comes from, the failure it prevents, a line that violates it and a line that nearly does, and the loader refuses a block missing any of them. Adding an invariant is then editing a register rather than editing a script, and the argument for the rule sits on the same block as the rule.

The check-run name matters as much as the check. GitHub takes it from the job's name: and falls back to the job id, a ruleset matches that literal string, and a renamed job silently detaches a requirement from the thing it was requiring.

What a pull request body carries

The template prompts for it and no check reads it. What a reader needs: what changed in the words somebody will use in a year, the specific failure it prevents, the evidence with its commands, what a guard here refuses together with the run that watched it fail, what the change does not cover, and whether anybody other than the author has read it.

Where a change adds or edits a guard, show it refusing: the deliberate violation, the run that went red on it, and the run that stays green without it. A guard nobody watched fail is a guard nobody knows the direction of.