Governance
OpenRegs turns law into data other people rely on. Somebody has to be answerable for each answer it gives, and this document says who, by when, and what to do when that fails. It is deliberately short: everything here that can be checked by a machine is checked by one, and the parts that cannot are marked as such.
- Who owns what lives in
MAINTAINERS.yaml, which is the single source of truth..github/CODEOWNERSis generated from it and the pre-receive hook reads it; neither is written by hand. - How to contribute is
CONTRIBUTING.md. - How a review is actually driven, command by command, is
docs/runbooks/atom-review.md.
1. Maintainer teams
Section titled “1. Maintainer teams”Ownership is always assigned to a team, never to a person, so that one person
leaving never leaves a path unowned. The teams, as MAINTAINERS.yaml declares
them:
| Team | Answerable for |
|---|---|
@openregs/core-maintainers | The fallback: tooling, docs, and anything no narrower rule claims. |
@openregs/fixreg-maintainers | The FIXREG regime — its canonical texts, its obligation atoms, its graph. |
@openregs/spec-maintainers | spec/: the schemas and taxonomies every regime is validated against. |
@openregs/governance | Who is answerable: this roster, the generated CODEOWNERS, the branch-protection policy. |
All four teams currently hold one person. MAINTAINERS.yaml names one
maintainer and no others, because everyone it names is somebody who agreed to be
named: it carried two invented personas at .invalid addresses until they were
removed, and openregs init-regime had already copied them into a public
repository of real law. The teams stay four so that handing spec/ or a regime
over later is adding a member rather than rewriting every rule that mentions it.
What one-member teams cost is stated rather than hidden, in two places that both end when a second person joins: the escalation ladder in §4 reaches nobody the responder team does not already hold, and the review posture in §5 does not ask GitHub to require an approval it would refuse to count. Neither is worked around.
Per-regime teams
Section titled “Per-regime teams”Each regime under regimes/ names exactly one maintainer team, in the
regimes: block of the roster. That team owns the regime’s canon/ and atoms/
— the two paths where merging turns a machine proposal into law — and everything
else in the regime directory:
| Regime | Maintainer team |
|---|---|
fixreg | @openregs/fixreg-maintainers |
A new regime is not governed until it has an entry there. make gen-codeowners
routes it; nothing else has to be edited, and nothing else may be.
Machine identities
Section titled “Machine identities”openregs-bot (role proposer) opens atomize/* branches and can never
approve one. openregs-ci (role ci) is the only identity whose check results
count. Both restrictions are enforced by the pre-receive hook, not by convention.
A bot handle is a role name the hook matches an incoming pusher against, not a
person and not an account. The roster gives bots no name and no address, they are
never written into a Reviewed-by trailer, and listing one neither claims that a
forge account exists nor creates it. Until an account of that name exists with the
scopes its role needs, the role binds where the hook runs and nowhere else — the
same limit §5 draws around review.
Joining and leaving
Section titled “Joining and leaving”Adding a name to MAINTAINERS.yaml adds somebody who can merge law into the
canon, so /MAINTAINERS.yaml is itself an owned path: @openregs/governance must
approve. A maintainer may step down by opening the same kind of pull request; a
team that has stopped answering is dealt with by the ladder in §4, which ends in
that pull request being opened by somebody else.
Only somebody who agreed to it goes in the roster. A name there is a published
claim that a particular human is answerable for what merges, and it is what every
merge commit’s Reviewed-by trailer is written from. Padding it to make a rule
demonstrable is how two people who do not exist came to be the declared
maintainers of a public corpus of real EU law. So make gen-codeowners — and CI’s
python -m openregs.governance --check, here and in every corpus repository
openregs init-regime creates — refuses to generate ownership from a roster
naming a person at a domain the RFCs reserve so that it can never resolve to
anybody. Rules that need several identities are exercised against fixtures built
inside a test, which is what a fixture is for.
2. Ownership has one source
Section titled “2. Ownership has one source”make gen-codeowners # regenerate .github/CODEOWNERS from MAINTAINERS.yamlopenregs validate maintainers MAINTAINERS.yamlThe test suite runs the generator with --check and fails when the committed
.github/CODEOWNERS and the roster have drifted. Editing CODEOWNERS by hand moves
ownership somewhere the roster does not know about, which is the one failure a
single source of truth exists to prevent.
Two pattern forms are supported and the generator refuses to emit anything else:
* and a root-anchored path (trailing slash for a directory). A pattern the hook
cannot evaluate is a rule that silently owns nothing.
3. Review service levels
Section titled “3. Review service levels”These are promises about time, and they are machine-readable: the
review_slas: block of MAINTAINERS.yaml carries the same durations, and the
test suite fails if this table and that block disagree. Durations are business
days throughout — measuring a review promise in calendar days quietly promises
the weekend too. The clock starts when the item is opened.
- First response — somebody with standing has read it and said something.
- Resolution — merged, closed, or a stated reason it is neither. “Still reading Article 5” is a resolution if it is written down; silence is not.
- Escalate after — past this, the ladder in §4 opens.
| Item | What it is | Owed by | First response | Resolution | Escalate after |
|---|---|---|---|---|---|
atom-pr | a machine-proposed obligation atom waiting on a human | the regime’s maintainer team (@openregs/fixreg-maintainers for FIXREG) | 2 business days | 5 business days | 10 business days |
quarantine-item | a document a validation gate refused, waiting on triage | the regime’s maintainer team (@openregs/fixreg-maintainers for FIXREG) | 2 business days | 10 business days | 15 business days |
spec-change | a change to a schema or taxonomy under spec/ | @openregs/spec-maintainers | 3 business days | 10 business days | 20 business days |
roster-change | a change to MAINTAINERS.yaml | @openregs/governance | 3 business days | 10 business days | 20 business days |
Why atom-pr is the tightest clock: proposing an atom costs a second and
reviewing one does not, so the queue in front of the humans is the thing that
grows. Why quarantine-item is looser: its fix is usually a mapper change plus a
replay, not a reading — see
docs/runbooks/quarantine-triage.md.
What an SLA does not do. It does not weaken a single rule in §5. An atom PR that nobody has reviewed by day 5 is late; it is not thereby merged. Lateness is answered by escalation, never by lowering the bar for a merge, because the bar is the only thing standing between a machine’s reading of a statute and an answer somebody relies on.
4. Escalation
Section titled “4. Escalation”Each rung is measured from when the item was opened, not from the previous rung, so a stalled item climbs the ladder on its own schedule.
| After | Escalate to | What you do |
|---|---|---|
| 10 business days | @openregs/core-maintainers | Comment on the item naming the team. Any core maintainer may review it in the responder team’s place. The code-owner rule is not waived: this works because core-maintainers own the fallback, and reviewing a regime’s paths means being added to that regime’s team. |
| 20 business days | @openregs/governance | Open an issue naming the team and link the item. Governance answers in writing with one of three things: a reviewer assigned by name, a decision that the item will not be taken, or a date it will be revisited. |
| 30 business days | @openregs/governance | Governance proposes a change to MAINTAINERS.yaml — a member added to the responder team, or the regime reassigned. Nothing else on this ladder changes who is answerable; this rung is the one that does. |
Escalation is public. It happens on the item and in the repository, never in a private message, because “who was asked and when” is part of the record for the same reason “who reviewed what” is.
What the ladder currently does. Read it against MAINTAINERS.yaml before
believing it: every rung escalates to a team, one person holds every team, and so
the ladder reaches nobody who is not already answerable for the item. Rungs 1
and 2 name the same person the clock already ran out on; rung 3 is the only one
that changes anything, and what it changes is this roster. Roster.escalation_reach
computes that set and the test suite fails if this paragraph and the roster
disagree — in either direction, so the paragraph goes when the reach does. The
ladder is not deleted for being short-handed: it is the procedure the moment
there is a second person to escalate to, and a project that removes its
escalation path while it is inconvenient does not get it back.
Conflict and disagreement
Section titled “Conflict and disagreement”A disagreement about what a text means is settled by the maintainer team that
owns the regime, in the pull request, in writing — the merge commit’s
Reviewed-by trailer then records who accepted that reading. A disagreement
about process, or between teams, goes to @openregs/governance, whose decision
stands until a later pull request changes the rule it applied. Neither is a vote:
someone with standing decides, and the record says who.
Security reports
Section titled “Security reports”Do not open a public issue for a vulnerability in the tooling. Report it through
the channel SECURITY.md names — GitHub’s private vulnerability
reporting, which reaches @openregs/core-maintainers and needs no mail to arrive
anywhere — and expect a first response inside the atom-pr first-response window.
MAINTAINERS.yaml is not a disclosure channel: the address it carries is a
forge no-reply, which identifies an account and accepts no mail, and pointing a
researcher at an address that silently drops their report is worse than offering
none. A wrong answer about the law is not a security report — it is an atom PR,
and it goes through §5 like everything else.
5. What is enforced by a machine, and what is not
Section titled “5. What is enforced by a machine, and what is not”Governance that depends on everybody remembering it is not governance. This is the line:
| Rule | Enforced by |
|---|---|
main advances only by merging a branch | the pre-receive hook, mechanically |
| at least one approving review from a code owner of every changed path | the hook, mechanically |
| an approval covers one commit; new work dismisses it | the hook, mechanically |
| nobody approves their own branch; a bot never approves | the hook, mechanically |
| every required check green at the merged commit, published by the ci identity | the hook, mechanically |
| the merge commit records the reviewers it was made on | the hook, mechanically |
force-pushes and deletions of main refused | the hook, mechanically |
| CODEOWNERS matches the roster | make test, mechanically |
| the roster names nobody at an address reserved so it can never be a person | make gen-codeowners and CI, mechanically |
this table’s durations match MAINTAINERS.yaml | make test, mechanically |
| the SLAs and the ladder above | people, by hand |
| the checks half of these rules on a hosted GitHub repository | a human applying .github/branch-protection.github.json |
| the review half on GitHub | nobody, until a second maintainer joins |
| who a pusher actually is | the transport, not the hook |
every commit a pull request adds carries a Signed-off-by | .github/workflows/dco.yml, mechanically — but on the forge only |
The last five are honest limits, not oversights. Branch protection on a hosted
repository is server-side configuration and no test here may talk to GitHub;
openregs review github-settings renders the payload a human applies, and names
the four rules GitHub has no setting for.
Review is the one rule this project declares and does not ask GitHub to enforce.
pull_request.review_enforcement.posture in .github/branch-protection.yaml is
solo: GitHub refuses to count a self-approval and enforce_admins exempts
nobody, so requiring an approval where one person holds write access would make
main unmergeable by everybody. The rule stands and the hook enforces it; the
posture says only that GitHub is not being asked to, and the file carries the
reason and the condition that ends it. A second maintainer with write access ends
it.
The hook authorizes, it does not authenticate: the pushing identity arrives in
OPENREGS_PUSHER, which a real transport sets. And the sign-off is checked by a
workflow rather than by the hook, so a branch pushed straight at a bare remote is
never asked for one — a trailer is a claim a person makes, and the pull request is
where they make it.
6. Contribution rights, and the licence they carry
Section titled “6. Contribution rights, and the licence they carry”Contributions are taken under a DCO. Every commit a pull request adds carries
a Signed-off-by trailer naming its author — the Developer Certificate of Origin
1.1, which is a statement the contributor makes rather than rights they assign.
The rule, the reasoning and the mechanics are
CONTRIBUTING.md under Sign your work;
.github/workflows/dco.yml checks it on every pull request, over the commits
that pull request adds and no others, so the rule reaches forward and never back.
Why a DCO and not a contributor licence agreement: the reviewers this project depends on are domain experts inside regulated firms, and a CLA asks them for a legal review before their first comment. The cost of that choice is stated rather than buried — a DCO grants no right to relicense a contribution later, so it narrows what the project may do with its own licence, and the narrowing starts with the first outside contribution.
What licence that is, for the serve plane, is not yet decided. Whether the
tree stays Apache-2.0 with the trademark carrying the commercial line, or the
serve plane is relicensed to AGPL before a hosted product reaches general
availability, is open. The analysis — the rehosting risk, what copyleft would
cost the people who self-host, how a partial relicense breaks the
machine-readable code/data split, and a recommendation — is
docs/decisions/d3-license-posture.md.
It is pending sign-off, and nothing in the tree has moved on account of it:
the code is Apache-2.0 (/LICENSE), the corpus CC-BY-4.0
(/regimes/LICENSE-DATA), and MAINTAINERS.yaml already makes both files
@openregs/governance’s to change, because relicensing is a governance act and
not a fallback edit.
A relicense is therefore visible rather than gradual.
tooling/ci/licence_consistency.py reads
every statement of the split this repository makes — the two licence files,
NOTICE, README.md, this section, the packaging metadata, the container image
label, the release-manifest schema, every built release, and what openregs init-regime writes into a new corpus repository — against the one place the
choice is declared, tooling/openregs/governance/licensing.py. It runs in the
test suite, so a half changed in one artifact fails the pull request that changed
it, and half a relicense cannot be merged while looking like a typo fix.
If that decision goes the other way, the DCO goes with it — relicensing optionality needs a CLA — and the migration, including the part that is not automatable, is §7 of the same document.
7. Amending this document
Section titled “7. Amending this document”This document is owned by @openregs/core-maintainers through the fallback rule
and MAINTAINERS.yaml by @openregs/governance. Changing a duration means
changing both — the roster block and the table in §3 — in one pull request,
because the test suite will not let them disagree. That is the point: the promise
and its statement move together or not at all.
Rendered from openregs/openregs@f3a2d10:GOVERNANCE.md