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
59 changes: 59 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Bug report
description: Something behaves wrong or crashes — with the build, mode, and rail/rule named.
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time. Three things make a report actionable here, and the form
asks for all three: **which build** (`keel --version` stamps the commit and build
kind), **which account** (paper and live share nothing — a figure from one says
nothing about the other), and **which rail or rule** was involved.

**Security first:** if this bug could make keel trade what it should not, or expose
credentials, it is a security issue — do not file it here. See
[SECURITY.md](../blob/main/SECURITY.md) for the private channel.
- type: input
id: version
attributes:
label: "Output of `keel --version`"
description: "Paste it verbatim — it names the version, commit, and build kind."
placeholder: "keel 0.7.1+a4912bb [checkout]"
validations:
required: true
- type: dropdown
id: mode
attributes:
label: Which account?
options:
- paper (synthetic cash, nothing real moves)
- live (real funds)
- backtest / simulate
- not mode-related
validations:
required: true
- type: input
id: component
attributes:
label: Rail or rule involved
description: >
A rail number or name from keel/execution/guards.py (e.g. "rail 14 subscription
allowance", "kill-switch"), a rule family (turtle_breakout, rsi_meanrev, ...), or
"n/a" if neither.
placeholder: "e.g. rail 6 concentration cap / rsi_meanrev / n/a"
validations:
required: true
- type: textarea
id: what_happened
attributes:
label: What happened?
description: What you ran, what you expected, what happened instead. Commands and log lines beat prose.
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: How to reproduce
description: The exact command(s), config knobs that differ from default, and whether it needs market data or a key.
validations:
required: false
54 changes: 54 additions & 0 deletions .github/ISSUE_TEMPLATE/compliance_question.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Compliance & classification question
description: "Should X be classified / treated this way? A question about rulings — not a bug, not a feature."
labels: ["fiqh", "question"]
body:
- type: markdown
attributes:
value: |
**This is the distinct route for classification questions** (#285): "should this
asset/mechanism be treated differently" is neither broken behaviour nor a missing
feature, and triaging it as either gets it the wrong resolution.

Two facts about how this project treats rulings:

- **keel is not a fatwa engine.** It enforces the ruling you supply; it does not
issue one. A question here explores how a ruling should be *recorded and
enforced*, and what others attested — it does not make the project adjudicate.
- **Your deployment, your ruling.** If you conclude an asset should be classified
differently for you, the action is a local attestation — `keel assets attest
--source ... --attested-by ...` — not necessarily a code change here.
- type: input
id: asset_or_mechanism
attributes:
label: Asset or mechanism
description: The token, wrapper, venue feature, or mechanism the question is about.
validations:
required: true
- type: textarea
id: question
attributes:
label: The question
description: What treatment you are unsure about, and what you currently attest / run.
validations:
required: true
- type: textarea
id: source
attributes:
label: Sources you are weighing
description: >
Scholars, councils, standards, or venue documentation you have found. A question
with no source behind it can still be asked — but a proposed CHANGE to a default
classification cannot be merged without one (CONTRIBUTING.md, governance section).
validations:
required: false
- type: dropdown
id: outcome
attributes:
label: What would resolve it for you?
options:
- A discussion of how different operators attest this
- A change to how the mechanism records/enforces rulings
- A change to a default classification (needs sources, discussed before any PR)
- Not sure — that is part of the question
validations:
required: true
14 changes: 14 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
blank_issues_enabled: false
contact_links:
- name: Security report (private)
url: ../security/advisories/new
about: >
Anything that could make keel trade what it should not, or expose credentials —
including a rail that can be bypassed. Report it privately via SECURITY.md; do not
open a public issue.
- name: Questions & ideas (Discussions)
url: https://github.com/CodeGateSoftware/keel/discussions
about: Ask how something works, float an idea, or join the Compliance & classification category.
- name: Contributing
url: https://github.com/CodeGateSoftware/keel/blob/main/CONTRIBUTING.md
about: The documentation standard, the gates a PR must pass, and scope guidance — read before your first PR.
41 changes: 41 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Feature request
description: Propose a change to the mechanism — new capability, new venue adapter, better rails.
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
A note on scope before you write: changes to the **mechanism** (how things are
recorded, checked, audited, executed) are ordinary engineering and start here.
Changes to a **default classification** are rulings in code's clothing — they need a
cited source and discussion first (see
[CONTRIBUTING.md](../blob/main/CONTRIBUTING.md), "Governance: rulings vs. machinery")
and belong in the *Compliance & classification* Discussions category.
- type: textarea
id: problem
attributes:
label: The problem
description: What cannot be done today, or what costs more than it should. Concrete over hypothetical.
validations:
required: true
- type: textarea
id: proposal
attributes:
label: The proposal
description: What should exist instead, and where it would live (a rail, a command, a package, the port).
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: What you ruled out and why — this project records rejected alternatives on purpose.
validations:
required: false
- type: checkboxes
id: scope
attributes:
label: Scope check
options:
- label: This changes the mechanism, not a default classification (or it changes neither)
required: true
32 changes: 32 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!--
The bar is stated in CONTRIBUTING.md so it can be met, not guessed:
the documentation standard (say why, name what was measured, record
what would change the decision), tests first, and Conventional Commits.
-->

## What & why

<!-- What changes, and the constraint or measured fact that makes it necessary.
A PR whose "why" is only "what" will be asked for its reasoning. -->

## Tests-first evidence

<!-- The failing test(s) this PR makes pass, and evidence they failed for the RIGHT
reason — the assertion meant to assert, not an import error. Paste the red run. -->

- [ ] Tests written first, seen failing for the right reason

## Gates (all must pass)

- [ ] `uv run ruff check` clean
- [ ] `uv run mypy` clean
- [ ] `uv run pytest -q` green

## Scope check

- [ ] **This PR touches a rail or a default classification** — checked means it DOES;
leave checked only if true, and if so: cite the source and open the discussion
BEFORE review (CONTRIBUTING.md, "Governance: rulings vs. machinery").
- [ ] New dependency added (needs discussion first)

<!-- Delete sections that do not apply to your change rather than leaving them empty. -->
118 changes: 118 additions & 0 deletions tests/test_issue_templates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""Issue and PR templates: they gather what triage needs, and teach the standard on submit.

Templates do two jobs here (#285): they extract the information a report cannot be acted on
without (`keel --version`'s stamped build identity, paper-vs-live, WHICH rail or rule), and
they state the project's expectations at the exact moment someone is about to submit -- that
a security report goes to SECURITY.md not a public issue, that a classification question is
a discussion not a bug, that a PR carries tests-first evidence and names it when it touches
a rail or a default classification.

The failure modes are quiet ones, so they get tests:

- **A missing field** means triage round-trips for it, every time, forever.
- **A compliance question filed as a bug** gets triaged as one -- the issue that asked for
this template says plainly that this route must be distinct, because a ruling question is
neither broken behaviour nor a feature.
- **A security report landing in public issues** discloses to everyone simultaneously; the
whole point of #279 was a private alternative, and config.yml is what routes people to it.
- **A PR template without the rail/classification checkbox** lets the one PR class that
needs different review look like every other PR.
"""

from __future__ import annotations

from pathlib import Path

import yaml

_ROOT = Path(__file__).resolve().parents[1]
_TEMPLATES = _ROOT / ".github" / "ISSUE_TEMPLATE"


def _form(name: str) -> dict:
path = _TEMPLATES / name
assert path.is_file(), f".github/ISSUE_TEMPLATE/{name} does not exist (#285)"
parsed = yaml.safe_load(path.read_text())
assert isinstance(parsed, dict) and "body" in parsed, (
f"{name} is not a valid GitHub issue form (needs top-level keys incl. `body`)"
)
return parsed


def _field_ids(form: dict) -> set[str]:
"""Every `id:` across the form's attributes and markdown-free input blocks."""
return {b["id"] for b in form["body"] if isinstance(b, dict) and b.get("id")}


def test_bug_report_collects_build_identity_mode_and_the_rail_or_rule():
"""The three facts every bug report here is useless without.

`keel --version` stamps commit and build kind (release/checkout/dirty), because the
same symptom on two builds is two bugs. Paper-vs-live, because they share nothing.
The rail or rule involved, because that is the address of the behaviour.
"""
ids = _field_ids(_form("bug_report.yml"))
for needed in ("version", "mode", "component"):
assert needed in ids, (
f"bug_report.yml is missing the {needed!r} field -- triage will round-trip for it"
)


def test_the_compliance_question_route_is_distinct_from_bugs_and_features():
"""A classification question must arrive as itself, not wearing a bug costume.

"Should this asset be classified differently" is neither broken behaviour nor a missing
feature; triaged as either, it gets the wrong reviewer and the wrong resolution. The
template exists, is separate from bug_report/feature_request, and says where the
substantive conversation belongs (a source, and the local-attestation route).
"""
form = _form("compliance_question.yml")
text = str(form).lower()
assert "attestation" in text or "attest" in text, (
"compliance_question.yml must route the asker toward attestation, not a code change"
)
assert "source" in text, (
"compliance_question.yml must ask for a source -- a classification claim without one "
"is exactly what CONTRIBUTING.md's governance section refuses"
)


def test_config_routes_security_reports_to_security_md_not_public_issues():
"""The blank-issue chooser must point vulnerability reports at the private channel.

SECURITY.md describes the channel; config.yml is the signpost someone actually meets
first when the picker opens.
"""
path = _TEMPLATES / "config.yml"
assert path.is_file(), ".github/ISSUE_TEMPLATE/config.yml does not exist"
parsed = yaml.safe_load(path.read_text())
text = str(parsed)
assert "SECURITY.md" in text, (
"config.yml must route security reports to SECURITY.md, not to a public issue"
)


def test_the_pr_template_carries_gates_tests_first_and_the_guarded_kinds():
"""Gates passed, tests-first evidence, and the rail/classification disclosure.

The checkbox is the load-bearing part: a PR that touches a rail or a default
classification needs source-and-discussion review (CONTRIBUTING.md's governance
section), and nothing makes that visible at review time unless the author declared it
at submit time.
"""
path = _ROOT / ".github" / "PULL_REQUEST_TEMPLATE.md"
assert path.is_file(), ".github/PULL_REQUEST_TEMPLATE.md does not exist"
text = path.read_text().lower()
for gate in ("ruff", "mypy", "pytest"):
assert gate in text, f"the PR template must list the {gate} gate"
assert "test" in text and ("first" in text or "failing" in text), (
"the PR template must ask for tests-first evidence (a failing test seen failing)"
)
assert "rail" in text and "classification" in text, (
"the PR template must carry the checkbox for touching a rail or a default classification"
)


def test_feature_request_template_exists_as_a_form():
"""A basic sanity check: the form exists and parses as one."""
_form("feature_request.yml")
Loading