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
194 changes: 194 additions & 0 deletions .claude/shared/engineering-rules.md

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions .claude/shared/money-path-checklist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Recurring-defect checklist — run against any security- or money-path diff

Every item here caught at least one real finding in our own products. Reviewers and `/review-pr` run this item by item; authors run it before opening the PR.

1. **Self-referential verification** — is X checked against a value derived from X? (Seen: a receipt's token address compared to the log's own address.) Anchor checks to config/allowlists, never to the untrusted input.
2. **Defaults on the money path** — any `?? fallback` where the fallback has value? (Seen: unknown token `?? "USDC"`.) Unknown means reject-and-alert, never a default.
3. **Shared keyspaces** — do two limiters/caches/locks build the same key? (Seen: `ip:<addr>` shared by two throttles with different tiers, so alternating requests degraded the stricter limit to the looser one.) Namespace per purpose.
4. **One-shot checks** — does a safety check latch on first failure and never retry? (Seen: a decimals check latched false on a transient RPC error.) Distinguish "refuted" from "unreachable"; only refuted may latch.
5. **Unbounded wedge + alert flood** — can one poisoned item pin a cursor or queue forever, and does its alert re-fire every tick? Bound retries, dedup alerts.
6. **Silent-stall siblings** — for every loud failure path, is there a quiet branch that resets the failure counter? (Seen: a pending-log branch stalled with no alert while the tick "succeeded".)
7. **Upgrade path on existing state** — migrations against a populated DB, deploys against a root-owned volume, compose changes on a box mid-layout-change. Fresh-state testing proves nothing about the box you have.
8. **Cap semantics** — does the limit bind the actual resource or a client claim? (Seen: `Content-Length` checked while chunked bodies were still fully buffered.) Name which layer holds the real bound.
9. **Docs/code drift** — do README, bootstrap, examples, and error strings still describe the world this diff creates? Stale docs asserting a property that doesn't hold are worse than silence — they stop anyone checking.
10. **Test-double honesty** — does every fake honour the arguments it receives, and can every "never happened" assertion actually fail? (Seen: a filter-ignoring fake; a counter assertion on a path where the counter can't increment.)
11. **Payout parity** — anything that ranks or pays: compared byte-for-byte against the other side (contract `fee = price * bps / 10000`, the live on-chain rate, the other route), including the *window* handed to an identical comparator — two identical comparators can still disagree when their block windows differ.
12. **Fee omission in every path** — if a fee/discount is subtracted in one code path, is it subtracted in all paths that serve users? (Seen: the fallback path fixed while the production path kept the identical defect.)
13. **Error-body leakage** — what does each 4xx/5xx reveal? Exact scores are gradient oracles; flags are detection oracles. Fail-open or fail-closed decided per check, written down, and matched by the code.
14. **Read bounds on write paths** — every read is bounded at the query (`take(limit)`), never collect-then-slice; no fail-open catch around an unbounded read.
15. **Silent config fallbacks** — no `?? default` for critical config; unset/unrecognised is an error. Env compared exactly (`printf`, not `echo`); build-inlined vars need a rebuild; secrets checked for presence, never printed.
16. **Guarantee-on-every-path** — for each prose guarantee ("never reveals X", "payment flow untouched"), point at the test asserting it on the *rejected/failure* path.
17. **Threshold bands and boundaries** — the band between two thresholds, `>=` vs `>` at exact values, Unicode/empty input, concurrent duplicate requests.
18. **Has it run in anger?** — for a path that moves real money / sends real messages / writes to a third party: was it exercised in production via **tester mode** (restricted audience, identical pipeline, small stake, verified at the source of truth) before public launch? If the app has no tester mode yet, that's the first ticket — see `tester-mode-pattern.md`. And `grep` the audience flag: it must not appear downstream of access control.
71 changes: 71 additions & 0 deletions .claude/shared/tester-mode-pattern.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Tester mode — run real paths in production with a restricted audience

Synced from `pm-kit`. Generic pattern; the worked example is mini-quiz (see end).

## Why it exists

Some paths cannot be meaningfully faked: anything that moves money, spends gas, sends a real message, or writes to a third-party system. A staging environment proves the code typechecks against a mock; it does not prove that real gas gets spent from the real treasury and the real receipt comes back. Tester mode lets you run the *identical* production pipeline with real (small) stakes, visible only to a handful of internal accounts.

It has already paid for itself: our first native-CELO payout ran this way and surfaced three bugs that no offline test could have found — all three lived in the gap between "the code typechecks" and "real gas was spent from the real treasury".

## The model: restrict the audience, never the behaviour

Two flags, both **database columns** (not config, not env — toggling must never need a deploy):

| Flag | Lives on | Meaning |
|---|---|---|
| capability flag, e.g. `isTester` | the **account** | this account may see and act on tester-only entities |
| audience flag, e.g. `testerOnly` | the **entity** (quiz, campaign, map, product, drop…) | visible only to tester accounts |

Toggle both from the admin surface. Index the audience flag together with whatever the public listing filters on, so the public query stays cheap.

## Enforcement: server-side, at every surface

The frontend never decides. Every check runs in the API, and **each surface is gated independently** — one leaked room code or one missed UI condition must expose nothing. The honest work of porting this pattern is *enumerating the surfaces*; the checks themselves are one-liners. The standard set:

| Surface | Behaviour for non-testers |
|---|---|
| Listing / feed / search | filtered out **in the query**, not post-filtered |
| Direct lookup by id/code/slug | **404, not 403** — existence is part of what's hidden; probing confirms nothing |
| Join / mutate / submit | rejected with a specific error code |
| In-flight reads (state, results, progress) | **re-checked on every call** — no riding along mid-flow with a leaked code |
| Realtime (SSE / websocket / push) | checked at subscribe time |
| Aggregates (leaderboards, stats, analytics, exports) | **decide explicitly per aggregate** — see the gap below |
| Notifications / emails / social posts | tester entities never trigger public-facing sends |
| Deep links / OG previews / sitemaps | 404 / excluded |

Identity comes from the same auth the endpoints already use — there is no separate "tester API".

## The part that makes it valuable: downstream is flag-blind

Scheduler, scoring, payout, settlement, treasury, messaging workers contain **zero** references to the audience flag. A tester run is not a simulation; it is the identical pipeline with real prizes, real gas, real transfers. The only restricted dimension is *who can see it*.

Keep it this way. A sandbox branch inside the payment code means the one path you most need to prove — real money moving — is exactly the path a test never runs. **Do not add tester awareness downstream of access control.** (Reviewers: grep for the flag; if it appears in a worker or a payment module, that's a finding.)

## Running a production test

1. Mark participating accounts as testers.
2. Create the entity with a **small** stake, tick tester-only, schedule/publish it.
3. Preflight (funding, config) runs at creation/scheduling time, not at payout time — an underfunded run is refused before it exists, so you never drain a treasury to test the failure path.
4. **Verify results at the source of truth** — on-chain balance delta = prizes + gas, the third-party's own ledger, the recorded webhook body — not from your UI. The UI is one of the things under test.
5. Write up what the run proved and what it surfaced, with tx hashes, in the issue that tracked the launch. Numbers from the run become pinned tests.

## Known gap to decide up front: aggregates

Every app has some surface that sums over activity — a leaderboard, "total volume", analytics, a public counter. Tester activity flows into it unless excluded. Small volumes make it tolerable *today*, but it is an unexamined edge, not a decision. Decide per aggregate whether tester rows are included, record it in the repo's decisions doc, and pin the choice in a test. It is easier to exclude tester rows on day one than to explain a test artifact on a public board later. If aggregate standing ever gates prizes or promotion, exclusion becomes mandatory.

## Porting checklist (use in the PR that introduces it)

- [ ] Two DB flags (account capability, entity audience), admin-togglable, no deploy
- [ ] Every surface in the table above enumerated for this app and gated server-side; listing filters in the query
- [ ] 404 for hidden entities on direct lookup
- [ ] Re-check on in-flight reads and realtime subscribe
- [ ] `grep -r <audienceFlag>` shows nothing downstream of access control
- [ ] Aggregate decision recorded and pinned in a test
- [ ] Preflight at creation time; smallest viable stake documented
- [ ] Verification recipe written down: where the source of truth is and what delta to expect
- [ ] Doc's update triggers listed (new public surface, flag-semantics change, aggregate decision)

## Worked example: mini-quiz

`User.isTester` / `Quiz.testerOnly` in `apps/api/prisma/schema.prisma`; composite index `[kind, testerOnly, status, scheduledStart]`. Enforced in `routes/quizzes.public.ts` (listing filter, by-code → 404), `routes/rooms.ts` (404 not 403), `services/room.service.ts` (`joinRoom` → `TESTER_ONLY`; lobby/submit/results re-checked per call), `routes/room-events.ts` (SSE subscribe). Viewer identity via `services/tester-access.service.ts`. Downstream (scheduler, scoring, payout worker, treasury) flag-blind. Known gap: `services/leaderboard.service.ts` and admin analytics aggregate tester XP/answers — acceptable while testers are internal and volumes tiny; decide per-aggregate if standing ever gates prizes. Update triggers: new public quiz surface (route, SSE channel, aggregate); change to flag semantics; the aggregate decision.
79 changes: 79 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Bug report
description: Something is broken or behaves unexpectedly
title: "bug: "
labels: ["bug", "status: triage"]
body:
- type: textarea
id: what-happened
attributes:
label: What happened? (exact commands/steps + REAL output)
description: '"Confirmed" means you ran it. Distinguish "I ran this and here is the output" from "static reading suggests". Include what you expected instead.'
placeholder: "When I ... , the app ... . I expected ... ."
validations:
required: true
- type: textarea
id: repro
attributes:
label: Steps to reproduce
description: Numbered steps. An agent or teammate should be able to follow them exactly.
placeholder: |
1. Go to ...
2. Click ...
3. See error
validations:
required: true
- type: textarea
id: rootcause
attributes:
label: Root cause (file:line), if known
description: Makes the fix unambiguous and the review checkable. "Static reading suggests" is fine — say which it is.
validations:
required: false
- type: input
id: version
attributes:
label: Version / commit tested
placeholder: "v2.4.13 / a1b2c3d, MiniPay Android"
validations:
required: false
- type: textarea
id: where
attributes:
label: "Where is the defect (not just where you noticed it)?"
description: Name the code path that serves users. If you found it in a fallback/dev-only path, check whether the production path has the same defect and say so.
placeholder: "Found in the log-scan route, but the subgraph path (production) has the identical fee omission."
validations:
required: false
- type: textarea
id: measurement
attributes:
label: "How would we know it's fixed?"
description: "What metric, log, or check would show the fix worked — without asking the reporter to retest. If the answer is 'we can't measure it', say so; instrumenting may be the first task."
validations:
required: false
- type: input
id: environment
attributes:
label: Environment
description: Browser/device, app version or URL, wallet if relevant (e.g. MiniPay).
placeholder: "MiniPay on Android 14, mondeto.app production"
validations:
required: false
- type: dropdown
id: severity
attributes:
label: Priority (one per issue — split if two items would be scheduled differently)
options:
- "critical — money correctness, security, or user-visible wrong data"
- "high — major feature broken, workaround exists"
- "medium"
- "low — minor / cosmetic"
validations:
required: true
- type: textarea
id: evidence
attributes:
label: Logs / screenshots
description: Console output, error messages, screenshots, tx hashes.
validations:
required: false
5 changes: 5 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Question / discussion
url: https://github.com/celo-org/mondeto/discussions
about: For open questions that aren't a bug or a planned piece of work.
28 changes: 28 additions & 0 deletions .github/ISSUE_TEMPLATE/task.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Task / chore
description: Technical or process work that isn't a user story or a bug (refactor, deps, infra, docs)
title: "task: "
labels: ["chore", "status: triage"]
body:
- type: textarea
id: what
attributes:
label: What needs to be done?
description: Concrete outcome, not activity. "CI runs tests on every PR" not "look into CI".
validations:
required: true
- type: textarea
id: done
attributes:
label: Definition of done
placeholder: |
- [ ] ...
- [ ] ...
validations:
required: true
- type: dropdown
id: priority
attributes:
label: Priority
options: ["critical — money correctness, security, or user-visible wrong data", "high", "medium", "low"]
validations:
required: true
60 changes: 60 additions & 0 deletions .github/ISSUE_TEMPLATE/user_story.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: User story / feature
description: A piece of user-facing work, written so a human or agent can implement it without follow-up questions
title: "story: "
labels: ["enhancement", "status: triage"]
body:
- type: textarea
id: story
attributes:
label: User story
description: Who wants it, what they want, and why.
placeholder: "As a <user type>, I want <capability>, so that <outcome>."
validations:
required: true
- type: textarea
id: context
attributes:
label: Context & evidence
description: Why now? Link research, feedback, metrics, or the parent epic.
validations:
required: false
- type: textarea
id: acceptance
attributes:
label: Acceptance criteria
description: Verifiable checklist — these become the tests AND define "done" (a ticket closes when these are met, not when a related PR merges). Keep code work and ops work (rotate/migrate/purge) as separate boxes so a PR can close its half.
placeholder: |
- [ ] Given ..., when ..., then ...
- [ ] Given ..., when ..., then ...
- [ ] Errors are handled: ...
validations:
required: true
- type: textarea
id: out-of-scope
attributes:
label: Non-goals
description: What this deliberately does NOT include, and who owns the adjacent work. ("User-facing copy belongs to #12.") Also say if this is explicitly NOT a blocker.
validations:
required: true
- type: textarea
id: dependencies
attributes:
label: Depends on / blocks
description: Ordering that must survive without anyone remembering it.
placeholder: "Depends on #12. Blocks #34."
validations:
required: false
- type: dropdown
id: priority
attributes:
label: Priority
options: ["critical — money correctness, security, or user-visible wrong data", "high", "medium", "low"]
validations:
required: true
- type: textarea
id: notes
attributes:
label: Design / technical notes
description: Links to designs, affected files or modules, API changes, migration needs.
validations:
required: false
Loading