Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
# Contributing to keel

## Governance: rulings vs. machinery

**keel is not a fatwa engine. It is an enforcement engine for a ruling you supply.**

keel never derives a Shariah classification from market data. Every classification is a human
input — recorded with a source and an attributed name (`keel assets attest --source ...
--attested-by ...`) — and keel's job is to enforce what was recorded, deterministically,
rejecting anything unattested. That is what lets one codebase serve operators of different
schools and jurisdictions: the ruling lives in the attestation, not in the code.

For pull requests, that splits contributions into two kinds with different bars:

- **A PR that changes a *default classification*** — what sector a well-known token falls into,
whether a wrapper counts as `spot`, which backings are admissible — is a ruling arriving in
code's clothing. It would apply one contributor's fiqh to every operator who upgrades. Such a
PR must cite a **source** (a scholar, a council, a standard) and is **discussed** before it
merges; a classification with no source behind it is not mergeable, however confident the
author.
- **A PR that changes the *mechanism*** — how attestations are recorded, checked, or audited;
how the screen or the rails run; anything where the ruling stays in the operator's data — is
**ordinary engineering** and needs only ordinary review.

**If you disagree with a classification, do not litigate it here.** The project does not
adjudicate fiqh and will not become a court with a merge button. Attest your own ruling
locally — `keel assets attest` writes to *your* database, with your source and your name on
it — and run the enforcement engine under it. The disagreement then costs nobody anything:
upstream stays neutral, your deployment follows your ruling, and the audit trail records
exactly who said what.

## Licence: why Apache-2.0

keel is licensed under [Apache-2.0](LICENSE). That was a decision, not a default, and the
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ An offline-first, halal (long-only, no-leverage) auto-trading agent for Coinbase
`docs/superpowers/specs/2026-07-15-keel-autotrade-design.md` for the full design and
`docs/superpowers/plans/2026-07-15-keel-phase1-offline-foundation.md` for the Phase 1 build plan.

> **keel is not a fatwa engine. It is an enforcement engine for a ruling you supply.** keel
> never derives a Shariah classification from market data. You record one — with a source and an
> attributed name (`keel assets attest`) — and keel enforces it deterministically, rejecting
> anything unattested. The ruling lives in your attestation, not in the code, so two operators
> following different schools get different answers from the same code, by design. See
> `CONTRIBUTING.md` for what that means for pull requests.

## How keel works

keel runs as a scheduled **agent loop** (`keel agent`). Each cycle, for every allowlisted
Expand Down
105 changes: 105 additions & 0 deletions tests/test_governance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""The governance boundary: keel enforces rulings, it does not issue them.

Contributors to an open-source Shariah-compliance engine arrive from different madhāhib and
jurisdictions, and they will disagree about rulings -- what counts as riba exposure, whether a
staking wrapper is permissible, how a governance token is classified. In a religious context an
ungoverned disagreement is more corrosive than a technical one: left ambiguous, it fragments the
project into forks that each carry a blessing the others do not recognise.

The architecture already answers it. A Shariah classification is a human input (`keel assets
attest`, with `--source` and `--attested-by`); `screen_asset` enforces what was recorded and
never derives a ruling from price data; an absent attestation is a rejection, not a default
pass. The ruling lives in the attestation, not in the code -- so the same code serves a Hanafi
operator and a Shafi'i operator without the project adjudicating between them.

None of that is visible to someone reading the repo, and #280's judgement is that every document
written before it is stated explicitly would have to be rewritten afterwards. These tests pin
the statement where the issue asks for it: the README's first screen (where a stranger decides
what this project is), and `CONTRIBUTING.md` (where a would-be contributor decides what a PR may
change). The sentence is pinned VERBATIM in both places so the two cannot drift into saying
slightly different things -- a boundary that varies by where you read it is not a boundary.
"""

from __future__ import annotations

from pathlib import Path

_ROOT = Path(__file__).resolve().parents[1]

#: The one-sentence boundary, quoted exactly. It is the project's answer to "whose fiqh is
#: this?" and every doc that states it must state it the same way.
_BOUNDARY = "keel is not a fatwa engine. It is an enforcement engine for a ruling you supply."


def _readme() -> str:
return (_ROOT / "README.md").read_text()


def _readme_first_screen() -> str:
"""Everything before the first `##` section: what a reader sees before scrolling."""
return _readme().split("\n## ", 1)[0]


def _contributing() -> str:
return (_ROOT / "CONTRIBUTING.md").read_text()


def test_the_readme_first_screen_states_the_boundary():
"""The sentence must be on the README's first screen, not three sections down.

A stranger deciding whether keel is for them is asking exactly one governance question --
whose ruling does this enforce? -- and an answer below the fold is an answer they never see.
"""
assert _BOUNDARY in _readme_first_screen(), (
"the governance boundary sentence must appear in the README's first screen (before the "
"first `##` section): a reader should not have to scroll to learn whose fiqh keel enforces"
)


def test_contributing_states_the_same_boundary_verbatim():
"""CONTRIBUTING.md carries the identical sentence, framed as PR scope.

Verbatim on purpose: paraphrases drift, and a boundary stated two ways invites the question
of which one governs.
"""
assert _BOUNDARY in _contributing(), (
"CONTRIBUTING.md must carry the boundary sentence verbatim; paraphrasing it leaves the "
"two documents free to drift apart"
)


def test_default_classification_changes_need_a_source_mechanism_changes_do_not():
"""The two kinds of PR must be told apart in CONTRIBUTING.md, with different bars.

A PR that changes a DEFAULT classification is a ruling arriving in code's clothing: it
silently applies someone's fiqh to every operator who upgrades, so it needs a cited source
and discussion. A PR that changes the MECHANISM -- how an attestation is recorded, checked,
or audited -- is ordinary engineering and needs only ordinary review. Contributors who are
not told which is which will file the second as the first, or worse, the first as the
second.
"""
text = _contributing()
assert "classification" in text and "source" in text, (
"CONTRIBUTING.md must say that a PR changing a default classification requires a source"
)
assert "mechanism" in text, (
"CONTRIBUTING.md must say that a PR changing the mechanism is ordinary engineering"
)


def test_disagreement_has_a_route_that_is_not_upstream_adjudication():
"""A stated local route for disagreement, so the project never has to adjudicate fiqh.

The route the architecture already provides: an operator who reads a classification
differently attests THEIR ruling locally (`keel assets attest`, in their own database) rather
than merging it upstream. Without that sentence, the only path a disagreeing contributor
sees is a pull request -- and the project becomes a fiqh court with a merge button.
"""
text = _contributing()
assert "keel assets attest" in text, (
"CONTRIBUTING.md must name the local route (`keel assets attest`) for a disagreeing "
"operator"
)
assert "upstream" in text, (
"CONTRIBUTING.md must say the disagreement is attested locally, not merged upstream"
)
Loading