Skip to content

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/CODEOWNERS is 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.

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:

TeamAnswerable for
@openregs/core-maintainersThe fallback: tooling, docs, and anything no narrower rule claims.
@openregs/fixreg-maintainersThe FIXREG regime — its canonical texts, its obligation atoms, its graph.
@openregs/spec-maintainersspec/: the schemas and taxonomies every regime is validated against.
@openregs/governanceWho 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.

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:

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

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.

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.


Terminal window
make gen-codeowners # regenerate .github/CODEOWNERS from MAINTAINERS.yaml
openregs validate maintainers MAINTAINERS.yaml

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


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.
ItemWhat it isOwed byFirst responseResolutionEscalate after
atom-pra machine-proposed obligation atom waiting on a humanthe regime’s maintainer team (@openregs/fixreg-maintainers for FIXREG)2 business days5 business days10 business days
quarantine-itema document a validation gate refused, waiting on triagethe regime’s maintainer team (@openregs/fixreg-maintainers for FIXREG)2 business days10 business days15 business days
spec-changea change to a schema or taxonomy under spec/@openregs/spec-maintainers3 business days10 business days20 business days
roster-changea change to MAINTAINERS.yaml@openregs/governance3 business days10 business days20 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.


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.

AfterEscalate toWhat you do
10 business days@openregs/core-maintainersComment 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/governanceOpen 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/governanceGovernance 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.

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.

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:

RuleEnforced by
main advances only by merging a branchthe pre-receive hook, mechanically
at least one approving review from a code owner of every changed paththe hook, mechanically
an approval covers one commit; new work dismisses itthe hook, mechanically
nobody approves their own branch; a bot never approvesthe hook, mechanically
every required check green at the merged commit, published by the ci identitythe hook, mechanically
the merge commit records the reviewers it was made onthe hook, mechanically
force-pushes and deletions of main refusedthe hook, mechanically
CODEOWNERS matches the rostermake test, mechanically
the roster names nobody at an address reserved so it can never be a personmake gen-codeowners and CI, mechanically
this table’s durations match MAINTAINERS.yamlmake test, mechanically
the SLAs and the ladder abovepeople, by hand
the checks half of these rules on a hosted GitHub repositorya human applying .github/branch-protection.github.json
the review half on GitHubnobody, until a second maintainer joins
who a pusher actually isthe 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.


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