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
13 changes: 13 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,19 @@ is a good read for how a test argues its own existence.
- Anything that weakens a fails-closed path to make an operational annoyance go away; the
annoyance is the smaller problem.

## What to expect from a solo maintainer

This is a solo-maintained project, and the response times below are what one person can
actually keep — stated here (#291) so a contributor's expectations come from the repo,
not from the 24/7 responsiveness a large project can imply:

- **Issues** are triaged within **3 days** (a label and a first reply; a fix may take
longer, and the triage will say so).
- **PRs** get a first review within **a week** when the gates pass. A slower review is
not a no.
- **Security reports** follow [`SECURITY.md`](SECURITY.md)'s SLA, privately, and take
precedence over everything here.

## Licence: why Apache-2.0

keel is licensed under [Apache-2.0](LICENSE). That was a decision, not a default, and the
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ adapter, deliberately divergent, that the conformance suite runs against.
- [`docs/experiments/`](docs/experiments) — the experiment record, including the honest
result linked above; every document states what was measured, on what engine, with the
defect that forced a restatement.
- [`docs/launch.md`](docs/launch.md) — the pre-launch gate and the announcement plan:
what must be true before anything is announced, where, in what order, and what the
post says (the honest result included).
- [`docs/RELEASING.md`](docs/RELEASING.md) — how a release is cut.

## Asking questions, and contributing
Expand Down
98 changes: 98 additions & 0 deletions docs/launch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# The pre-launch gate and the announcement plan

A project that attracts attention before it can absorb it dies of that attention. This
document is the gate (#291): **nothing is announced until every box below is ticked.**
When a box is ticked it gets its evidence link, and the gate itself ratchets — a box can
be ticked, but the gate cannot be quietly removed from the repo that needs it
(`tests/test_launch_gate.py` pins it). The plan below the gate is what happens *after*
the last box, in the order given, and never before.

## The gate

- [x] **Phase 6 fully closed** — licence detected by GitHub (Apache-2.0), description and
topics set, `SECURITY.md` with private-vulnerability reporting enabled, the positioning
statement ("not a fatwa engine") in the README's first screen. Milestone 9 closed with
all four issues done (#296–#298 and the licence work).
- [x] **Phase 7 fully closed** — the README rewritten for the stranger (#300),
`CONTRIBUTING.md` at the documentation standard (#299), the Contributor Covenant with
the religious-disagreement stance (#302), issue/PR templates (#303), Discussions live
with a *Compliance & classification* category, and nine real `good first issue`s
(#308–#316) plus `help-wanted` on the deeper ones. Milestone 10 closed.
- [x] **The fiqh basis published** — `docs/fiqh-basis.md`: every encoded ruling with its
in-repo source, attested-vs-computed, the open questions, how to disagree (#317/#288).
- [x] **The scholarly-review stance decided and stated honestly** — "No scholarly review
of keel's fiqh basis has occurred," the review path defined, the outreach shortlist in
the document as a plan, not a claim (#318/#289).
- [x] **The Arabic entry point** — `README.ar.md` with the switcher, terminology exact,
scope stated (#319/#290).
- [x] **CI green on `main`** — both matrix legs (3.11, 3.14; the legs came in #301), and
the merge gate (`test` context) has been the required context since #268.
- [x] **The code-quality scans actually configured** — tokenless and always on
(#320): Dependabot over every manifest, a weekly `pip-audit` over the exported lock,
CodeQL on Python. `code-quality.yml` remains the optional Sonar/Snyk tier for if those
tokens are ever created. *Open item to verify after the announcement lull:* the five
`packages/*` Dependabot entries share the root `uv.lock` — confirm they are live in
the Dependabot log; if inert, collapse to the root entry.
- [x] **A maintainer response commitment that is honest for one person, stated in
`CONTRIBUTING.md`** — issues triaged within 3 days, PRs first-reviewed within a week,
security routed to `SECURITY.md`'s SLA.

## The audience, in order

Small, high-trust communities beat a broad launch: one credible post in the right room
outperforms a Show HN, and a Show HN *first* is the failure mode this ordering exists to
prevent. One venue at a time, and answer every reply in the first 48 hours — attention
that goes unanswered dies unanswered.

1. **Islamic fintech practitioner networks** (IFN and similar): the people whose day job
is exactly this problem, who will read the screening axes before the trading code.
2. **`r/islamicfinance`, Muslim developer Discord and Telegram groups**: the audience the
Arabic README exists for; post the honest result in the first paragraph.
3. **Islamic finance programmes — IIUM, INCEIF, Durham**: the same shortlist as the
scholarly-review outreach; a review may start as a conversation a post begins.
4. **Only then: Hacker News / Reddit / Lobsters** — after the smaller rooms have found
the repo, so the first wave of questions comes from people who already understand
what "not a fatwa engine" means.

## What the announcement must say

Lead with the compliance engine, not the trading bot. State the measured result **in the
post itself** — no shipped rule family is net-positive at the taker fee actually paid:
**0 of 90** and **0 of 82** under production-faithful execution
([the experiment record](experiments/2026-08-13-restated-under-a-production-faithful-engine.md)).
Being the one who says it first is the whole credibility play; a post that hides the
result hands it to the first commenter. Say the boundary and the stance plainly too —
**keel is not a fatwa engine**, and **No scholarly review of keel's fiqh basis has
occurred** — and point at what is asked for: the `good first issue`s, the review path,
the Arabic README.

### The draft, ready to adapt

> I built an open-source Shariah-compliance **engine** for spot crypto trading — not a
> trading bot with a halal coat of paint, but the compliance machinery: allowlist
> admission where Shariah classifications are attested with a source and never inferred
> from market data, a fails-closed screen, eighteen un-overridable safety rails including
> §65.4 *qabd* (constructive possession) encoded as an executable check, and an
> audit trail of who attested what.
>
> The honest measured result, stated up front: **no shipped rule family is net-positive
> at the taker fee actually paid — 0 of 90 and 0 of 82 under production-faithful
> execution.** The project's point is the enforcement machinery and the honest
> measurement, not a claim of alpha.
>
> Two things it is not: **keel is not a fatwa engine. It is an enforcement engine for a
> ruling you supply.** So two operators following different schools get different
> answers from the same code, by design. And **no scholarly review of keel's fiqh basis
> has occurred** — the basis is one operator's sourced reading, published as
> docs/fiqh-basis.md precisely so it can be audited and challenged; the review path is
> defined and the review itself is not claimed.
>
> There is an Arabic entry point (README.ar.md), the fiqh basis document with its
> sources, and good-first-issues open. If you want an auditable screening engine and are
> willing to help build one: https://github.com/CodeGateSoftware/keel

## The non-goal

Do not announce to get stars. Announce to find the handful of people who want an
auditable Shariah screening engine and will help build one — a hundred quiet readers who
check the sources beat a thousand who upvote the title.
214 changes: 214 additions & 0 deletions tests/test_launch_gate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
"""The pre-launch gate and the announcement plan — written down, in that order (#291).

#291's premise is that a project which attracts attention before it can absorb it dies of
that attention. The gate exists so "are we ready to announce?" is a checklist with
evidence links, not a feeling; and the plan exists so the announcement, when it comes,
says the honest thing in the right rooms, in the right order. Both live in
docs/launch.md, and this file pins them so the gate can only ratchet: a box may be ticked
with evidence, but the gate itself cannot quietly disappear from the repo that needs it.

Three facts are pinned hardest. The measured result must be stated IN THE POST with its
real numbers -- 0 of 90 and 0 of 82 under production-faithful execution (the number in
#291's own text, '0 of 20', was a misremembering of an earlier 0-of-19 hourly record; the
gate doc corrects it rather than repeating it). The audience order is the issue's order --
small high-trust communities before any broad launch -- because a Show HN first is the
failure mode. And the maintainer-response commitment must be stated where contributors
read it: CONTRIBUTING.md, with honest solo-maintainer numbers, not the 24/7 responsiveness
a big project can imply.
"""

from __future__ import annotations

import re
from pathlib import Path

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

#: The gate document itself.
_DOC = "docs/launch.md"

#: The experiment record behind the real numbers -- the same file test_readme.py pins.
_EXPERIMENT_RECORD = "docs/experiments/2026-08-13-restated-under-a-production-faithful-engine.md"

#: The measured verdict, with the numbers that are actually in the record. An announcement
#: plan that states a softer or rounder figure is a plan to misquote the project's one
#: credibility asset.
_REAL_NUMBERS = ("0 of 90", "0 of 82")

#: The rule the whole document exists to enforce.
_NOTHING_BEFORE_THE_GATE = "nothing is announced until every box below is ticked"

#: The audience, in the order #291 gives: trust-rich small rooms before broad ones. The
#: ordering is the content; pinned as an ordered tuple and asserted by index.
_AUDIENCE_ORDER = (
"Islamic fintech",
"r/islamicfinance",
"IIUM, INCEIF",
"Hacker News",
)


def _unwrapped(text: str) -> str:
"""Join markdown wrapping: drop blockquote markers, then collapse all whitespace."""
return " ".join(re.sub(r"(?m)^\s*>\s?", "", text).split())


def _read(relative: str) -> str:
"""A repo file's text; empty until it exists, so a red run FAILS rather than errors."""
path = _ROOT / relative
return path.read_text() if path.is_file() else ""


def test_the_gate_document_exists_and_states_the_rule():
"""The gate is a document, and its first law is stated in it, verbatim.

A gate that lives in someone's head opens when energy is high and evidence is thin;
the sentence is pinned so weakening it to 'announce when it feels ready' is a diff.
"""
text = _unwrapped(_read(_DOC))
assert text, f"{_DOC} must exist -- the gate is a document, not a feeling"
assert _NOTHING_BEFORE_THE_GATE in text.lower(), (
f"{_DOC} must state the rule verbatim: {_NOTHING_BEFORE_THE_GATE!r}"
)


def test_the_gate_names_each_phase_and_the_scans():
"""Every prerequisite the issue lists, present as gate items with their evidence.

Phase 6 (licence, discoverability, positioning), Phase 7 (contributor readiness),
the fiqh basis and the review-path stance, the Arabic entry point, CI green on main,
the code-quality scans actually configured, and the maintainer-response commitment
stated in CONTRIBUTING -- the checklist is the document's spine.
"""
text = _unwrapped(_read(_DOC))
for pin in (
"Phase 6",
"Phase 7",
"fiqh basis",
"review",
"Arabic",
"CI green",
"scans",
"response",
):
assert pin.lower() in text.lower(), (
f"{_DOC}'s gate must name {pin!r} -- a prerequisite the gate does not list is "
"a prerequisite that can be forgotten"
)


def test_the_announcement_plan_states_the_real_numbers():
"""The post states the measured result itself, with the numbers the record shows.

'0 of 20' (the figure in #291's text) is a misremembering; the record's verdict is
0 of 90 and 0 of 82. The plan is pinned to the real figures and to the record link,
because 'being the one who says it first' only works if what is said first is true.
"""
text = _unwrapped(_read(_DOC))
for number in _REAL_NUMBERS:
assert number in text, (
f"{_DOC} must state the measured result as {number!r} -- the record's real "
"verdict, not a rounder figure that flatters it"
)
# launch.md lives in docs/, so its live link target is experiments/... relative to
# itself. Pinned WITH the markdown link delimiter, so a docs/experiments/... form
# (which would not resolve from docs/launch.md) cannot satisfy it; the file
# existence check below keeps the link honest against renames.
record_link = "](experiments/2026-08-13-restated-under-a-production-faithful-engine.md)"
assert record_link in _read(_DOC), (
f"{_DOC} must link the experiment record as a markdown link with a doc-relative "
"target -- the form that actually resolves from docs/launch.md"
)
assert (_ROOT / _EXPERIMENT_RECORD).is_file(), (
"the experiment record the gate cites no longer exists -- update the link"
)


def test_the_audience_order_is_small_rooms_before_broad_launch():
"""The issue's order, pinned: practitioner communities first, Hacker News last.

One credible post in the right place outperforms a Show HN -- and a Show HN first is
the failure mode the gate exists to prevent. The order is asserted by position, so
reordering the list is a deliberate diff, not an edit.
"""
text = _read(_DOC)
positions = [text.lower().find(audience.lower()) for audience in _AUDIENCE_ORDER]
assert all(p >= 0 for p in positions), (
f"{_DOC}'s audience section must name all of {_AUDIENCE_ORDER} -- the rooms the "
"announcement is actually for"
)
assert positions == sorted(positions), (
f"{_DOC} must order the audience {_AUDIENCE_ORDER} -- trust-rich small rooms "
f"before any broad launch; found positions {positions}"
)


def test_the_plan_states_the_non_goal():
"""Stars are not the goal; the handful of builders is.

#291's explicit non-goal is pinned so the plan cannot drift into launch-theatre: the
point of announcing is to find the people who want an auditable Shariah screening
engine and will help build one.
"""
text = _unwrapped(_read(_DOC)).lower()
assert "stars" in text, (
f"{_DOC} must state the non-goal -- announcing is not for stars"
)


def test_the_announcement_draft_leads_with_the_engine_and_the_honest_result():
"""A ready-to-adapt draft exists, and it says the two things the plan demands.

The assertions are scoped to the DRAFT ITSELF (the blockquote between 'The draft'
and 'The non-goal'), not the whole document -- the same phrases appear in the plan
above, and a draft whose body was deleted would pass a doc-wide check. The draft
must lead with the compliance engine (not 'a trading bot': asserted by position),
carry the measured result, and state the boundary and the no-review stance.
"""
raw = _read(_DOC)
start = raw.find("### The draft")
end = raw.find("## The non-goal")
assert start >= 0 and end > start, (
f"{_DOC} must contain the announcement draft as its own section"
)
draft = _unwrapped(raw[start:end]).lower()
for pin in (
"compliance",
"0 of 90",
"not a fatwa engine",
"no scholarly review",
):
assert pin in draft, (
f"the announcement draft in {_DOC} must say {pin!r} -- the post is where the "
"honest claims live, not only the repo"
)
assert draft.index("engine") < draft.index("bot"), (
"the draft must LEAD with the engine: 'engine' before 'bot', or the post opens "
"as a trading-bot announcement with a compliance footnote"
)


def test_contributing_states_the_solo_maintainer_response_commitment():
"""The gate item with a human cost, stated where contributors read it.

CONTRIBUTING.md must carry an honest-for-one-person commitment: triage and response
numbers a solo maintainer can keep, pointing security reports at SECURITY.md's SLA.
An unstated commitment defaults to the reader's most hopeful assumption, which is the
one thing a solo maintainer cannot meet.
"""
contributing = _unwrapped(_read("CONTRIBUTING.md"))
assert "solo maintainer" in contributing.lower(), (
"CONTRIBUTING.md must name the solo-maintainer reality its response times come from"
)
assert "SECURITY.md" in contributing, (
"CONTRIBUTING.md's response commitment must route security reports to SECURITY.md's "
"SLA rather than restating it (two SLAs drift apart)"
)


def test_the_readme_maps_the_launch_document():
"""The gate is discoverable from the README's documentation map."""
assert "docs/launch.md" in _read("README.md"), (
"README.md's documentation map must link docs/launch.md -- the gate is findable, "
"not filed"
)
Loading