Your repository is full of sentences that must match something, and nobody checks the match. The schema must match the service that writes it. The rota must match who actually answers. Each pair held the day you or a colleague wrote it down, and nobody records the day the two stop matching.
One case, to be concrete. CODEOWNERS hands /src/api to @alice. The folder
was renamed to /services/api in the spring. The rule now matches nothing,
reviews still go to Alice, and no tool reports a problem: there was no
event on either file to notice. You find out at
an audit, or the day a colleague merges what nobody reviewed.
That gap has a name: drift. Two records of one fact, coming apart in silence, while both still get obeyed.
You know it by its local names: config drift, doc rot, schema drift, a stale CODEOWNERS. The mechanism is one: two records, and nobody compares them.
The usual cure is to pick one place and call it the source of truth. But a source has a direction: everything downstream must watch it, and it watches nothing. The folder was renamed in a pull request that never read the file. A file everyone must keep current is a chore for everyone, so it is a chore for nobody, and the chosen file goes stale like any other. gate starts from the opposite fact: there is no source, so there is no direction. The team that owns the folders declares them, once. The review rule declares the folder it points at. Two declarations about one folder cannot differ in silence. Either they are equal, or you get the exact point of contact:
$ gate status
status: refused 1
ownership.swift:89 · Owns_3_carol · Zone_docs against Zone_src: an owner and
the path they own must share one zone
That is a CODEOWNERS rule, translated once, judged on every change: carol
keeps docs, and one line assigns her a folder in src. The refusal
names the file, the line, and both sides of the disagreement. A diff
compares two texts. This compares two claims about one thing.
The same refusal on the live page (gate serve), at its line. The picture
is a door, and so is this line: the same bench, live in your
browser,
over the demo, judged as you type.
The check is one lookup per claim and nothing else: no search, no solver,
no build. The cost is linear in the number of claims: milliseconds on a
real repository, and still milliseconds when the repository is ten times
the size. So it runs on every keystroke, in every commit,
and in CI, with nobody waiting on it. The sentence is measured:
docs/BENCH.md carries the numbers, and python3 bin/bench.py reprints
them on your machine.
No server. No runtime. No new formats. Nothing leaves your repository.
git clone https://github.com/DanielSwift1992/gate && cd gate # no install step
./gate import codeowners CODEOWNERS --tree . --policy owners.csv -o ownership.swift # your own ownership, judged
./gate drift ../api/openapi.json --client ../sdk-js # your contract, your client
./gate serve # the same facts as a live pageRun the first two lines in repositories you already have. Ownership is a
fact you already keep, in a file that cannot say whether an
owner exists or whether a pattern still matches anything. The command
turns it into a judged world, and you write none of it: it writes one
small Swift file, ownership.swift, and no Swift toolchain is involved,
because the judge reads it directly. Drop the -o and it writes nothing
at all: the same reading, printed, and your repository left as it was.
owners.csv is two columns, and it
adds the one thing CODEOWNERS cannot express: which zone each owner
keeps.
Now break what you just imported. ./gate serve opens the bench on it:
rename the folder inside the declaration, and the refusal lands on that
keystroke, at its line. ./gate status in the terminal says the same
thing, word for word. Commit it, and the next section's one uses: step
has CI say it again on every push. Same judge, same declarations,
different moment.
drift reads an API contract and a client library out of git on your
machine, and prints what the copies have been doing to each other.
It uploads nothing and fetches nothing. It observes rather than
judges: the exit code follows a threshold you declare (--fail-over 30),
never a verdict of ours.
No repository of your own at hand? gate demo makes one: a small tree
with a CODEOWNERS whose one rule reaches outside its zone, so the first
thing you see is the refusal above. Everything it makes is committed the
moment it is made, and git checkout . is the whole way back.
Every command here is written gate. Until it is on your path it is
./gate, run from the clone: nothing else to install, nothing else to
undo.
One step checks every CODEOWNERS rule and workflow paths: filter
against the files that actually exist, on every push:
- uses: DanielSwift1992/gate@v0.2.3A rule that points to a deleted or renamed path fails the check, with
the file and line in the log and as an annotation on the pull request.
A repository with no CODEOWNERS and no path filters passes, and the log
says there was nothing to check. version: picks the gate release,
tree: checks a subdirectory instead of the root.
The state of the art for these is a search: grep, a ticket, whoever remembers. Here each is a lookup against the file, in milliseconds:
| The question | Today | Here |
|---|---|---|
| May this person read this document? | ask the owner, or read the ACLs by hand | gate check view Emp0042 FinanceShare |
| What breaks if we move them to Sales? | find out after the move | gate diff transfer Emp0042 Sales |
| Who may merge this, and since when? | convention, and memory | gate guard merge · gate log |
| Which commit broke this rule? | bisect by hand, if anyone notices | git bisect run gate status |
| Is anything inconsistent right now? | an audit, quarterly | gate status, on every keystroke |
The names come from a sandbox this repository makes for you; your own commands use your own names. You pay once, per name you translate. Every question after that is a composition of what you already translated, and compositions are free: the opposite of a reporting tool, where each new question is new work. The tool is general; the table is one example. You point it at any pair: a Jira ticket and the TODO that cites it, k8s RBAC and the cluster it describes, an API contract and a client in another language.
Two of those pairs are one command each today, because the half you keep is already a file somebody else's machine obeys:
./gate import codeowners CODEOWNERS --tree . --policy owners.csv # who owns what
./gate import workflows --tree . # what wakes a workflowThe second reads the paths: filters your workflows are woken by and
holds them against this tree. A filter naming a folder that was renamed
goes on being obeyed and wakes nothing: no red line, no log, no mail.
It reads the filter by its address in the document, and a file it cannot
read exactly it names, with the line, rather than guessing at it.
Yours is wherever two places state one fact. Take a thing you shipped this month and ask: where is this written down, and where else? Who checks the two against each other, and when did anybody last do it? The pairs you cannot answer for are your drift, and every team has them: the schema and the service that writes it, the contract and the client, the rota and the pager, the config nobody dares delete.
You are looking for pairs, not objects: one fact always has more than one record. And the loudest marker is anything that calls itself the source of truth: a self-declared source is the record nobody compares to the others any more, so that is usually where drift collects.
Gate first the pairs that cross a boundary: two teams, two repositories, two languages. A fact drifts where it changes hands.
The quick first look is one command, over the git history already in your
clone. gate findings prints plain sentences: who changed which facts,
across how many commits, and whether any hook or workflow checked those
edits. gate findings --history says when a pair parted and how much has
passed through since. It needs no setup and judges nothing yet: these
are readings, not verdicts.
Something drifts? Gate it
To gate a pair: put your half of it in a file, once. From then on every change is checked against it, and the other side sees it the next time they check.
If your half is already a table (CODEOWNERS, a CSV), one command puts it
in, and your file stays in place: whatever read it keeps reading it. What
enters gate is a second record of the same fact, the kind of pair this
tool exists to hold. A CSV round-trips: gate export prints the tables
back, and the diff against the originals is empty. A CODEOWNERS is judged
against your tree as it lands: the refusal at the top of this page is
exactly that, a rule reaching outside its zone, and a pattern that
matches no file is named beside the verdict. And the two stay compared:
the world names its source on its own from: line, every gate status
translates the file again and holds the two together, and a line changed
on either side alone is named at its line. If your half is not a table,
you write it as a file yourself: a page of plain declarations, translated
by hand, once. The rules are small, and they are yours to read. That is
the upkeep, all of it.
The same move works between teams. You do not test my API, and I do not mock yours. Each side states its half in its own repository, the seam is judged, and a disagreement is named at its address on both sides. What used to be an integration test is a verdict.
You already write claims: every line of CODEOWNERS is one. These seven
lines are that claim with its parts named: the zone, a path in it, an
owner posted to it, and the tie between them. gate bare prints them,
from the demo's ownership.swift:
Zone_docs: Realm
Path_2_docs_: Room
Place = Zone_docs
Owner_carol: Keeper
Post = Zone_docs
Key = WardenKey
Owns_2_carol = Owns<Owner_carol, Path_2_docs_>
The file on disk is full Swift, gate bare … --full prints it, and
swiftc -typecheck reads it as it stands: this view is a projection
over that one source, never a second copy of it.
The judge asks one question of every claim: the same, or not, and at
which line. The files are bare Swift: the same language with the ceremony stripped,
and no DSL. Records are declarations, rules are type constraints in the
same text, so a record that violates a rule does not get flagged: it fails
to exist. And because it is plain Swift, a second, independent reader
exists whenever you want one: swiftc -typecheck passes on these files as
they are, with no project and no build. Git keeps doing what it already
does best: history, authorship, review, rollback.
The porcelain is deliberately git-shaped: init · status/fsck · log · check · diff · apply · import/export · verify · guard · library · survey · drift · badge · mine · theirs · declare · seam · attention · serve · report · stdlib · my · demo · findings · --version. A refusal exits
non-zero, so hooks and CI need no wrappers, and every command ends by
naming the one step that comes next, so the whole ladder stays out of
your head. gate stdlib show forms-organization prints any shelf page
exactly as it shipped.
A drifted record is one problem: two spellings of one fact, and the judge names the line where they part. What two people mean by a record is another problem, and no check turns two readings into one. gate does not try. Its one move is to put the agreement in writing before the argument: each side states its half in its own file, the judge compares the halves from then on, and you hold a meeting when the verdict says you differ. If CODEOWNERS hands the payments folder to an intern, the rule holds and every record agrees with it. Whether it should is a claim you can state too: write it as policy, let the other side write theirs, and the verdict says if you agree. Agreement here is not assumed. It is stated twice, and confirmed.
Every pair you gate is one thing you stop keeping in your head. An unwritten agreement has no address: to obey one, you remember them all. A gated pair has an address: the judge brings the one line your change touched, and the rest stay written. Today you keep other people's facts by hand: you review renames because a stale rule routes them to you, you spend audit week rebuilding answers, and you remember what breaks when a path moves. Gate a pair and you state your half once; the judge holds it from there. Agreement is a verdict: a break is named at its line, on the breaker's screen, the day it lands. A rename goes red for the renamer. A new hire's access is one diff with an owner's name on it. Deleting an old config takes an afternoon, because its readers are a list. The checking does not pile up: a claim is one lookup, so a hundred pairs cost what ten did. Your work did not get faster. The asking is gone.
The cover you just read is the product. The pieces below are one file away, each where you would look for it:
- docs/DETAILS.md: carrying gate vendored in your repository, verifying the zero-egress contract yourself, where it plugs in (hook, CI, review, editor), layout and ownership, and the arithmetic of why a judged pair pays.
- docs/SECURITY.md: what a verdict promises, and where to report a wrong one.
- docs/CHANGELOG.md: what exists, in the order it came to be.
- After
gate init, your repository is met by a letter of its own:stdlib/readme.swift, beside your files and judged with them. gate init --ciwrites the other half of the same step:.github/workflows/gate.yml, which takes one binary from the latest release and asks it for a verdict on every push. No toolchain on the runner, nothing to install, and a step already there is left alone. The hook holds a commit on the machine that makes it; this holds what arrives.
LICENSE MIT · docs/NOTICE.md lists the bundled pieces and their terms
gate the shim that finds the binary: an explicit GATE_CLI, this
clone's own build, a copy carried in by `gate init --vendor`,
or one on PATH. It says so in one sentence when there is none
gate.cmd the same shim on Windows, in that platform's own spelling
action.yml the door for a stranger's CI: one `uses:` step fetches the
released binary at a pinned tag, checks the sha256 published
beside it, and judges the tree it stands in (the step is
bin/gate-audit.sh, and the battery runs its three vectors)
gate.manifest.swift
this repository's own declared layout: its worlds, their
roles, and the judge's row, judged like anybody's
CODEOWNERS · owners.csv
who owns this repository, and the zone that owner keeps:
the two files the first recipe above reads, kept here for
the same reason yours are kept in yours
ownership.swift this repository's own ownership, printed from those two by
the command CODEOWNERS itself names, and judged with the
rest of the world
gate.policy.swift
who keeps this repository, said once: the email git
records bound to the keeper ownership.swift declares. A
name the world does not declare is refused at this line
bin/gate-judge the judge, one static binary, built at a pin from the
public theory corpus, verification-is-identification:
github.com/DanielSwift1992/verification-is-identification
(bin/build-judge.sh [pin] rebuilds it)
bin/judge.js · bin/judge-cli.js · bin/judge-where.js
both courts as a node port, for machines the binary
was not built for, held to it line for line
bin/gate-cli.swift
the CLI, one file of Swift: every verb this tool answers,
with the court compiled in at the judge's own pin
(bin/build-cli.sh builds it; the binary is not committed,
because every executable line here stays text)
bin/shelf-into-swift.py
the shelf, written into the build as text so the binary
carries it: a downloaded gate has no stdlib/ beside it. The
pages themselves stay in stdlib/, and the battery holds what
the binary carries against them, page for page
stdlib/ the judge's own words, printed as real Swift files, self-judged
web/ui.html the workbench; bin/judge.js judges it in the browser
web/codemirror.* the editor (CodeMirror 5, MIT, vendored)
demo/ runnable worlds: CODEOWNERS + policy, CSV org, K8s RBAC
docs/ DETAILS.md, SECURITY.md, CHANGELOG.md, NOTICE.md, and
the cover's picture with its provenance
tests/smoke.py the battery: 562 checks this repository holds itself
to, end-to-end runs through judge parity through
documentation contracts; the definition of green
tests/windows.py the Windows measure: the reviewer's road as asserts
$ gate badge
badge: 189 claims · holds
The badge is this repository's own, and the judge re-counts it on every run.
Working prototype under active development, MIT licensed: see LICENSE, and docs/NOTICE.md for the bundled pieces and their terms. The judge is a native binary with a versioned verdict contract (canon v2). The CLI is one file of Swift, built into a single binary the way git is one tool. We want to hear about a wrong verdict before anything else: see docs/SECURITY.md.
Where it runs. The CLI is one binary, built from one Swift file by
bin/build-cli.sh, and the bench is a page. gate is a shim that finds that
binary, so a clone that has not built one says so in a sentence with the
two ways out. The judge is a native binary built for one platform: bin/gate-judge
here is Mach-O arm64, and on any other machine it does not execute. On
such a machine node runs both courts instead: bin/judge-cli.js runs the
plain court, bin/judge-where.js runs the certificate court, both ported
line for line, and the battery holds the port to the binary's own lines.
gate --version says which court ran on your machine. On macOS, CI runs
the full battery on every push. On Windows, CI builds this same one file
into that platform's own binary on every push, asks it what it carries, and
walks the road a reviewer walks: tests/windows.py makes the demo, takes
the kit, breaks a claim, and the break is refused at its line, on that
platform's own paths.
On Linux, CI rebuilds the judge at the same pin and runs the full
battery on every push, so every platform named here is measured. A release
carries the binary per platform, and beside each one its court's revision
and a sha256: the honest check is the rebuild, and both are one command.
Roadmap, next: apply routing over the declared layout · more domain forms.
Something drifts? Gate it
