diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4eb687c4..644c0dab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/README.md b/README.md index ca82ab41..60ec52d0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/launch.md b/docs/launch.md new file mode 100644 index 00000000..cda31b22 --- /dev/null +++ b/docs/launch.md @@ -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. diff --git a/tests/test_launch_gate.py b/tests/test_launch_gate.py new file mode 100644 index 00000000..145d3b9b --- /dev/null +++ b/tests/test_launch_gate.py @@ -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" + )