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.
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.
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.
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.
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.
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.
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.
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.
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.