From e11ad60857285433a0ea58556fa024275750f39c Mon Sep 17 00:00:00 2001 From: George Rodafinos Date: Fri, 24 Oct 2025 13:46:01 -0700 Subject: [PATCH 1/7] ci: add lenient markdownlint config for KB files --- .markdownlint.jsonc | Bin 0 -> 426 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .markdownlint.jsonc diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc new file mode 100644 index 0000000000000000000000000000000000000000..5c926fd9fe2ded80e9624006c5688e0ff12d9170 GIT binary patch literal 426 zcmaJ-%L>9U5S+8%KS=RtTeUs)CV2H5#M+nIhcvAs(qC66=?WH9LPB!MF>yi7nwksw5mrQ$ zVY#MDjT5sKE0)N~(CQx1e|O;Mx5Adr2`i+q2Py|g>1bibc_O+?=%;1BMy71&y=7kG y8@ioi0Z%Z^%~|0S_5Pwh)&2IqX_xQ?)jL;o9LnS=qpjIZj+=1a&N_4$V}uvo4@ubo literal 0 HcmV?d00001 From 78eae6962841bb35da1e0a63dc601a99a7b8cf14 Mon Sep 17 00:00:00 2001 From: George Rodafinos Date: Fri, 24 Oct 2025 13:47:09 -0700 Subject: [PATCH 2/7] docs: remove files superseded by .claude/knowledge/ KB --- docs/folder_structure.md | 54 ------- docs/modules/bridge.md | 298 ------------------------------------- docs/modules/governance.md | 55 ------- docs/project_structure.md | 43 ------ 4 files changed, 450 deletions(-) delete mode 100644 docs/folder_structure.md delete mode 100644 docs/modules/bridge.md delete mode 100644 docs/modules/governance.md delete mode 100644 docs/project_structure.md diff --git a/docs/folder_structure.md b/docs/folder_structure.md deleted file mode 100644 index 2d591fae..00000000 --- a/docs/folder_structure.md +++ /dev/null @@ -1,54 +0,0 @@ -# Folder Structure (VPS Runtime) - -## VPS Server (runtime) -``` -/home/platform/htdocs/platform.local/frontend - ├── docker-compose.yml (builds from Dockerfile; exposes 127.0.0.1:3010) - ├── Dockerfile (Next.js 14 multi-stage build) - └── src/ - ├── pages/ - │ ├── _app.tsx - │ └── signup.tsx - └── styles/ - └── globals.css -``` - -## Runtime files -- `/root/ddp.env` (Compose env_file — DB credentials, secrets) -- `/var/www/html/` (public Nginx docroot, not used by Next container) - -## Recommended additions on VPS -- `scripts/redeploy-frontend.sh` (one-command rebuild/verify; executable) -- `tailwind.config.js` -- `postcss.config.js` -- `README.md` (quickstart + CSS playbook) - -## Permissions -- Keep `/root/ddp.env` readable by root only (0600). -- App source owned by deploy user; docker group for Compose runner. - -## Local Developer Environment (Windows) -Local repo root: `G:\Coopeverything\TogetherOS\ddp-on-vps` - -SSH keys: -- Private key: `G:\Coopeverything\TogetherOS\ssh_keys\id_ed25519` -- Public key: `G:\Coopeverything\TogetherOS\ssh_keys\id_ed25519.pub` - -Ensure public key present in: -- `/root/.ssh/authorized_keys` -- `/home/platform/.ssh/authorized_keys` - -System OpenSSH tools (Windows): -- `C:\Windows\System32\OpenSSH\ssh-keygen.exe` -- `C:\Windows\System32\OpenSSH\ssh-keyscan.exe` - -Other local dev assets: -- `G:\Coopeverything\TogetherOS\open-webui\` (local UI experiments; not part of deploy) - -## GitHub Actions Secrets (Deploy Staging) -- SSH_PRIVATE_KEY -- VPS_HOST = continentjump -- VPS_IP = -- VPS_USER = platform -- VPS_PATH = /home/platform/htdocs/platform.local/frontend - diff --git a/docs/modules/bridge.md b/docs/modules/bridge.md deleted file mode 100644 index 3cf89baf..00000000 --- a/docs/modules/bridge.md +++ /dev/null @@ -1,298 +0,0 @@ -# Bridge — AI Assistant Platform - -> An always‑available assistant that helps people understand, deliberate, and act together. Bridge answers questions from our knowledge, tidies threads, and assists moderation with transparent, auditable suggestions — to change how humans decide and behave with one another and the planet. - -**Owner(s):** @coopeverything-core -**Labels:** module:bridge, type:increment, size:XS|S, slice:qna|tidy|ops, target:Now|Next|Later -**Status:** Progress 0% -**Next milestone:** Pilot Bridge-assisted landing page (minimal `/bridge` with streaming Q&A, rate-limit, and NDJSON logs) -**Blockers/Notes:** None - ---- - -> ### Parts (subpages) -> This page is the canonical overview. Detailed work happens in these focused subpages: -> - **docs/modules/bridge/landing-pilot.md** — minimal public `/bridge` page, streaming Q&A, logs & validator **(ready)**. -> - **docs/modules/bridge/faq-seed.md** — curated questions & answers from pilot testers **(coming soon)**. -> - **docs/modules/bridge/api.md** — ask/tidy API contracts, error taxonomy, examples **(coming soon)**. -> - **docs/modules/bridge/ethics-charter.md** — tone, privacy, assist-not-adjudicate guardrails **(coming soon)**. - ---- - -## 1) Why Bridge - -Bridge is a **cooperation amplifier**. It lowers friction to collective intelligence: people can grasp the facts, the trade‑offs, and the next steps without wading through miles of text or adversarial back‑and‑forth. Bridge teaches and reinforces the practices that make cooperation real. - -* **Lower friction for understanding**: converts long docs/threads into concise, cited summaries so members *understand before reacting*. -* **Nudge empathy and deliberation**: encourages steel‑manning, calm language, and trade‑off thinking; suggests reframes when a discussion heats up. -* **Preserve shared memory**: keeps auditable logs of questions, sources, and summaries; makes learning cumulative across the network. -* **Empower local ↔ global**: connects local questions to global knowledge while respecting privacy and consent. -* **Build trust in AI**: every suggestion includes sources, a confidence disclaimer, and appears as assistance — never as adjudication. - -**North‑star outcomes** - -* Faster, calmer decisions with documented trade‑offs. -* More first‑time contributors completing a helpful action in their first session. -* Fewer circular debates; clearer next steps in threads and proposals. - ---- - -## 2) Principles & Guardrails (the Social Contract in code) - -* **Assist, not adjudicate.** Bridge suggests; humans decide. -* **Cite & disclaim.** Every answer shows sources and includes: *“Bridge may be imperfect; verify important details.”* -* **Privacy first.** Index only public repo docs + approved KB exports; redact PII (emails/phones/handles) in outputs and logs. -* **Auditability.** Append‑only logs with IDs, timestamps, content hashes; validation scripts prove integrity. -* **Small, reversible steps.** Deliver tiny increments with clear acceptance and roll‑back paths. - ---- - -## 3) Scope (What Bridge does) - -**Member Q&A / Brainstorm (grounded)** -Answers questions using TogetherOS documentation and approved knowledge exports. Provides citations and simple, respectful prompting for brainstorming. - -**Thread tidy (summaries, tags, actions)** -Summarizes forum topics into a standard structure (problem → options → trade‑offs → open questions → next steps), proposes tags, and extracts candidate actions with links. - -**Moderation assist (suggestions, not decisions)** -Detects heated tone or derailments and suggests de‑escalations, label proposals, or merge/split hints. Includes an appeal link and logs the suggestion with reasons. - -**Onboarding nudge** -“Ask Bridge” is present on first run; suggests two tiny next actions and a person/project to follow, reducing time‑to‑first‑contribution. - -Out of scope (for now): punitive moderation, decision‑making authority, automated enforcement. - ---- - -## 4) Success Metrics (SLOs tied to human behavior) - -* **Time‑to‑first‑useful‑answer (p95)** in fixture mode < 800ms -* **Citation coverage** = 100% for non‑empty answers/summaries -* **Deliberation quality**: % of threads with a tidy card and extracted actions -* **Trust index**: ≥ 70% “helpful” ratings after 30 days -* **Appeals**: median resolution within 7 days; 5xx error budget tracked - ---- - -## 5) Phased Plan (each phase = small, verifiable) - -### Phase 0 — Foundations (Now) - -**Goal:** Ground Bridge in our knowledge and ethics. - -* Curate **Bridge Knowledge Dataset** → `packages/bridge-fixtures/docs.jsonl` (Manifesto, OPS/CI, STATUS, Modules, Social Contract). -* Write the **Bridge Ethics Charter** (tone, fairness, transparency, non‑punitive suggestions). -* Build **dataset curation script** (dedupe, trim, redact PII); add to CI with proof lines. -* Define **citation format** `{ path, lines[] }` + standard disclaimer text. - -**Acceptance:** JSONL exists and passes `scripts/validate.sh`; random samples map to real docs. - ---- - -### Phase 1 — MVP: Q&A + Tidy + Logs (Now) - -**Goal:** Answer a docs question and summarize a thread with sources and logs. - -**API (fixture‑first)** - -* `POST /api/bridge/qa` → `{ "answer": "", "sources": [{"path":"docs/.md","lines":[x,y]}], "disclaimer":"…" }` -* `POST /api/bridge/tidy` → `{ "summary":"", "tags":["type:increment","size:S"], "links":[""], "sources":[…], "disclaimer":"…" }` - -**UI** - -* Persistent **Ask Bridge** input (page‑aware). -* **Tidy with Bridge** button on forum topics → non‑blocking summary card with Copy/Hide/Show + source chips. - -**Logs (append‑only)** - -* `logs/bridge/actions-YYYY-MM-DD.ndjson` entries: `{ id, ts, action: "qa|tidy", inputs, sources, content_hash }` -* `scripts/validate.sh` checks: file exists, last non‑empty line parses as JSON; prints `LINT=OK`, `VALIDATORS=GREEN`, `SMOKE=OK`. - -**Acceptance** - -* Non‑empty outputs include ≥1 valid source from `docs/**` or `STATUS/**`. -* Storybook story renders tidy card; empty/loading/error states covered. - ---- - -### Phase 2 — Deliberation Structure & Empathy (Next) - -**Goal:** Encourage better conversations. - -* Standard **summary structure** enforced in templates (problem → options → trade‑offs → open questions → next steps). -* **Tone cues** (light heuristics): suggest neutral reframes; never punitive. -* **Action extraction**: propose 1–3 next steps + tags (human‑editable). - -**Acceptance:** ≥10 sample threads produce structured summaries; facilitators rate ≥70% “useful”. - ---- - -### Phase 3 — Moderation Assist (Suggestions only) (Later) - -**Goal:** Transparent moderation support with appeal paths. - -* Detect toxicity/derail and **suggest** labels/merges/splits with short rationales and links. -* **Appeal link** on each suggestion; corrections form a learning queue (governed). - -**Acceptance:** Suggestions include source/explanation; appeals logged; no auto‑punitive action. - ---- - -### Phase 4 — Federation & Local Knowledge (Later) - -**Goal:** Help local groups while sharing learning globally. - -* **Per‑group indices** (workspace scoping) with opt‑in export. -* Global **insight cards** (anonymized patterns) curated by humans. - -**Acceptance:** Local index enabled; global feed shows anonymized insights with curator sign‑off. - ---- - -### Phase 5 — Continuous Learning & Audits (Later) - -**Goal:** Community‑governed improvement. - -* Feedback tagging (helpful/bias/off‑topic) and weekly review. -* Monthly **audit MD** (what Bridge suggested, where it erred, how it changed). - -**Acceptance:** Monthly audit published; trending issues down over time. - ---- - -## 6) Architecture (minimal, auditable) - -* **Interfaces** → `apps/frontend/app/(modules)/bridge/*` - `/bridge` explainer page; Ask input; Tidy button + card; Storybook stories. -* **Domain** → `packages/bridge-domain/*` - Entities: `BridgeQuery`, `BridgeAnswer`, `BridgeSummary`. -* **API** → `packages/bridge-api/*` - Handlers: `/api/bridge/qa`, `/api/bridge/tidy` (fixture‑first, schema‑checked). -* **Fixtures** → `packages/bridge-fixtures/*` - `docs.jsonl` + tiny keyword index (deterministic search for MVP). -* **Logs** → `logs/bridge/` - Append‑only NDJSON; daily rotation; `.gitkeep` tracked. - -**Config**: `BRIDGE_ENABLED`, `BRIDGE_TIDY_ENABLED`, `BRIDGE_FIXTURES`, `BRIDGE_LOG_DIR`, `BRIDGE_LOG_KEY`. - ---- - -## 7) Data, Privacy & Ethics - -* Index only **public repo docs + approved KB exports**; exclude private messages by default. -* Redact PII in summaries and logs; store **paths + line ranges**, not bodies. -* Every output shows disclaimer + source chips; members can click **Challenge/Correct**. - ---- - -## 8) Training & Setup (clear actions) - -1. **Assemble dataset** → export Manifesto, OPS/CI, STATUS, Modules into `docs.jsonl` with `{title, path, text}`. -2. **Curation script** → dedupe, trim, and PII redaction (emails/phones/handles/URLs). -3. **Fixture‑first retrieval** → keyword index over JSONL; normalize citations to `{ path, lines[] }`. -4. **Summarizer** → deterministic template‑based summarizer; optional local LLM (Ollama) later behind a flag. -5. **Tone cues** → minimal heuristic rules (e.g., 2nd‑person accusations, all‑caps spikes) with suggested reframes. -6. **Logging** → NDJSON append with `id, ts, inputs, sources, content_hash`; integrity check in `scripts/validate.sh`. -7. **Governance loop** → Bridge Oversight Circle; appeal labels; weekly triage. - ---- - -## 9) API Contracts (MVP) - -**POST /api/bridge/qa** - -```json -Req: { "question": "How do I run smoke?" } -Res: { - "answer": "Run `scripts/smoke.sh` …", - "sources": [{ "path": "docs/CI/Actions_Playbook.md", "lines": [42, 60] }], - "disclaimer": "Bridge may be imperfect; verify important details." -} -Errors: 204 (empty), 401, 403, 422 (no sources), 500 -``` - -**POST /api/bridge/tidy** - -```json -Req: { "threadId": "abc123" } -Res: { - "summary": "- What’s proposed…\n- Open questions…", - "tags": ["type:increment","size:S"], - "links": ["https://…/thread/abc123"], - "sources": [{ "path": "STATUS/What_we_finished_What_is_left.md", "lines": [12, 28] }], - "disclaimer": "Bridge may be imperfect; verify important details." -} -``` - -**Schemas & errors** should be validated (Zod or equivalent) with a standard taxonomy: `401` unauth, `403` disabled/flag off, `422` contract breach (e.g., missing sources), `204` empty input, `500` unexpected. - ---- - -## 10) CI Hooks & Proof Lines - -* `scripts/validate.sh` must output exactly: - -``` -LINT=OK -VALIDATORS=GREEN -SMOKE=OK -``` - -* Checks: JSONL fixture integrity, last log line parses as JSON, API example schemas pass, Storybook builds. - ---- - -## 11) Smaller Projects (to split into docs & issues) - -Break this module into contributor‑friendly projects: - -1. **Bridge Knowledge Dataset** - *Docs export + curation script + JSONL fixtures* - -2. **Bridge Ethics Charter** - *Codifies tone, fairness, transparency; links to Social Contract; informs prompts and UI* - -3. **Q&A Endpoint (Fixture‑First)** - *`/api/bridge/qa` + schemas + tests + citations* - -4. **Thread‑Tidy Endpoint (Fixture‑First)** - *`/api/bridge/tidy` + structure template + tests + tags* - -5. **Ask Bridge UI** - *Global input, loading/empty/error states, source chips, a11y* - -6. **Tidy Card UI** - *Summary card with Copy/Hide/Show; Storybook; keyboard/focus order* - -7. **Append‑only Logs** - *NDJSON writer + daily rotation + integrity validator* - -8. **Tone Cues & Reframes (Heuristics)** - *Non‑punitive prompts that suggest listening and trade‑offs* - -9. **Appeals & Feedback Loop** - *Challenge/Correct UI; oversight cadence; monthly audit template* - -10. **Federated Indices (Local ↔ Global)** - *Workspace scoping; anonymized insight cards pipeline* - -Each project should include: **scope, acceptance, labels (module:bridge, slice:*, size:XS|S, target:Now|Next|Later)** and a tiny proof (contract test or script output). - ---- - -## 12) Pilot: Public Landing Page (Owner-led) - -We’ll first ship a minimal /bridge page so visitors can ask “What is TogetherOS?” and get a calm, mission-first, streamed answer. This pilot uses a hosted LLM via API (no tools yet), logs anonymized requests, and seeds the Bridge FAQ we’ll curate from trusted testers. Contributors can help with the streaming UI, the /api/bridge/ask endpoint (rate-limit + error taxonomy + NDJSON logs), Storybook states, and a CI validator that prints LINT=OK / VALIDATORS=GREEN / SMOKE=OK. -For full scope, acceptance, and owner guidance (including how I’ll work with the assistant to draft prompts, simulate testers, and curate the FAQ), see docs/modules/bridge/landing-pilot.md. - ---- - -## 13) Link Hygiene - -* Keep this doc referenced from the **Modules Hub** and from the **Manifesto CTA** for contributors: “Find the whole list of modules here.” -* When renaming/moving files, list likely inbound links and provide a safe find/replace command in the PR description. - ---- - -**When code starts:** open branch `feature/bridge-qna-tidy-mvp` and implement Phase 1. diff --git a/docs/modules/governance.md b/docs/modules/governance.md deleted file mode 100644 index 204c2eee..00000000 --- a/docs/modules/governance.md +++ /dev/null @@ -1,55 +0,0 @@ -# Proposals & Decisions (Governance) - -**Scope:** Create, deliberate, and decide on proposals with transparent rules and lightweight, testable flows. -**Owner(s):** @coopeverything-core -**Labels:** `module:governance` - -## Status -Progress: 0% -Next milestone: Submit a minimal proposal and see it in a list. -Blockers/Notes: none - -## Why this exists -Members must be able to turn ideas into proposals, discuss them, and make decisions. We ship a thin vertical slice first so contributors can see end-to-end value quickly: submit → list → view details (voting later). - -## MVP slices (order) -1. **Proposal create (API + domain)** - - **acceptance:** - - `POST /api/proposals` validates with Zod (`title`, `summary`, `authorId`, `createdAt`). - - Stores to in-memory/fixture repo; returns `201` with `{id}`. - - Unit test covers happy path + validation errors. -2. **Proposal list (UI)** - - **acceptance:** - - Route `/governance` lists `title`, `author`, `createdAt`. - - Empty state, loading skeleton, and generic error are present. - - Storybook story for `` with empty/loaded states. -3. **Proposal details (UI + API)** - - **acceptance:** - - `/governance/[id]` shows `title`, `summary`, timestamps. - - 404 guarded (invalid id). - - Contract test for `GET /api/proposals/:id` with Zod parsing. -4. **Seed & fixtures (ops)** - - **acceptance:** - - `packages/governance-fixtures/seed.ts` adds 3 demo proposals. - - `pnpm -w seed:governance` runs and logs inserted ids. - - Proof-line in `scripts/validate.sh` confirms seeds runnable. - -## Code map -- `apps/frontend/app/(modules)/governance/*` (routes, server actions, tests) -- `packages/governance-domain/*` (entities, repo interfaces, unit tests) -- `packages/governance-api/*` (REST handlers, Zod contracts) -- `packages/governance-ui/*` (components, Storybook stories) -- `packages/governance-fixtures/*` (seed data, demo JSON) - -## UI contract (brief) -- `/governance` → `` (state: `proposals[]`) -- `/governance/[id]` → `` (state: `proposal | 404`) -- States required on both pages: **empty**, **loading**, **error**. - -## Done → Tell the story (DoD) -- Tests or manual steps verified (list loads, details render, create works). -- Docs updated (this page + link in `docs/modules/INDEX.md` already present). -- Proofs in PR body: -LINT=OK -VALIDATORS=GREEN -SMOKE=OK diff --git a/docs/project_structure.md b/docs/project_structure.md deleted file mode 100644 index 678201dc..00000000 --- a/docs/project_structure.md +++ /dev/null @@ -1,43 +0,0 @@ -# Project Structure - -## Repo layout (recommended) -``` -. -├── apps/ -│ └── frontend/ (Next.js 14 + Tailwind v4) -│ ├── docker-compose.yml -│ ├── Dockerfile -│ ├── src/ -│ │ ├── pages/ -│ │ │ ├── _app.tsx -│ │ │ └── signup.tsx -│ │ └── styles/globals.css -│ ├── tailwind.config.js -│ ├── postcss.config.js -│ └── public/ -├── packages/ -│ └── ui/ (future shared components, tokens) -├── scripts/ -│ └── smoke.sh (basic repo checks; runs in CI) -├── .github/ -│ └── workflows/ -│ ├── ci.yml (build/lint/test/smoke; skips install if no package.json) -│ └── deploy.yml (rsync + VPS redeploy on PR label 'staging-ok') -└── docs/ - ├── DDP_Knowledge.md - ├── DDP_Tech_Roadmap.md - └── OPERATIONS.md -``` - -## CSS update playbook (short version) -1. Edit `src/styles/globals.css` (scoped `.signup` rules). -2. Run: `docker compose build --no-cache && docker compose up -d` -3. Verify: - ```bash - curl -s "http://127.0.0.1:3010/signup?nocache=$(date +%s)" | grep -n ' Date: Fri, 24 Oct 2025 19:34:48 -0700 Subject: [PATCH 3/7] docs: remove redundant files and update indexes (#90) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: remove files superseded by .claude/knowledge/ KB Removed files: - docs/modules/bridge.md → .claude/knowledge/bridge-module.md - docs/modules/governance.md → .claude/knowledge/governance-module.md - docs/folder_structure.md → .claude/knowledge/architecture.md - docs/project_structure.md → .claude/knowledge/architecture.md These files were fully superseded by the comprehensive Knowledge Base added in PR #89. The KB versions are more detailed and up-to-date. * docs: update index files to reference .claude/knowledge/ KB [skip ci] --- docs/folder_structure.md | 54 ------- docs/index.md | 25 +++- docs/modules/INDEX.md | 25 +++- docs/modules/bridge.md | 298 ------------------------------------- docs/modules/governance.md | 55 ------- docs/project_structure.md | 43 ------ 6 files changed, 46 insertions(+), 454 deletions(-) delete mode 100644 docs/folder_structure.md delete mode 100644 docs/modules/bridge.md delete mode 100644 docs/modules/governance.md delete mode 100644 docs/project_structure.md diff --git a/docs/folder_structure.md b/docs/folder_structure.md deleted file mode 100644 index 2d591fae..00000000 --- a/docs/folder_structure.md +++ /dev/null @@ -1,54 +0,0 @@ -# Folder Structure (VPS Runtime) - -## VPS Server (runtime) -``` -/home/platform/htdocs/platform.local/frontend - ├── docker-compose.yml (builds from Dockerfile; exposes 127.0.0.1:3010) - ├── Dockerfile (Next.js 14 multi-stage build) - └── src/ - ├── pages/ - │ ├── _app.tsx - │ └── signup.tsx - └── styles/ - └── globals.css -``` - -## Runtime files -- `/root/ddp.env` (Compose env_file — DB credentials, secrets) -- `/var/www/html/` (public Nginx docroot, not used by Next container) - -## Recommended additions on VPS -- `scripts/redeploy-frontend.sh` (one-command rebuild/verify; executable) -- `tailwind.config.js` -- `postcss.config.js` -- `README.md` (quickstart + CSS playbook) - -## Permissions -- Keep `/root/ddp.env` readable by root only (0600). -- App source owned by deploy user; docker group for Compose runner. - -## Local Developer Environment (Windows) -Local repo root: `G:\Coopeverything\TogetherOS\ddp-on-vps` - -SSH keys: -- Private key: `G:\Coopeverything\TogetherOS\ssh_keys\id_ed25519` -- Public key: `G:\Coopeverything\TogetherOS\ssh_keys\id_ed25519.pub` - -Ensure public key present in: -- `/root/.ssh/authorized_keys` -- `/home/platform/.ssh/authorized_keys` - -System OpenSSH tools (Windows): -- `C:\Windows\System32\OpenSSH\ssh-keygen.exe` -- `C:\Windows\System32\OpenSSH\ssh-keyscan.exe` - -Other local dev assets: -- `G:\Coopeverything\TogetherOS\open-webui\` (local UI experiments; not part of deploy) - -## GitHub Actions Secrets (Deploy Staging) -- SSH_PRIVATE_KEY -- VPS_HOST = continentjump -- VPS_IP = -- VPS_USER = platform -- VPS_PATH = /home/platform/htdocs/platform.local/frontend - diff --git a/docs/index.md b/docs/index.md index 19bc2fee..84dd4541 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,5 @@ # docs/INDEX.md + # TogetherOS — Docs Index (Canon) This page lists the **canonical docs**. If you rename or add a doc, update this index in the same PR. @@ -6,6 +7,7 @@ This page lists the **canonical docs**. If you rename or add a doc, update this --- ## Canon (start here) + - [OPERATIONS.md](./OPERATIONS.md) — contributor playbook (tiny, verifiable steps) - [Manifesto.md](./Manifesto.md) — vision and purpose - [TogetherOS_CATEGORIES_AND_KEYWORDS.md](./TogetherOS_CATEGORIES_AND_KEYWORDS.md) — 8 Paths & keywords @@ -13,15 +15,33 @@ This page lists the **canonical docs**. If you rename or add a doc, update this - [OPS/MAINTAINERS_DEPLOY.md](./OPS/MAINTAINERS_DEPLOY.md) — internal deploy notes (names of secrets only) ## Status & Planning + - Public status explainer: [STATUS_v2.md](./STATUS_v2.md) - Tracker file (append-only log): [STATUS/What_we_finished_What_is_left_v2.txt](../STATUS/What_we_finished_What_is_left_v2.txt) - Tech roadmap: [roadmap/TECH_ROADMAP.md](./roadmap/TECH_ROADMAP.md) +## Knowledge Base (Detailed Specs) + +For comprehensive implementation guides, architecture patterns, and module specifications, see: + +- [Main Knowledge Base](../.claude/knowledge/togetheros-kb.md) — Core identity, workflow, and principles +- [Tech Stack](../.claude/knowledge/tech-stack.md) — Framework versions, dependencies, tooling +- [Architecture Patterns](../.claude/knowledge/architecture.md) — Data models, API contracts, monorepo structure +- [Bridge Module](../.claude/knowledge/bridge-module.md) — Complete AI assistant specification +- [Governance Module](../.claude/knowledge/governance-module.md) — Proposals & decisions implementation +- [Social Economy](../.claude/knowledge/social-economy.md) — Support Points, timebanking, Social Horizon currency +- [Cooperation Paths](../.claude/knowledge/cooperation-paths.md) — Full taxonomy with subcategories +- [CI/CD Discipline](../.claude/knowledge/ci-cd-discipline.md) — Proof lines, validation workflows +- [Data Models](../.claude/knowledge/data-models.md) — Core entities and relationships + ## Contributor Hubs + - Discussions landing: https://github.com/coopeverything/TogetherOS/discussions/88 - Repository README: [../README.md](../README.md) +- Modules Hub: [modules/INDEX.md](./modules/INDEX.md) ## 8 Paths (quick reference) + - Collaborative Education - Social Economy - Common Wellbeing @@ -35,18 +55,21 @@ This page lists the **canonical docs**. If you rename or add a doc, update this > [TogetherOS_CATEGORIES_AND_KEYWORDS.md](./TogetherOS_CATEGORIES_AND_KEYWORDS.md). ## OPS Docs (short & practical) + - OPS ground rules: [OPS/TogetherOS_OPS_Project_Knowledge.md](./OPS/TogetherOS_OPS_Project_Knowledge.md) - CI specifics: [CI/Actions_Playbook.md](./CI/Actions_Playbook.md) - ## How to propose a docs change + 1. One smallest change per PR. 2. Update this index if the change adds/renames/removes a doc. 3. Include proof lines in the PR body: +``` LINT=OK VALIDATORS=GREEN SMOKE=OK +``` 4. If docs-only, ensure `ci/docs` passes (markdown + links). diff --git a/docs/modules/INDEX.md b/docs/modules/INDEX.md index c65d9cb2..2cdb998d 100644 --- a/docs/modules/INDEX.md +++ b/docs/modules/INDEX.md @@ -1,29 +1,48 @@ # TogetherOS — Modules Hub + This hub lists all platform modules with links to specs, active increments, and current progress. + > Update this index whenever a module page is added or renamed. Each module keeps tiny public metrics or proof-lines (e.g., dashboards or `LINT=OK`, `VALIDATORS=GREEN`, `SMOKE=OK`) so progress stays visible across the repo. ## Core Modules + - [Monorepo & Scaffolding](./scaffold.md) - [UI System](./ui.md) - [Identity & Auth](./auth.md) - [Profiles](./profiles.md) - [Groups & Orgs](./groups.md) - [Forum / Deliberation](./forum.md) -- [Proposals & Decisions (Governance)](./governance.md) +- [Proposals & Decisions (Governance)](../../.claude/knowledge/governance-module.md) - [Social Economy Primitives](./social-economy.md) - [Support Points & Reputation](./reputation.md) - [Onboarding](./onboarding.md) - [Bridge — Internal pilot (core team only)](./bridge/landing-pilot.md) + - [Full Bridge Specification](../../.claude/knowledge/bridge-module.md) - [Search & Tags](./search.md) - [Notifications & Inbox](./notifications.md) - [Docs Site Hooks](./docs-hooks.md) - [Observability](./observability.md) - [Security & Privacy](./security.md) +## Knowledge Base (Comprehensive Specs) + +For detailed implementation guides and architecture patterns, see: + +- [Main Knowledge Base](../../.claude/knowledge/togetheros-kb.md) — Core identity and workflow +- [Tech Stack](../../.claude/knowledge/tech-stack.md) — Framework versions, dependencies, tooling +- [Architecture Patterns](../../.claude/knowledge/architecture.md) — Data models, API contracts, monorepo structure +- [Bridge Module](../../.claude/knowledge/bridge-module.md) — Complete AI assistant specification +- [Governance Module](../../.claude/knowledge/governance-module.md) — Proposals & decisions implementation +- [Social Economy](../../.claude/knowledge/social-economy.md) — Support Points, timebanking, Social Horizon currency +- [Cooperation Paths](../../.claude/knowledge/cooperation-paths.md) — Full taxonomy with subcategories +- [CI/CD Discipline](../../.claude/knowledge/ci-cd-discipline.md) — Proof lines, validation workflows +- [Data Models](../../.claude/knowledge/data-models.md) — Core entities and relationships + ## How we build -- **Branches:** `feature/-` from `main` (one tiny change per PR). + +- **Branches:** `feature/` from `main` (one tiny change per PR). - **Issues:** use the **Increment** template; label `module:`, `type:increment`, and `size:S|M|L`. - **Status:** authoritative overview lives in [../STATUS_v2.md](../STATUS_v2.md); each module page shows its own `Progress: X%`. -- **Definition of Done (DoD):** code merged → docs updated (this hub or module page) → proofs in PR body: `LINT=OK` `VALIDATORS=GREEN` `SMOKE=OK`. +- **Definition of Done (DoD):** code merged + docs updated (this hub or module page) + proofs in PR body: `LINT=OK` `VALIDATORS=GREEN` `SMOKE=OK`. diff --git a/docs/modules/bridge.md b/docs/modules/bridge.md deleted file mode 100644 index 3cf89baf..00000000 --- a/docs/modules/bridge.md +++ /dev/null @@ -1,298 +0,0 @@ -# Bridge — AI Assistant Platform - -> An always‑available assistant that helps people understand, deliberate, and act together. Bridge answers questions from our knowledge, tidies threads, and assists moderation with transparent, auditable suggestions — to change how humans decide and behave with one another and the planet. - -**Owner(s):** @coopeverything-core -**Labels:** module:bridge, type:increment, size:XS|S, slice:qna|tidy|ops, target:Now|Next|Later -**Status:** Progress 0% -**Next milestone:** Pilot Bridge-assisted landing page (minimal `/bridge` with streaming Q&A, rate-limit, and NDJSON logs) -**Blockers/Notes:** None - ---- - -> ### Parts (subpages) -> This page is the canonical overview. Detailed work happens in these focused subpages: -> - **docs/modules/bridge/landing-pilot.md** — minimal public `/bridge` page, streaming Q&A, logs & validator **(ready)**. -> - **docs/modules/bridge/faq-seed.md** — curated questions & answers from pilot testers **(coming soon)**. -> - **docs/modules/bridge/api.md** — ask/tidy API contracts, error taxonomy, examples **(coming soon)**. -> - **docs/modules/bridge/ethics-charter.md** — tone, privacy, assist-not-adjudicate guardrails **(coming soon)**. - ---- - -## 1) Why Bridge - -Bridge is a **cooperation amplifier**. It lowers friction to collective intelligence: people can grasp the facts, the trade‑offs, and the next steps without wading through miles of text or adversarial back‑and‑forth. Bridge teaches and reinforces the practices that make cooperation real. - -* **Lower friction for understanding**: converts long docs/threads into concise, cited summaries so members *understand before reacting*. -* **Nudge empathy and deliberation**: encourages steel‑manning, calm language, and trade‑off thinking; suggests reframes when a discussion heats up. -* **Preserve shared memory**: keeps auditable logs of questions, sources, and summaries; makes learning cumulative across the network. -* **Empower local ↔ global**: connects local questions to global knowledge while respecting privacy and consent. -* **Build trust in AI**: every suggestion includes sources, a confidence disclaimer, and appears as assistance — never as adjudication. - -**North‑star outcomes** - -* Faster, calmer decisions with documented trade‑offs. -* More first‑time contributors completing a helpful action in their first session. -* Fewer circular debates; clearer next steps in threads and proposals. - ---- - -## 2) Principles & Guardrails (the Social Contract in code) - -* **Assist, not adjudicate.** Bridge suggests; humans decide. -* **Cite & disclaim.** Every answer shows sources and includes: *“Bridge may be imperfect; verify important details.”* -* **Privacy first.** Index only public repo docs + approved KB exports; redact PII (emails/phones/handles) in outputs and logs. -* **Auditability.** Append‑only logs with IDs, timestamps, content hashes; validation scripts prove integrity. -* **Small, reversible steps.** Deliver tiny increments with clear acceptance and roll‑back paths. - ---- - -## 3) Scope (What Bridge does) - -**Member Q&A / Brainstorm (grounded)** -Answers questions using TogetherOS documentation and approved knowledge exports. Provides citations and simple, respectful prompting for brainstorming. - -**Thread tidy (summaries, tags, actions)** -Summarizes forum topics into a standard structure (problem → options → trade‑offs → open questions → next steps), proposes tags, and extracts candidate actions with links. - -**Moderation assist (suggestions, not decisions)** -Detects heated tone or derailments and suggests de‑escalations, label proposals, or merge/split hints. Includes an appeal link and logs the suggestion with reasons. - -**Onboarding nudge** -“Ask Bridge” is present on first run; suggests two tiny next actions and a person/project to follow, reducing time‑to‑first‑contribution. - -Out of scope (for now): punitive moderation, decision‑making authority, automated enforcement. - ---- - -## 4) Success Metrics (SLOs tied to human behavior) - -* **Time‑to‑first‑useful‑answer (p95)** in fixture mode < 800ms -* **Citation coverage** = 100% for non‑empty answers/summaries -* **Deliberation quality**: % of threads with a tidy card and extracted actions -* **Trust index**: ≥ 70% “helpful” ratings after 30 days -* **Appeals**: median resolution within 7 days; 5xx error budget tracked - ---- - -## 5) Phased Plan (each phase = small, verifiable) - -### Phase 0 — Foundations (Now) - -**Goal:** Ground Bridge in our knowledge and ethics. - -* Curate **Bridge Knowledge Dataset** → `packages/bridge-fixtures/docs.jsonl` (Manifesto, OPS/CI, STATUS, Modules, Social Contract). -* Write the **Bridge Ethics Charter** (tone, fairness, transparency, non‑punitive suggestions). -* Build **dataset curation script** (dedupe, trim, redact PII); add to CI with proof lines. -* Define **citation format** `{ path, lines[] }` + standard disclaimer text. - -**Acceptance:** JSONL exists and passes `scripts/validate.sh`; random samples map to real docs. - ---- - -### Phase 1 — MVP: Q&A + Tidy + Logs (Now) - -**Goal:** Answer a docs question and summarize a thread with sources and logs. - -**API (fixture‑first)** - -* `POST /api/bridge/qa` → `{ "answer": "", "sources": [{"path":"docs/.md","lines":[x,y]}], "disclaimer":"…" }` -* `POST /api/bridge/tidy` → `{ "summary":"", "tags":["type:increment","size:S"], "links":[""], "sources":[…], "disclaimer":"…" }` - -**UI** - -* Persistent **Ask Bridge** input (page‑aware). -* **Tidy with Bridge** button on forum topics → non‑blocking summary card with Copy/Hide/Show + source chips. - -**Logs (append‑only)** - -* `logs/bridge/actions-YYYY-MM-DD.ndjson` entries: `{ id, ts, action: "qa|tidy", inputs, sources, content_hash }` -* `scripts/validate.sh` checks: file exists, last non‑empty line parses as JSON; prints `LINT=OK`, `VALIDATORS=GREEN`, `SMOKE=OK`. - -**Acceptance** - -* Non‑empty outputs include ≥1 valid source from `docs/**` or `STATUS/**`. -* Storybook story renders tidy card; empty/loading/error states covered. - ---- - -### Phase 2 — Deliberation Structure & Empathy (Next) - -**Goal:** Encourage better conversations. - -* Standard **summary structure** enforced in templates (problem → options → trade‑offs → open questions → next steps). -* **Tone cues** (light heuristics): suggest neutral reframes; never punitive. -* **Action extraction**: propose 1–3 next steps + tags (human‑editable). - -**Acceptance:** ≥10 sample threads produce structured summaries; facilitators rate ≥70% “useful”. - ---- - -### Phase 3 — Moderation Assist (Suggestions only) (Later) - -**Goal:** Transparent moderation support with appeal paths. - -* Detect toxicity/derail and **suggest** labels/merges/splits with short rationales and links. -* **Appeal link** on each suggestion; corrections form a learning queue (governed). - -**Acceptance:** Suggestions include source/explanation; appeals logged; no auto‑punitive action. - ---- - -### Phase 4 — Federation & Local Knowledge (Later) - -**Goal:** Help local groups while sharing learning globally. - -* **Per‑group indices** (workspace scoping) with opt‑in export. -* Global **insight cards** (anonymized patterns) curated by humans. - -**Acceptance:** Local index enabled; global feed shows anonymized insights with curator sign‑off. - ---- - -### Phase 5 — Continuous Learning & Audits (Later) - -**Goal:** Community‑governed improvement. - -* Feedback tagging (helpful/bias/off‑topic) and weekly review. -* Monthly **audit MD** (what Bridge suggested, where it erred, how it changed). - -**Acceptance:** Monthly audit published; trending issues down over time. - ---- - -## 6) Architecture (minimal, auditable) - -* **Interfaces** → `apps/frontend/app/(modules)/bridge/*` - `/bridge` explainer page; Ask input; Tidy button + card; Storybook stories. -* **Domain** → `packages/bridge-domain/*` - Entities: `BridgeQuery`, `BridgeAnswer`, `BridgeSummary`. -* **API** → `packages/bridge-api/*` - Handlers: `/api/bridge/qa`, `/api/bridge/tidy` (fixture‑first, schema‑checked). -* **Fixtures** → `packages/bridge-fixtures/*` - `docs.jsonl` + tiny keyword index (deterministic search for MVP). -* **Logs** → `logs/bridge/` - Append‑only NDJSON; daily rotation; `.gitkeep` tracked. - -**Config**: `BRIDGE_ENABLED`, `BRIDGE_TIDY_ENABLED`, `BRIDGE_FIXTURES`, `BRIDGE_LOG_DIR`, `BRIDGE_LOG_KEY`. - ---- - -## 7) Data, Privacy & Ethics - -* Index only **public repo docs + approved KB exports**; exclude private messages by default. -* Redact PII in summaries and logs; store **paths + line ranges**, not bodies. -* Every output shows disclaimer + source chips; members can click **Challenge/Correct**. - ---- - -## 8) Training & Setup (clear actions) - -1. **Assemble dataset** → export Manifesto, OPS/CI, STATUS, Modules into `docs.jsonl` with `{title, path, text}`. -2. **Curation script** → dedupe, trim, and PII redaction (emails/phones/handles/URLs). -3. **Fixture‑first retrieval** → keyword index over JSONL; normalize citations to `{ path, lines[] }`. -4. **Summarizer** → deterministic template‑based summarizer; optional local LLM (Ollama) later behind a flag. -5. **Tone cues** → minimal heuristic rules (e.g., 2nd‑person accusations, all‑caps spikes) with suggested reframes. -6. **Logging** → NDJSON append with `id, ts, inputs, sources, content_hash`; integrity check in `scripts/validate.sh`. -7. **Governance loop** → Bridge Oversight Circle; appeal labels; weekly triage. - ---- - -## 9) API Contracts (MVP) - -**POST /api/bridge/qa** - -```json -Req: { "question": "How do I run smoke?" } -Res: { - "answer": "Run `scripts/smoke.sh` …", - "sources": [{ "path": "docs/CI/Actions_Playbook.md", "lines": [42, 60] }], - "disclaimer": "Bridge may be imperfect; verify important details." -} -Errors: 204 (empty), 401, 403, 422 (no sources), 500 -``` - -**POST /api/bridge/tidy** - -```json -Req: { "threadId": "abc123" } -Res: { - "summary": "- What’s proposed…\n- Open questions…", - "tags": ["type:increment","size:S"], - "links": ["https://…/thread/abc123"], - "sources": [{ "path": "STATUS/What_we_finished_What_is_left.md", "lines": [12, 28] }], - "disclaimer": "Bridge may be imperfect; verify important details." -} -``` - -**Schemas & errors** should be validated (Zod or equivalent) with a standard taxonomy: `401` unauth, `403` disabled/flag off, `422` contract breach (e.g., missing sources), `204` empty input, `500` unexpected. - ---- - -## 10) CI Hooks & Proof Lines - -* `scripts/validate.sh` must output exactly: - -``` -LINT=OK -VALIDATORS=GREEN -SMOKE=OK -``` - -* Checks: JSONL fixture integrity, last log line parses as JSON, API example schemas pass, Storybook builds. - ---- - -## 11) Smaller Projects (to split into docs & issues) - -Break this module into contributor‑friendly projects: - -1. **Bridge Knowledge Dataset** - *Docs export + curation script + JSONL fixtures* - -2. **Bridge Ethics Charter** - *Codifies tone, fairness, transparency; links to Social Contract; informs prompts and UI* - -3. **Q&A Endpoint (Fixture‑First)** - *`/api/bridge/qa` + schemas + tests + citations* - -4. **Thread‑Tidy Endpoint (Fixture‑First)** - *`/api/bridge/tidy` + structure template + tests + tags* - -5. **Ask Bridge UI** - *Global input, loading/empty/error states, source chips, a11y* - -6. **Tidy Card UI** - *Summary card with Copy/Hide/Show; Storybook; keyboard/focus order* - -7. **Append‑only Logs** - *NDJSON writer + daily rotation + integrity validator* - -8. **Tone Cues & Reframes (Heuristics)** - *Non‑punitive prompts that suggest listening and trade‑offs* - -9. **Appeals & Feedback Loop** - *Challenge/Correct UI; oversight cadence; monthly audit template* - -10. **Federated Indices (Local ↔ Global)** - *Workspace scoping; anonymized insight cards pipeline* - -Each project should include: **scope, acceptance, labels (module:bridge, slice:*, size:XS|S, target:Now|Next|Later)** and a tiny proof (contract test or script output). - ---- - -## 12) Pilot: Public Landing Page (Owner-led) - -We’ll first ship a minimal /bridge page so visitors can ask “What is TogetherOS?” and get a calm, mission-first, streamed answer. This pilot uses a hosted LLM via API (no tools yet), logs anonymized requests, and seeds the Bridge FAQ we’ll curate from trusted testers. Contributors can help with the streaming UI, the /api/bridge/ask endpoint (rate-limit + error taxonomy + NDJSON logs), Storybook states, and a CI validator that prints LINT=OK / VALIDATORS=GREEN / SMOKE=OK. -For full scope, acceptance, and owner guidance (including how I’ll work with the assistant to draft prompts, simulate testers, and curate the FAQ), see docs/modules/bridge/landing-pilot.md. - ---- - -## 13) Link Hygiene - -* Keep this doc referenced from the **Modules Hub** and from the **Manifesto CTA** for contributors: “Find the whole list of modules here.” -* When renaming/moving files, list likely inbound links and provide a safe find/replace command in the PR description. - ---- - -**When code starts:** open branch `feature/bridge-qna-tidy-mvp` and implement Phase 1. diff --git a/docs/modules/governance.md b/docs/modules/governance.md deleted file mode 100644 index 204c2eee..00000000 --- a/docs/modules/governance.md +++ /dev/null @@ -1,55 +0,0 @@ -# Proposals & Decisions (Governance) - -**Scope:** Create, deliberate, and decide on proposals with transparent rules and lightweight, testable flows. -**Owner(s):** @coopeverything-core -**Labels:** `module:governance` - -## Status -Progress: 0% -Next milestone: Submit a minimal proposal and see it in a list. -Blockers/Notes: none - -## Why this exists -Members must be able to turn ideas into proposals, discuss them, and make decisions. We ship a thin vertical slice first so contributors can see end-to-end value quickly: submit → list → view details (voting later). - -## MVP slices (order) -1. **Proposal create (API + domain)** - - **acceptance:** - - `POST /api/proposals` validates with Zod (`title`, `summary`, `authorId`, `createdAt`). - - Stores to in-memory/fixture repo; returns `201` with `{id}`. - - Unit test covers happy path + validation errors. -2. **Proposal list (UI)** - - **acceptance:** - - Route `/governance` lists `title`, `author`, `createdAt`. - - Empty state, loading skeleton, and generic error are present. - - Storybook story for `` with empty/loaded states. -3. **Proposal details (UI + API)** - - **acceptance:** - - `/governance/[id]` shows `title`, `summary`, timestamps. - - 404 guarded (invalid id). - - Contract test for `GET /api/proposals/:id` with Zod parsing. -4. **Seed & fixtures (ops)** - - **acceptance:** - - `packages/governance-fixtures/seed.ts` adds 3 demo proposals. - - `pnpm -w seed:governance` runs and logs inserted ids. - - Proof-line in `scripts/validate.sh` confirms seeds runnable. - -## Code map -- `apps/frontend/app/(modules)/governance/*` (routes, server actions, tests) -- `packages/governance-domain/*` (entities, repo interfaces, unit tests) -- `packages/governance-api/*` (REST handlers, Zod contracts) -- `packages/governance-ui/*` (components, Storybook stories) -- `packages/governance-fixtures/*` (seed data, demo JSON) - -## UI contract (brief) -- `/governance` → `` (state: `proposals[]`) -- `/governance/[id]` → `` (state: `proposal | 404`) -- States required on both pages: **empty**, **loading**, **error**. - -## Done → Tell the story (DoD) -- Tests or manual steps verified (list loads, details render, create works). -- Docs updated (this page + link in `docs/modules/INDEX.md` already present). -- Proofs in PR body: -LINT=OK -VALIDATORS=GREEN -SMOKE=OK diff --git a/docs/project_structure.md b/docs/project_structure.md deleted file mode 100644 index 678201dc..00000000 --- a/docs/project_structure.md +++ /dev/null @@ -1,43 +0,0 @@ -# Project Structure - -## Repo layout (recommended) -``` -. -├── apps/ -│ └── frontend/ (Next.js 14 + Tailwind v4) -│ ├── docker-compose.yml -│ ├── Dockerfile -│ ├── src/ -│ │ ├── pages/ -│ │ │ ├── _app.tsx -│ │ │ └── signup.tsx -│ │ └── styles/globals.css -│ ├── tailwind.config.js -│ ├── postcss.config.js -│ └── public/ -├── packages/ -│ └── ui/ (future shared components, tokens) -├── scripts/ -│ └── smoke.sh (basic repo checks; runs in CI) -├── .github/ -│ └── workflows/ -│ ├── ci.yml (build/lint/test/smoke; skips install if no package.json) -│ └── deploy.yml (rsync + VPS redeploy on PR label 'staging-ok') -└── docs/ - ├── DDP_Knowledge.md - ├── DDP_Tech_Roadmap.md - └── OPERATIONS.md -``` - -## CSS update playbook (short version) -1. Edit `src/styles/globals.css` (scoped `.signup` rules). -2. Run: `docker compose build --no-cache && docker compose up -d` -3. Verify: - ```bash - curl -s "http://127.0.0.1:3010/signup?nocache=$(date +%s)" | grep -n ' Date: Sat, 25 Oct 2025 12:35:43 -0700 Subject: [PATCH 4/7] feat(ci): optimize workflows with caching and path filters (#91) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: remove files superseded by .claude/knowledge/ KB Removed files: - docs/modules/bridge.md → .claude/knowledge/bridge-module.md - docs/modules/governance.md → .claude/knowledge/governance-module.md - docs/folder_structure.md → .claude/knowledge/architecture.md - docs/project_structure.md → .claude/knowledge/architecture.md These files were fully superseded by the comprehensive Knowledge Base added in PR #89. The KB versions are more detailed and up-to-date. * docs: update index files to reference .claude/knowledge/ KB [skip ci] * feat(ci): optimize workflows with caching and path filters - Add dependency caching to reduce runtime by 25-30 seconds - Add path filters to prevent unnecessary workflow runs - Optimize docs workflow with link checker caching - Expected performance: 5+ minutes -> ~2 minutes per run LINT=OK VALIDATORS=GREEN SMOKE=OK * Delete .github/workflows/app-token-smoke.yml not needed --- .github/workflows/app-token-smoke.yml | 31 --------------------------- .github/workflows/ci_docs.yml | 23 +++++++++++++++++--- .github/workflows/lint.yml | 29 ++++++++++++++++++++++--- .github/workflows/smoke.yml | 31 +++++++++++++++++++++++++-- 4 files changed, 75 insertions(+), 39 deletions(-) delete mode 100644 .github/workflows/app-token-smoke.yml diff --git a/.github/workflows/app-token-smoke.yml b/.github/workflows/app-token-smoke.yml deleted file mode 100644 index 2baabd4e..00000000 --- a/.github/workflows/app-token-smoke.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: app-token-smoke -on: - workflow_dispatch: -permissions: - contents: read - actions: read - -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Mint GitHub App token (official) - id: mint - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ secrets.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY_PEM }} - installation-id: ${{ secrets.APP_INSTALLATION_ID }} - - - name: Install jq (ensure present) - run: | - sudo apt-get update -y - sudo apt-get install -y jq - - - name: Show who we are (safe) - env: - TOKEN: ${{ steps.mint.outputs.token }} - run: | - set -e - echo "Token tail: ${TOKEN: -8}" - curl -s -H "Authorization: Bearer ${TOKEN}" -H "Accept: application/vnd.github+json" https://api.github.com/user | jq -r '.login,.type' diff --git a/.github/workflows/ci_docs.yml b/.github/workflows/ci_docs.yml index 0bcf5c35..fe137263 100644 --- a/.github/workflows/ci_docs.yml +++ b/.github/workflows/ci_docs.yml @@ -5,20 +5,33 @@ on: branches: [main] paths: - '**/*.md' + - '.markdownlint*' + - '.lychee*' pull_request: branches: [main] paths: - '**/*.md' + - '.markdownlint*' + - '.lychee*' jobs: docs-lint: runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 8 # Reduced from 10 minutes steps: - name: Checkout uses: actions/checkout@v4 + # Cache lychee for faster link checking + - name: Cache lychee + uses: actions/cache@v4 + with: + path: ~/.cache/lychee + key: lychee-${{ runner.os }}-${{ hashFiles('**/*.md') }} + restore-keys: | + lychee-${{ runner.os }}- + # Markdown style/lint - name: markdownlint uses: DavidAnson/markdownlint-cli2-action@v16 @@ -26,12 +39,16 @@ jobs: globs: | **/*.md - # Link checker + # Link checker with caching - name: Link check uses: lycheeverse/lychee-action@v1 with: args: > - --no-progress --verbose --exclude-mail --accept 200,204 + --no-progress --verbose --exclude-mail --accept 200,204,429 + --cache --max-cache-age 1d --include **/*.md env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Docs validation complete + run: echo "DOCS=OK" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c0b07797..28414e97 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -4,9 +4,19 @@ name: lint on: workflow_dispatch: pull_request: + paths: + - '.github/workflows/**' + - 'scripts/**' + - '**.yml' + - '**.yaml' push: branches: - main + paths: + - '.github/workflows/**' + - 'scripts/**' + - '**.yml' + - '**.yaml' jobs: lint: @@ -15,16 +25,29 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install dependencies + - name: Cache system dependencies + uses: actions/cache@v4 + id: sys-cache + with: + path: /usr/local/bin/actionlint + key: actionlint-${{ runner.os }}-v1.6.26 + + - name: Install system dependencies (if not cached) + if: steps.sys-cache.outputs.cache-hit != 'true' run: | - sudo apt-get update + sudo apt-get update -qq sudo apt-get install -y jq yamllint gh curl - - name: Install actionlint + - name: Install actionlint (if not cached) + if: steps.sys-cache.outputs.cache-hit != 'true' run: | curl -sSfL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash -o /tmp/actionlint.sh sudo bash /tmp/actionlint.sh latest /usr/local/bin + + - name: Verify tools + run: | actionlint --version && echo "ACTIONLINT=OK" + yamllint --version && echo "YAMLLINT=OK" - name: Run lint script if: ${{ hashFiles('scripts/lint.sh') != '' }} diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 834c68bf..487ebd99 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -4,9 +4,23 @@ name: smoke on: workflow_dispatch: pull_request: + paths: + - '.github/workflows/**' + - 'scripts/**' + - 'docs/**' + - '**.md' + - '**.yml' + - '**.yaml' push: branches: - main + paths: + - '.github/workflows/**' + - 'scripts/**' + - 'docs/**' + - '**.md' + - '**.yml' + - '**.yaml' schedule: # Daily at 12:00 UTC (05:00 PT) - cron: '0 12 * * *' @@ -25,12 +39,25 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Install validation tooling + - name: Cache validation tools + uses: actions/cache@v4 + id: tools-cache + with: + path: | + /usr/local/bin/actionlint + ~/.cache/apt + key: validation-tools-${{ runner.os }}-v1.6.26 + + - name: Install validation tooling (if not cached) + if: steps.tools-cache.outputs.cache-hit != 'true' run: | - sudo apt-get update + sudo apt-get update -qq sudo apt-get install -y jq yamllint gh curl curl -sSfL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash -o /tmp/actionlint.sh sudo bash /tmp/actionlint.sh latest /usr/local/bin + + - name: Verify cached tools + run: | actionlint --version echo "ACTIONLINT=OK" From 1ca2b78795a66aba3857b5cc84b0f61cc10f382b Mon Sep 17 00:00:00 2001 From: CoopEverything! <132305976+coopeverything@users.noreply.github.com> Date: Sat, 25 Oct 2025 19:11:47 -0700 Subject: [PATCH 5/7] docs(rewards): Add Reward System module spec and builder skill (#92) * docs: remove files superseded by .claude/knowledge/ KB * docs(rewards): add module spec and builder skill --- docs/modules/rewards.md | 857 ++++++++++++++++++++ docs/skills/reward-builder-skill.md | 1170 +++++++++++++++++++++++++++ 2 files changed, 2027 insertions(+) create mode 100644 docs/modules/rewards.md create mode 100644 docs/skills/reward-builder-skill.md diff --git a/docs/modules/rewards.md b/docs/modules/rewards.md new file mode 100644 index 00000000..0551e329 --- /dev/null +++ b/docs/modules/rewards.md @@ -0,0 +1,857 @@ +# Rewards Module — Recognition & Reputation + +## Overview + +**Rewards** is TogetherOS's system for recording meaningful contributions and converting participation into visible recognition through Support Points, badges, and cooperative currency flows. + +**Status:** 0% implementation (🎯 **First contributor module**) +**Owner:** @coopeverything-core +**Labels:** `module:rewards`, `good-first-issue` +**Priority:** Foundation for community engagement + +--- + +## Why Rewards Exists + +### The Problem +- Contributions go unrecognized → people disengage +- No visible path from participation to impact +- Trust and reputation built on word-of-mouth only +- Early contributors deserve credit for building foundation + +### The Solution +Rewards **makes cooperation visible and rewarding**: +- Every action (code, docs, governance, care) generates verifiable events +- Support Points quantify contribution across all domains +- Badges tell the story of what each person has done +- Recognition naturally enhances trust and opportunity + +### North-Star Outcomes +- Early code/infrastructure contributors get lasting recognition +- First-time contributors see clear progression paths +- Reputation becomes portable proof of cooperative skill +- Recognition system scales to all 8 Cooperation Paths + +--- + +## Core Principles + +1. **Recognition is nourishment** — Humans thrive when peers acknowledge contributions +2. **Transparency and fairness** — Every reward derives from recorded, verifiable action +3. **Scalable cooperation** — Same logic applies across all participation domains +4. **Proof of cooperation** — Actions, not titles/possessions, define contribution + +--- + +## Domains of Contribution + +All contribution domains will eventually generate reward events: + +| Domain | Examples | +|--------|----------| +| **Technology & Infrastructure** 🎯 | Code, docs, automation, systems design, maintenance | +| **Governance & Civic Life** | Facilitating deliberations, drafting proposals, mediation | +| **Education & Mentorship** | Teaching, translating, mentoring, documenting | +| **Social Economy** | Launching co-ops, mutual aid, timebanking | +| **Community Care** | Emotional support, accessibility, crisis management | +| **Culture & Media** | Films, music, writing, art uplifting cooperation | +| **Environment & Planet** | Regenerative projects, ecological restoration | +| **Design & UX** | Usability, accessibility, aesthetics improvements | + +**🎯 Phase A Focus:** Technology & Infrastructure (code contributors first) + +--- + +## Reward Mechanics + +### 1. Support Points (SP) +**Purpose:** Quantitative acknowledgment of contribution + +**How They Work:** +- Earned through verified actions (PR merges, proposals, facilitation) +- Increase visibility and unlock privileges +- Enable participation in collective decisions +- Partially convertible to timebank credits or Social Horizon + +**Example Weights (Phase A):** +```typescript +const SP_WEIGHTS = { + pr_merged_small: 5, // < 50 lines changed + pr_merged_medium: 10, // 50-200 lines + pr_merged_large: 20, // > 200 lines + docs_contribution: 8, // Documentation PR + code_review: 3, // Helpful review feedback + issue_triage: 2, // Issue labeling/clarification + bug_fix: 15, // Critical bug resolution +} +``` + +### 2. Badges & Skill Trees +**Purpose:** Represent milestones in contribution or mastery + +**Badge Examples:** +- 🔧 **First PR** — Merged your first contribution +- 🏗️ **Foundation Builder** — 10+ PRs in pre-MVP phase +- 📚 **Documentation Champion** — 5+ doc improvements +- 🐛 **Bug Hunter** — Fixed 5+ critical bugs +- 🎨 **UI Craftsperson** — 3+ UI/UX improvements +- 🔍 **Code Reviewer** — 10+ helpful reviews +- 🚀 **Module Launcher** — Shipped a complete module + +**Visibility:** Publicly displayed on profiles, portable across projects + +### 3. Timebank Credits (Future) +- Barter units exchangeable for help/mentorship within network +- Support Points partially convertible (e.g., 100 SP → 1 hour credit) + +### 4. Social Horizon Fractions (Future) +- Cooperative currency for lasting value creation +- Distributed with anti-speculation safeguards +- Tied to verified community benefit + +--- + +## Implementation Sequence + +### Phase A: Foundation (Now) 🎯 +**Goal:** Store early contributor actions (coders, designers) + +**Deliverables:** +- [ ] Event ledger schema + NDJSON storage +- [ ] GitHub webhook integration (PR events) +- [ ] Event collector API endpoint +- [ ] Basic event validation + deduplication +- [ ] Fixture data for testing + +**Outcome:** PR merges automatically recorded in event ledger + +--- + +### Phase B: Reward Logic (Next) +**Goal:** Calculate points and award badges + +**Deliverables:** +- [ ] Reward engine (event → SP calculation) +- [ ] Badge ruleset (YAML config) +- [ ] Member profile API (balances, badges, history) +- [ ] Anti-gaming safeguards (cooldowns, diversity checks) + +**Outcome:** Members see earned Support Points and badges + +--- + +### Phase C: Community Integration (Later) +**Goal:** Expand to all participation domains + +**Deliverables:** +- [ ] Bridge integration (Q&A, tidy contributions) +- [ ] Forum integration (facilitation, quality posts) +- [ ] Governance integration (proposals, votes, facilitation) + +**Outcome:** All 8 Cooperation Paths generating rewards + +--- + +### Phase D: Exchange Layer (Later) +**Goal:** Points convertible into cooperative currency + +**Deliverables:** +- [ ] Timebank integration +- [ ] Social Horizon integration +- [ ] Conversion rules and limits + +**Outcome:** SP → timebank credits → real cooperative value + +--- + +### Phase E: Analytics & Growth (Later) +**Goal:** Transparent, motivational data loops + +**Deliverables:** +- [ ] Public leaderboards (opt-in) +- [ ] Contribution reports +- [ ] Analytics dashboard + +**Outcome:** Community sees cooperative progress + +--- + +## Data Models + +### Event (Core Entity) +```typescript +interface RewardEvent { + id: string // UUID + actor_id: string // Member who performed action + event_type: RewardEventType + timestamp: Date + context: EventContext // Domain-specific metadata + source: string // Origin (github, forum, bridge) + weight: number // SP value (calculated) + status: 'pending' | 'processed' | 'rejected' + processed_at?: Date +} + +type RewardEventType = + // Code & Infrastructure + | 'pr_merged' + | 'pr_reviewed' + | 'issue_created' + | 'issue_triaged' + | 'bug_fixed' + | 'docs_contribution' + // Governance (future) + | 'proposal_submitted' + | 'proposal_facilitated' + | 'vote_cast' + // Community (future) + | 'moderation_action' + | 'community_event_hosted' + | 'bridge_qa_helpful' + | 'thread_tidied' + +interface EventContext { + // GitHub events + pr_number?: number + pr_size?: 'small' | 'medium' | 'large' + files_changed?: number + lines_changed?: number + repository?: string + + // Forum events (future) + thread_id?: string + post_quality?: number + + // Governance events (future) + proposal_id?: string + decision_id?: string + + // Generic + [key: string]: any +} +``` + +### Member Balance +```typescript +interface MemberRewardBalance { + member_id: string + support_points_total: number // All-time earned + support_points_available: number // Current balance + support_points_allocated: number // Locked in proposals + badges: Badge[] + level: number // Derived from total SP + created_at: Date + updated_at: Date +} +``` + +### Badge +```typescript +interface Badge { + id: string // UUID + name: string // Display name + description: string + icon: string // Emoji or URL + criteria: BadgeCriteria + rarity: 'common' | 'uncommon' | 'rare' | 'legendary' + earned_at?: Date +} + +interface BadgeCriteria { + event_types: RewardEventType[] + threshold: number // How many events required + conditions?: Record +} +``` + +### Transaction Log +```typescript +interface RewardTransaction { + id: string // UUID + member_id: string + type: 'earn' | 'allocate' | 'reclaim' | 'convert' + amount: number + source_event_id?: string // Link to RewardEvent + target_id?: string // Proposal/initiative receiving allocation + timestamp: Date + metadata: Record +} +``` + +--- + +## API Contracts + +### POST /api/rewards/events + +**Purpose:** Receive contribution events from external systems + +**Request:** +```typescript +{ + actor_id: string // Member UUID + event_type: string // From RewardEventType + source: string // 'github' | 'forum' | 'bridge' + context: { + // Event-specific data + pr_number?: number + files_changed?: number + // ... + } +} +``` + +**Response (Success):** +```typescript +201 Created +{ + id: string // Event UUID + weight: number // Calculated SP + processed: boolean +} +``` + +**Response (Duplicate):** +```typescript +409 Conflict +{ + error: { + code: "EVENT_ALREADY_PROCESSED", + message: "Event with this source and context already exists" + } +} +``` + +--- + +### GET /api/rewards/members/:id/balance + +**Purpose:** Retrieve member's reward balance and badges + +**Response:** +```typescript +200 OK +{ + member_id: string + support_points: { + total: number + available: number + allocated: number + } + badges: Badge[] + level: number + rank_percentile?: number // Optional: where they stand + recent_events: RewardEvent[] // Last 10 +} +``` + +--- + +### GET /api/rewards/leaderboard + +**Purpose:** Public leaderboard (opt-in members only) + +**Query Params:** +```typescript +{ + period?: 'week' | 'month' | 'all-time' + domain?: CooperationPath // Filter by contribution domain + limit?: number // Default 50, max 100 +} +``` + +**Response:** +```typescript +200 OK +{ + period: string + updated_at: Date + leaders: Array<{ + member_id: string + handle: string // Public identifier + support_points: number + badges_count: number + rank: number + }> +} +``` + +--- + +## GitHub Integration (Phase A) + +### Webhook Configuration + +**Events to Listen:** +- `pull_request` (opened, closed, merged) +- `pull_request_review` (submitted) +- `issues` (opened, labeled) +- `push` (to main/release branches) + +**Webhook Handler Flow:** +```typescript +async function handleGitHubWebhook(payload: WebhookPayload) { + // 1. Verify signature + if (!verifyGitHubSignature(payload)) { + return 401 + } + + // 2. Extract event data + const event = extractRewardEvent(payload) + + // 3. Map GitHub user to TogetherOS member + const member = await mapGitHubToMember(payload.sender) + + // 4. Create reward event + const rewardEvent = await createRewardEvent({ + actor_id: member.id, + event_type: determineEventType(payload), + source: 'github', + context: extractContext(payload) + }) + + // 5. Process immediately or queue + await processRewardEvent(rewardEvent) + + return 200 +} +``` + +### Event Mapping Examples + +**PR Merged:** +```typescript +{ + event_type: 'pr_merged', + context: { + pr_number: 42, + pr_size: 'medium', // Based on files/lines changed + files_changed: 8, + lines_changed: 156, + repository: 'coopeverything/TogetherOS' + } +} +``` + +**Code Review:** +```typescript +{ + event_type: 'pr_reviewed', + context: { + pr_number: 42, + review_quality: 'helpful', // Determined by PR author reaction + comments_count: 3 + } +} +``` + +--- + +## Event Storage (NDJSON) + +### Log Format +```typescript +// logs/rewards/events-YYYY-MM-DD.ndjson +{ + "id": "uuid", + "timestamp": "2025-01-15T10:30:00Z", + "event_type": "pr_merged", + "actor_id": "member-uuid", + "source": "github", + "context": { + "pr_number": 42, + "pr_size": "medium", + "files_changed": 8, + "lines_changed": 156 + }, + "weight": 10, + "status": "processed", + "content_hash": "sha256..." +} +``` + +### Validation Rules +- File must be valid NDJSON (each line = JSON object) +- Required fields: `id`, `timestamp`, `event_type`, `actor_id` +- Integrity: SHA-256 chain validation +- Rotation: Daily log files + +### CI Validation +```bash +# scripts/validate.sh checks: +# - NDJSON format valid +# - Last line parses successfully +# - Required fields present +# - No duplicate event IDs + +# Expected output: +LINT=OK +VALIDATORS=GREEN +SMOKE=OK +``` + +--- + +## Anti-Gaming Safeguards + +### Cooldowns +- Same event type: 1 hour minimum between similar actions +- PR spam: Max 5 PRs per day counted +- Review spam: Max 10 reviews per day + +### Diversity Checks +- Bonus for contributing across multiple domains +- Penalty for only single-type contributions + +### Multi-Review Validation +- Large SP awards (>50) require admin approval +- Suspicious patterns flagged for review +- Appeal process for rejected events + +### Public Audit +- All rules and weights published in versioned docs +- Monthly review of reward distribution +- Community proposals can adjust weights + +--- + +## Badge Ruleset (YAML Config) + +### Example Configuration +```yaml +badges: + - id: first-pr + name: First PR + description: Merged your first pull request + icon: 🔧 + rarity: common + criteria: + event_types: [pr_merged] + threshold: 1 + + - id: foundation-builder + name: Foundation Builder + description: Merged 10+ PRs in pre-MVP phase + icon: 🏗️ + rarity: uncommon + criteria: + event_types: [pr_merged] + threshold: 10 + conditions: + phase: pre-mvp + + - id: bug-hunter + name: Bug Hunter + description: Fixed 5+ critical bugs + icon: 🐛 + rarity: rare + criteria: + event_types: [bug_fixed] + threshold: 5 + conditions: + severity: critical + + - id: module-launcher + name: Module Launcher + description: Shipped a complete module to production + icon: 🚀 + rarity: legendary + criteria: + event_types: [pr_merged] + threshold: 1 + conditions: + module_complete: true +``` + +--- + +## UI Components (Phase B) + +### RewardBalance Widget +```typescript +interface RewardBalanceProps { + memberId: string +} + +// Display on member profile sidebar +// Shows: SP total, available, allocated +// Visual: Progress bar toward next level +``` + +### BadgeCollection +```typescript +interface BadgeCollectionProps { + badges: Badge[] + layout: 'grid' | 'list' +} + +// Display earned badges with tooltips +// Click to see criteria and progress +``` + +### RewardHistory +```typescript +interface RewardHistoryProps { + memberId: string + limit?: number +} + +// Timeline of reward events +// Filter by event type, domain +// Export capability +``` + +### Leaderboard +```typescript +interface LeaderboardProps { + period: 'week' | 'month' | 'all-time' + domain?: CooperationPath +} + +// Opt-in only (privacy default) +// Anonymized handles for non-opted-in +// Motivational, not competitive +``` + +--- + +## Repository Pattern + +### Interface +```typescript +// packages/rewards-domain/repos/RewardEventRepo.ts +export interface RewardEventRepo { + create(event: CreateRewardEventInput): Promise + findById(id: string): Promise + findByMember(memberId: string, filters?: EventFilters): Promise + findPending(): Promise + markProcessed(id: string): Promise + checkDuplicate(source: string, context: EventContext): Promise +} + +export interface EventFilters { + event_types?: RewardEventType[] + date_range?: { start: Date; end: Date } + status?: 'pending' | 'processed' | 'rejected' + limit?: number +} +``` + +### In-Memory Implementation (MVP) +```typescript +// packages/rewards-domain/repos/InMemoryRewardEventRepo.ts +export class InMemoryRewardEventRepo implements RewardEventRepo { + private events: Map = new Map() + + async create(input: CreateRewardEventInput): Promise { + const event: RewardEvent = { + id: generateId(), + actor_id: input.actor_id, + event_type: input.event_type, + timestamp: new Date(), + context: input.context, + source: input.source, + weight: calculateWeight(input.event_type, input.context), + status: 'pending', + } + this.events.set(event.id, event) + return event + } + + async findByMember(memberId: string, filters?: EventFilters): Promise { + let results = Array.from(this.events.values()) + .filter(e => e.actor_id === memberId) + + if (filters?.event_types) { + results = results.filter(e => filters.event_types!.includes(e.event_type)) + } + + if (filters?.date_range) { + results = results.filter(e => + e.timestamp >= filters.date_range!.start && + e.timestamp <= filters.date_range!.end + ) + } + + return results.slice(0, filters?.limit || 100) + } + + // ... other methods +} +``` + +--- + +## Testing Strategy + +### Unit Tests +```typescript +// packages/rewards-domain/__tests__/RewardEvent.test.ts +describe('RewardEvent', () => { + it('calculates weight for small PR', () => { + const event = createPREvent({ size: 'small' }) + expect(event.weight).toBe(5) + }) + + it('prevents duplicate events', async () => { + await repo.create({ source: 'github', context: { pr_number: 42 } }) + const isDupe = await repo.checkDuplicate('github', { pr_number: 42 }) + expect(isDupe).toBe(true) + }) +}) +``` + +### Contract Tests +```typescript +// apps/api/src/modules/rewards/__tests__/createEvent.test.ts +describe('POST /api/rewards/events', () => { + it('accepts valid event', async () => { + const response = await request(app) + .post('/api/rewards/events') + .send({ + actor_id: 'member-123', + event_type: 'pr_merged', + source: 'github', + context: { pr_number: 42 } + }) + + expect(response.status).toBe(201) + expect(response.body).toHaveProperty('id') + }) + + it('rejects duplicate event', async () => { + // Create first time + await createEvent({ pr_number: 42 }) + + // Try duplicate + const response = await request(app) + .post('/api/rewards/events') + .send({ pr_number: 42 }) + + expect(response.status).toBe(409) + }) +}) +``` + +### Integration Tests +```typescript +// Full flow: GitHub webhook → Event → SP calculation → Badge award +describe('GitHub webhook integration', () => { + it('awards badge for first PR merge', async () => { + const payload = createPRMergedPayload({ user: 'alice' }) + await handleWebhook(payload) + + const member = await getMember('alice') + expect(member.badges).toContainBadge('first-pr') + expect(member.support_points_total).toBe(10) + }) +}) +``` + +--- + +## Governance & Ethics + +### Transparency Commitments +- **Public weights:** All SP calculations documented +- **Versioned rules:** Changes tracked in git with rationale +- **Monthly audits:** Distribution reports published +- **Community proposals:** Weight adjustments via governance + +### Consent & Privacy +- **Opt-in leaderboards:** Public display requires consent +- **Anonymized aggregates:** Community stats don't expose individuals +- **Export capability:** Members download their complete history +- **Deletion rights:** Remove from public view anytime + +### Fairness Principles +- **Quality over quantity:** Emphasize meaningful contributions +- **Collaboration bonuses:** Reward helping others succeed +- **Empathy weight:** Facilitation and care work valued equally +- **Anti-whale:** Prevent gaming through cooldowns and review + +--- + +## Definition of Done (Phase A) + +When Phase A is complete: + +✅ Event ledger schema defined and documented +✅ NDJSON storage with daily rotation working +✅ GitHub webhook handler receives PR events +✅ Events mapped to reward events correctly +✅ Deduplication prevents double-counting +✅ Fixture data seeds test events +✅ Validation script checks NDJSON integrity +✅ Unit tests pass for event creation +✅ Contract tests pass for API endpoints +✅ Proof lines in CI: `LINT=OK`, `VALIDATORS=GREEN`, `SMOKE=OK` + +--- + +## Contributing (For Developers) + +### 🎯 This is the FIRST module open to contributors! + +We've designed the Reward System to be accessible with clear, small issues perfect for first-time TogetherOS contributors. + +**Why Start Here:** +- Foundation for all future contribution tracking +- Well-scoped tasks with clear acceptance criteria +- Your work directly benefits YOU (you'll earn the first badges!) +- Pattern you establish will be used across all modules + +### Getting Started + +1. **Read this spec** — Understand the full picture +2. **Check Issues** — Look for `good-first-issue` labels +3. **Join Discussions** — Ask questions in Discussions #88 +4. **Small PRs** — One change per PR, well-tested +5. **Follow workflow** — Branch from `Claude-1st-build`, target back to it + +### Issue Breakdown Strategy + +We've broken Phase A into ~15 small issues, each completable in 2-4 hours: + +**Category: Schema & Models** +- Define RewardEvent entity +- Define MemberRewardBalance entity +- Define Badge entity +- Create Zod validation schemas + +**Category: Repository Layer** +- Create RewardEventRepo interface +- Implement InMemoryRewardEventRepo +- Add deduplication logic +- Create fixture seed data + +**Category: API Layer** +- Create POST /api/rewards/events endpoint +- Create GET /api/rewards/members/:id/balance endpoint +- Add event validation middleware +- Error handling and status codes + +**Category: GitHub Integration** +- Setup webhook receiver +- Map PR events to RewardEvent +- Calculate SP weights +- Handle edge cases (bot PRs, etc.) + +**Category: Infrastructure** +- NDJSON log writer +- Log rotation (daily) +- Validation script +- CI integration + +See detailed issues in GitHub with `module:rewards` label. + +--- + +## Related KB Files + +- [Main KB](./togetheros-kb.md) — Core principles, workflow +- [Architecture](./architecture.md) — Domain-driven design, repository pattern +- [Data Models](./data-models.md) — Complete entity specifications +- [Social Economy](./social-economy.md) — Support Points allocation, timebanking +- [Cooperation Paths](./cooperation-paths.md) — All 8 domains that generate rewards +- [CI/CD Discipline](./ci-cd-discipline.md) — Proof lines, validation workflows diff --git a/docs/skills/reward-builder-skill.md b/docs/skills/reward-builder-skill.md new file mode 100644 index 00000000..fee0fdfd --- /dev/null +++ b/docs/skills/reward-builder-skill.md @@ -0,0 +1,1170 @@ +# Reward Builder Skill + +## Purpose + +This skill helps maintainers and contributors build the TogetherOS Reward System module efficiently. It provides templates, patterns, and guidance for creating well-structured, contributor-friendly issues and implementing features that follow TogetherOS principles. + +--- + +## Target Users + +- **Maintainers:** Breaking down module into issues, reviewing PRs +- **Contributors:** First-time and experienced developers building features +- **Project Leads:** Planning sprints and prioritizing work + +--- + +## Skill Capabilities + +### 1. Issue Creation Templates +### 2. Code Implementation Patterns +### 3. Testing Strategies +### 4. PR Review Checklists +### 5. Documentation Standards + +--- + +## 1. Issue Creation Templates + +### Template: Entity Definition + +```markdown +## Title +Define [EntityName] Entity + +## Description +Create the core domain model for [entity purpose]. + +## Acceptance Criteria +- [ ] TypeScript interface defined in `packages/types/src/rewards.ts` +- [ ] All required fields documented with JSDoc comments +- [ ] Validation logic for field constraints +- [ ] Unit tests cover edge cases +- [ ] Type exports added to index.ts + +## Technical Details + +**File:** `packages/types/src/rewards.ts` + +**Interface Structure:** +\`\`\`typescript +interface [EntityName] { + id: string // UUID + [field]: [type] // [description] + // ... more fields +} +\`\`\` + +**Validation Rules:** +- [Rule 1] +- [Rule 2] + +## Related Files +- `packages/validators/src/rewards.ts` (Zod schemas) +- `docs/modules/rewards.md` (specification reference) + +## Size +`size:XS` (1-2 hours) + +## Labels +`good-first-issue`, `module:rewards`, `type:entity` + +## Help Available +Ask questions in Discussions #88 or tag @[maintainer] +``` + +--- + +### Template: Repository Implementation + +```markdown +## Title +Implement [RepositoryName] with In-Memory Storage + +## Description +Create repository interface and in-memory implementation for [entity]. + +## Acceptance Criteria +- [ ] Interface defined in `packages/rewards-domain/repos/[Name]Repo.ts` +- [ ] In-memory implementation in `InMemory[Name]Repo.ts` +- [ ] CRUD operations implemented (create, find, update, delete) +- [ ] Fixture seed data for testing +- [ ] Unit tests achieve 90%+ coverage +- [ ] Repository exports added to index.ts + +## Technical Details + +**Interface Methods:** +\`\`\`typescript +export interface [Name]Repo { + create(input: Create[Name]Input): Promise<[Name]> + findById(id: string): Promise<[Name] | null> + list(filters: [Name]Filters): Promise<[Name][]> + update(id: string, updates: Partial<[Name]>): Promise<[Name]> + delete(id: string): Promise +} +\`\`\` + +**In-Memory Implementation:** +- Use `Map` for storage +- Implement filtering logic +- Handle not-found cases gracefully + +## Files to Create +- `packages/rewards-domain/repos/[Name]Repo.ts` (interface) +- `packages/rewards-domain/repos/InMemory[Name]Repo.ts` (implementation) +- `packages/rewards-domain/repos/__tests__/[Name]Repo.test.ts` (tests) + +## Dependencies +- Requires: [EntityName] entity defined +- Blocks: API handlers for [entity] + +## Size +`size:S` (2-4 hours) + +## Labels +`module:rewards`, `type:repository` + +## Testing Guidance +See "Repository Testing Pattern" in this skill document. +``` + +--- + +### Template: API Endpoint + +```markdown +## Title +Create [METHOD] /api/rewards/[route] Endpoint + +## Description +Implement API endpoint for [action description]. + +## Acceptance Criteria +- [ ] Handler created in `apps/api/src/modules/rewards/handlers/[name].ts` +- [ ] Zod schema validates input +- [ ] Repository integration works +- [ ] Error handling covers all cases (401, 403, 422, 500) +- [ ] Contract tests pass +- [ ] API documented in rewards.md spec + +## Technical Details + +**Endpoint:** `[METHOD] /api/rewards/[route]` + +**Request Schema:** +\`\`\`typescript +const [name]Schema = z.object({ + // fields +}) +\`\`\` + +**Response (Success):** +\`\`\`typescript +[status] [Status Text] +{ + // response body +} +\`\`\` + +**Error Responses:** +- `401 Unauthorized` — Missing/invalid auth +- `403 Forbidden` — Insufficient permissions +- `422 Unprocessable Entity` — Validation failed +- `500 Internal Server Error` — Unexpected failure + +## Files to Create/Modify +- `apps/api/src/modules/rewards/handlers/[name].ts` +- `packages/validators/src/rewards.ts` (add schema) +- `apps/api/src/modules/rewards/handlers/__tests__/[name].test.ts` + +## Dependencies +- Requires: [Repository] implementation +- Requires: [Entity] definition + +## Size +`size:S` (2-4 hours) + +## Labels +`module:rewards`, `type:api-endpoint` + +## Testing Guidance +See "API Contract Testing Pattern" in this skill document. +``` + +--- + +### Template: GitHub Integration + +```markdown +## Title +Implement GitHub [EventType] Webhook Handler + +## Description +Handle [event type] webhooks from GitHub and create reward events. + +## Acceptance Criteria +- [ ] Webhook handler receives and validates GitHub payload +- [ ] Event data extracted correctly +- [ ] GitHub user mapped to TogetherOS member +- [ ] RewardEvent created with correct weight +- [ ] Signature verification implemented +- [ ] Deduplication prevents double-counting +- [ ] Integration tests pass with sample payloads + +## Technical Details + +**Webhook Event:** `[github_event_name]` + +**Payload Structure:** +\`\`\`typescript +interface [EventName]Payload { + // GitHub webhook payload fields +} +\`\`\` + +**Event Mapping:** +\`\`\`typescript +{ + event_type: '[reward_event_type]', + context: { + // extracted context + }, + weight: [calculated_weight] +} +\`\`\` + +**Weight Calculation:** +[Describe logic] + +## Files to Create/Modify +- `apps/api/src/modules/rewards/handlers/githubWebhook.ts` +- `apps/api/src/modules/rewards/lib/calculateWeight.ts` +- `apps/api/src/modules/rewards/__tests__/githubWebhook.test.ts` + +## Sample Payloads +Create test fixtures in `packages/rewards-fixtures/github/` + +## Size +`size:M` (4-6 hours) + +## Labels +`module:rewards`, `type:integration`, `priority:high` + +## Security Considerations +- Verify webhook signature using GitHub secret +- Validate payload structure before processing +- Rate limit webhook endpoint +``` + +--- + +### Template: UI Component + +```markdown +## Title +Create [ComponentName] UI Component + +## Description +Build [component purpose] for member profiles/dashboard. + +## Acceptance Criteria +- [ ] Component created in `packages/ui/src/rewards/[ComponentName].tsx` +- [ ] Props interface defined and documented +- [ ] All states handled (loading, empty, error, success) +- [ ] Accessible (keyboard nav, ARIA labels, screen readers) +- [ ] Storybook stories for all states +- [ ] Tailwind styling follows design system +- [ ] Responsive design (mobile, tablet, desktop) + +## Technical Details + +**Component API:** +\`\`\`typescript +interface [ComponentName]Props { + [prop]: [type] + // ... more props +} +\`\`\` + +**States to Handle:** +\`\`\`typescript +type ComponentState = + | { status: 'loading' } + | { status: 'empty' } + | { status: 'error'; error: Error } + | { status: 'success'; data: [Type] } +\`\`\` + +**Storybook Stories:** +- Default +- Loading +- Empty +- Error +- With Data (multiple scenarios) + +## Files to Create +- `packages/ui/src/rewards/[ComponentName].tsx` +- `packages/ui/src/rewards/[ComponentName].stories.tsx` +- `packages/ui/src/rewards/__tests__/[ComponentName].test.tsx` + +## Design Reference +[Link to design mockup if available] + +## Size +`size:M` (4-6 hours) + +## Labels +`module:rewards`, `type:ui-component` + +## Accessibility Checklist +See "UI Component Accessibility" section in this skill. +``` + +--- + +## 2. Code Implementation Patterns + +### Pattern: Entity Definition + +```typescript +// packages/types/src/rewards.ts + +/** + * Represents a contribution event that earns rewards. + * + * Events are immutable records of actions taken by members + * that contribute value to the cooperative. + */ +export interface RewardEvent { + /** Unique identifier (UUID v4) */ + id: string + + /** Member who performed the action */ + actor_id: string + + /** Type of contribution event */ + event_type: RewardEventType + + /** When the event occurred (ISO 8601) */ + timestamp: Date + + /** Domain-specific metadata */ + context: EventContext + + /** Origin system (github, forum, bridge) */ + source: string + + /** Support Points value */ + weight: number + + /** Processing status */ + status: 'pending' | 'processed' | 'rejected' + + /** When event was processed */ + processed_at?: Date +} + +/** + * Contribution event types across all cooperation domains. + */ +export type RewardEventType = + // Code & Infrastructure + | 'pr_merged' + | 'pr_reviewed' + | 'issue_created' + | 'issue_triaged' + | 'bug_fixed' + | 'docs_contribution' + // Add more as needed + +/** + * Domain-specific context for events. + * Structure varies by event_type. + */ +export interface EventContext { + // GitHub-specific + pr_number?: number + pr_size?: 'small' | 'medium' | 'large' + files_changed?: number + lines_changed?: number + repository?: string + + // Extensible for other domains + [key: string]: any +} +``` + +**Best Practices:** +- ✅ Use JSDoc comments for all interfaces and fields +- ✅ Define union types explicitly (no `string` for enums) +- ✅ Make optional fields explicit with `?` +- ✅ Use Date objects, not strings (convert on boundaries) +- ✅ Keep entities pure (no framework dependencies) + +--- + +### Pattern: Repository Interface + +```typescript +// packages/rewards-domain/repos/RewardEventRepo.ts + +import { RewardEvent, RewardEventType, EventContext } from '@togetheros/types' + +/** + * Repository interface for managing reward events. + * + * Implementations can use in-memory storage, databases, + * or external services while maintaining the same contract. + */ +export interface RewardEventRepo { + /** + * Create a new reward event. + * + * @throws {ValidationError} If input invalid + * @throws {DuplicateError} If event already exists + */ + create(input: CreateRewardEventInput): Promise + + /** + * Find event by unique ID. + * + * @returns Event if found, null otherwise + */ + findById(id: string): Promise + + /** + * List events for a specific member. + * + * @param memberId - Member UUID + * @param filters - Optional filtering criteria + * @returns Array of matching events + */ + findByMember(memberId: string, filters?: EventFilters): Promise + + /** + * Find all pending (unprocessed) events. + * + * Used by reward processing job to calculate balances. + */ + findPending(): Promise + + /** + * Mark event as processed. + * + * Called after Support Points calculated and awarded. + */ + markProcessed(id: string): Promise + + /** + * Check if event already exists. + * + * Prevents duplicate rewards for same action. + */ + checkDuplicate(source: string, context: EventContext): Promise +} + +/** + * Input for creating a new reward event. + */ +export interface CreateRewardEventInput { + actor_id: string + event_type: RewardEventType + source: string + context: EventContext +} + +/** + * Filters for querying events. + */ +export interface EventFilters { + event_types?: RewardEventType[] + date_range?: { start: Date; end: Date } + status?: 'pending' | 'processed' | 'rejected' + limit?: number +} +``` + +**Best Practices:** +- ✅ Define clear interface boundaries +- ✅ Use async/await (Promises) for all methods +- ✅ Document error conditions with @throws +- ✅ Keep methods focused (single responsibility) +- ✅ Use descriptive parameter names + +--- + +### Pattern: In-Memory Repository + +```typescript +// packages/rewards-domain/repos/InMemoryRewardEventRepo.ts + +import { RewardEvent, RewardEventType } from '@togetheros/types' +import { RewardEventRepo, CreateRewardEventInput, EventFilters } from './RewardEventRepo' +import { generateId } from '../lib/uuid' +import { calculateWeight } from '../lib/calculateWeight' + +/** + * In-memory implementation of RewardEventRepo. + * + * Used for testing and MVP phase before database integration. + * NOT suitable for production (data lost on restart). + */ +export class InMemoryRewardEventRepo implements RewardEventRepo { + private events: Map = new Map() + + async create(input: CreateRewardEventInput): Promise { + // Check for duplicates + const isDupe = await this.checkDuplicate(input.source, input.context) + if (isDupe) { + throw new Error('Event already exists') + } + + // Create event + const event: RewardEvent = { + id: generateId(), + actor_id: input.actor_id, + event_type: input.event_type, + timestamp: new Date(), + context: input.context, + source: input.source, + weight: calculateWeight(input.event_type, input.context), + status: 'pending', + } + + this.events.set(event.id, event) + return event + } + + async findById(id: string): Promise { + return this.events.get(id) || null + } + + async findByMember(memberId: string, filters?: EventFilters): Promise { + let results = Array.from(this.events.values()) + .filter(e => e.actor_id === memberId) + + // Apply filters + if (filters?.event_types) { + results = results.filter(e => filters.event_types!.includes(e.event_type)) + } + + if (filters?.date_range) { + results = results.filter(e => + e.timestamp >= filters.date_range!.start && + e.timestamp <= filters.date_range!.end + ) + } + + if (filters?.status) { + results = results.filter(e => e.status === filters.status) + } + + // Apply limit + const limit = filters?.limit || 100 + return results.slice(0, limit) + } + + async findPending(): Promise { + return Array.from(this.events.values()) + .filter(e => e.status === 'pending') + } + + async markProcessed(id: string): Promise { + const event = this.events.get(id) + if (event) { + event.status = 'processed' + event.processed_at = new Date() + } + } + + async checkDuplicate(source: string, context: EventContext): Promise { + // Simple duplicate check based on source + key context fields + const key = this.generateDuplicateKey(source, context) + + return Array.from(this.events.values()).some(e => + this.generateDuplicateKey(e.source, e.context) === key + ) + } + + private generateDuplicateKey(source: string, context: EventContext): string { + // Create unique key from source + relevant context + // Adjust based on event type + if (context.pr_number) { + return `${source}:pr:${context.pr_number}` + } + if (context.issue_number) { + return `${source}:issue:${context.issue_number}` + } + // Fallback: stringify entire context + return `${source}:${JSON.stringify(context)}` + } +} +``` + +**Best Practices:** +- ✅ Implement full interface (no partial implementations) +- ✅ Handle edge cases (nulls, empty arrays, not found) +- ✅ Add private helper methods for clarity +- ✅ Document limitations (e.g., "not for production") +- ✅ Use Map/Set for efficient lookups + +--- + +### Pattern: Zod Validation Schema + +```typescript +// packages/validators/src/rewards.ts + +import { z } from 'zod' + +/** + * Schema for creating a reward event via API. + */ +export const createRewardEventSchema = z.object({ + actor_id: z.string().uuid('Invalid member UUID'), + event_type: z.enum([ + 'pr_merged', + 'pr_reviewed', + 'issue_created', + 'issue_triaged', + 'bug_fixed', + 'docs_contribution', + ]), + source: z.string().min(1), + context: z.record(z.any()), // Flexible for different event types +}) + +export type CreateRewardEventInput = z.infer + +/** + * Schema for PR merge context. + */ +export const prMergeContextSchema = z.object({ + pr_number: z.number().int().positive(), + pr_size: z.enum(['small', 'medium', 'large']), + files_changed: z.number().int().nonnegative(), + lines_changed: z.number().int().nonnegative(), + repository: z.string(), +}) + +/** + * Schema for filtering events. + */ +export const eventFiltersSchema = z.object({ + event_types: z.array(z.string()).optional(), + date_range: z.object({ + start: z.coerce.date(), + end: z.coerce.date(), + }).optional(), + status: z.enum(['pending', 'processed', 'rejected']).optional(), + limit: z.number().int().positive().max(100).optional(), +}) + +export type EventFilters = z.infer +``` + +**Best Practices:** +- ✅ Use descriptive error messages +- ✅ Validate all inputs at API boundaries +- ✅ Use z.infer to generate TypeScript types +- ✅ Separate schemas for different contexts +- ✅ Set reasonable limits (max array size, string length) + +--- + +### Pattern: API Handler + +```typescript +// apps/api/src/modules/rewards/handlers/createEvent.ts + +import { createRewardEventSchema } from '@togetheros/validators' +import { RewardEventRepo } from '@togetheros/rewards-domain/repos' + +/** + * Handle POST /api/rewards/events + * + * Creates a new reward event from external systems. + */ +export async function createEvent( + request: Request, + repo: RewardEventRepo +): Promise { + try { + // Parse and validate input + const body = await request.json() + const data = createRewardEventSchema.parse(body) + + // Create event + const event = await repo.create(data) + + // Return success + return Response.json( + { + id: event.id, + weight: event.weight, + processed: event.status === 'processed', + }, + { status: 201 } + ) + } catch (error) { + // Handle validation errors + if (error instanceof z.ZodError) { + return Response.json( + { + error: { + code: 'VALIDATION_ERROR', + message: 'Invalid input', + details: error.errors, + } + }, + { status: 422 } + ) + } + + // Handle duplicate errors + if (error.message === 'Event already exists') { + return Response.json( + { + error: { + code: 'EVENT_ALREADY_PROCESSED', + message: 'Event with this source and context already exists', + } + }, + { status: 409 } + ) + } + + // Handle unexpected errors + console.error('Error creating reward event:', error) + return Response.json( + { + error: { + code: 'INTERNAL_ERROR', + message: 'An unexpected error occurred', + } + }, + { status: 500 } + ) + } +} +``` + +**Best Practices:** +- ✅ Validate input with Zod schemas +- ✅ Handle all error types explicitly +- ✅ Return appropriate HTTP status codes +- ✅ Use consistent error response format +- ✅ Log errors for debugging (never expose internals to client) + +--- + +## 3. Testing Strategies + +### Unit Test Pattern: Entity + +```typescript +// packages/rewards-domain/__tests__/RewardEvent.test.ts + +import { describe, it, expect } from 'vitest' +import { createRewardEvent } from '../lib/createRewardEvent' + +describe('RewardEvent', () => { + it('creates event with valid input', () => { + const event = createRewardEvent({ + actor_id: 'member-123', + event_type: 'pr_merged', + source: 'github', + context: { pr_number: 42 } + }) + + expect(event.id).toBeDefined() + expect(event.actor_id).toBe('member-123') + expect(event.status).toBe('pending') + }) + + it('calculates weight for small PR', () => { + const event = createRewardEvent({ + event_type: 'pr_merged', + context: { pr_size: 'small' } + }) + + expect(event.weight).toBe(5) + }) + + it('calculates weight for medium PR', () => { + const event = createRewardEvent({ + event_type: 'pr_merged', + context: { pr_size: 'medium' } + }) + + expect(event.weight).toBe(10) + }) + + it('throws on invalid actor_id', () => { + expect(() => createRewardEvent({ + actor_id: 'not-a-uuid', + event_type: 'pr_merged' + })).toThrow('Invalid member UUID') + }) +}) +``` + +--- + +### Unit Test Pattern: Repository + +```typescript +// packages/rewards-domain/repos/__tests__/RewardEventRepo.test.ts + +import { describe, it, expect, beforeEach } from 'vitest' +import { InMemoryRewardEventRepo } from '../InMemoryRewardEventRepo' + +describe('RewardEventRepo', () => { + let repo: InMemoryRewardEventRepo + + beforeEach(() => { + repo = new InMemoryRewardEventRepo() + }) + + describe('create', () => { + it('creates event successfully', async () => { + const event = await repo.create({ + actor_id: 'member-123', + event_type: 'pr_merged', + source: 'github', + context: { pr_number: 42 } + }) + + expect(event.id).toBeDefined() + expect(event.actor_id).toBe('member-123') + }) + + it('prevents duplicate events', async () => { + await repo.create({ + source: 'github', + context: { pr_number: 42 } + }) + + await expect(repo.create({ + source: 'github', + context: { pr_number: 42 } + })).rejects.toThrow('Event already exists') + }) + }) + + describe('findByMember', () => { + it('returns all events for member', async () => { + await repo.create({ actor_id: 'member-123', ... }) + await repo.create({ actor_id: 'member-123', ... }) + await repo.create({ actor_id: 'member-456', ... }) + + const events = await repo.findByMember('member-123') + expect(events).toHaveLength(2) + }) + + it('filters by event type', async () => { + await repo.create({ event_type: 'pr_merged', ... }) + await repo.create({ event_type: 'pr_reviewed', ... }) + + const events = await repo.findByMember('member-123', { + event_types: ['pr_merged'] + }) + expect(events).toHaveLength(1) + expect(events[0].event_type).toBe('pr_merged') + }) + + it('respects limit', async () => { + for (let i = 0; i < 10; i++) { + await repo.create({ actor_id: 'member-123', ... }) + } + + const events = await repo.findByMember('member-123', { limit: 5 }) + expect(events).toHaveLength(5) + }) + }) +}) +``` + +--- + +### Contract Test Pattern: API + +```typescript +// apps/api/src/modules/rewards/__tests__/createEvent.test.ts + +import { describe, it, expect } from 'vitest' +import { createRewardEventSchema } from '@togetheros/validators' + +describe('POST /api/rewards/events', () => { + describe('input validation', () => { + it('accepts valid input', () => { + const result = createRewardEventSchema.safeParse({ + actor_id: '550e8400-e29b-41d4-a716-446655440000', + event_type: 'pr_merged', + source: 'github', + context: { pr_number: 42 } + }) + + expect(result.success).toBe(true) + }) + + it('rejects invalid actor_id', () => { + const result = createRewardEventSchema.safeParse({ + actor_id: 'not-a-uuid', + event_type: 'pr_merged', + source: 'github', + context: {} + }) + + expect(result.success).toBe(false) + }) + + it('rejects unknown event_type', () => { + const result = createRewardEventSchema.safeParse({ + actor_id: '550e8400-e29b-41d4-a716-446655440000', + event_type: 'unknown_type', + source: 'github', + context: {} + }) + + expect(result.success).toBe(false) + }) + }) +}) +``` + +--- + +## 4. PR Review Checklist + +### For Reviewers + +**Code Quality:** +- [ ] Follows TogetherOS code style and patterns +- [ ] No unnecessary complexity or premature optimization +- [ ] Functions are small and focused (single responsibility) +- [ ] Variable names are descriptive and clear +- [ ] Comments explain "why", not "what" + +**Testing:** +- [ ] Unit tests cover all code paths +- [ ] Contract tests validate API schemas +- [ ] Edge cases are tested (nulls, empty arrays, errors) +- [ ] Test coverage is >80% (aim for 90%+) + +**Documentation:** +- [ ] JSDoc comments on all exported functions/interfaces +- [ ] README updated if public API changed +- [ ] Module spec updated if behavior changed + +**TogetherOS Principles:** +- [ ] One tiny change per PR (smallest shippable increment) +- [ ] Docs-first: spec matches implementation +- [ ] Privacy-first: no PII exposure, IP hashing if needed +- [ ] Validation: Zod schemas validate all inputs + +**CI/CD:** +- [ ] PR includes proof lines in description +- [ ] All CI checks pass (ci/lint, ci/docs, ci/smoke) +- [ ] No linting errors or warnings +- [ ] Branch targets `Claude-1st-build` (not main) + +**Path Labels:** +- [ ] PR tagged with correct Cooperation Path +- [ ] Keywords listed in PR description + +**Git Hygiene:** +- [ ] Commit messages follow convention (type(scope): message) +- [ ] No merge commits (rebase preferred) +- [ ] Single focused change (not multiple unrelated changes) + +--- + +## 5. Documentation Standards + +### Module Spec Format + +Every module needs a comprehensive spec in `docs/modules/[module].md`: + +**Required Sections:** +1. **Overview** — Purpose, status, priority +2. **Why This Exists** — Problem/solution, outcomes +3. **Core Principles** — Non-negotiables +4. **Implementation Sequence** — Phases A/B/C/D +5. **Data Models** — Complete entity specifications +6. **API Contracts** — Request/response schemas +7. **UI Components** — Component specs (if applicable) +8. **Repository Pattern** — Interface + implementation guide +9. **Testing Strategy** — Unit/contract/integration patterns +10. **Definition of Done** — Acceptance checklist +11. **Contributing** — How developers can help +12. **Related KB Files** — Links to dependencies + +--- + +### JSDoc Standards + +```typescript +/** + * Brief one-line description of what this does. + * + * More detailed explanation if needed. Explain why this exists, + * what problem it solves, and any important constraints. + * + * @param paramName - Description of parameter + * @param optionalParam - Optional parameter description + * @returns Description of return value + * @throws {ErrorType} When this error occurs + * + * @example + * ```typescript + * const result = functionName('input') + * console.log(result) // Expected output + * ``` + */ +export function functionName( + paramName: string, + optionalParam?: number +): ReturnType { + // Implementation +} +``` + +--- + +### Inline Comment Guidelines + +**DO comment:** +- Why a specific approach was chosen +- Business logic or domain rules +- Complex algorithms or calculations +- Workarounds for known issues +- TODOs with context + +**DON'T comment:** +- What the code does (code should be self-documenting) +- Obvious operations +- Auto-generated comments + +**Examples:** + +✅ Good: +```typescript +// Use SHA-256 for deduplication to balance privacy and uniqueness +const key = createHash('sha256').update(data).digest('hex') + +// Cooldown prevents spam: max 5 PRs/day counted toward rewards +if (prCountToday >= 5) return +``` + +❌ Bad: +```typescript +// Create a hash +const key = createHash('sha256').update(data).digest('hex') + +// Check if greater than 5 +if (prCountToday >= 5) return +``` + +--- + +## Skill Usage Examples + +### Example 1: Creating Entity Definition Issue + +**Maintainer Task:** Break down "Event Model" into actionable issue + +**Use Skill:** +1. Open "1. Issue Creation Templates" → "Template: Entity Definition" +2. Fill in placeholders: + - `[EntityName]` → `RewardEvent` + - `[entity purpose]` → `contribution events that earn rewards` +3. Copy template to GitHub Issues +4. Add labels: `good-first-issue`, `module:rewards`, `type:entity`, `size:XS` +5. Assign to project board + +**Result:** Clear, actionable issue ready for contributor pickup + +--- + +### Example 2: Implementing Repository + +**Contributor Task:** Implement InMemoryRewardEventRepo + +**Use Skill:** +1. Read "2. Code Implementation Patterns" → "Pattern: Repository Interface" +2. Copy interface boilerplate +3. Read "Pattern: In-Memory Repository" +4. Implement methods following pattern +5. Read "3. Testing Strategies" → "Unit Test Pattern: Repository" +6. Write tests matching pattern +7. Submit PR with proof lines + +**Result:** High-quality implementation matching TogetherOS standards + +--- + +### Example 3: Reviewing PR + +**Maintainer Task:** Review PR for reward event creation + +**Use Skill:** +1. Open "4. PR Review Checklist" +2. Go through each section systematically +3. Leave specific feedback referencing patterns +4. If issues found, link to relevant skill sections +5. Approve when all boxes checked + +**Result:** Thorough, constructive review ensuring quality + +--- + +## Maintenance & Updates + +### When to Update This Skill + +- New issue type identified (add template) +- Code pattern evolves (update example) +- Test strategy improves (add new pattern) +- PR review catches common issue (add to checklist) +- Documentation standard changes (update guidelines) + +### Update Process + +1. Identify improvement needed +2. Update relevant section +3. Add example if helpful +4. Test with real issue/PR +5. Commit with clear message + +--- + +## Success Metrics + +**For Maintainers:** +- Time to create issue reduced from 20min → 5min +- Issue quality consistent across all created +- Fewer "what should I do?" questions + +**For Contributors:** +- First-time contributors ship PRs faster +- Code reviews have fewer rounds +- Tests follow standard patterns +- Documentation complete on first submission + +**For Project:** +- More contributors able to participate +- Higher quality contributions +- Faster feature delivery +- Better maintainability + +--- + +## Related Documentation + +- [Rewards Module Spec](../docs/modules/rewards.md) — Complete technical specification +- [Main KB](../docs/togetheros-kb.md) — Core principles and workflow +- [CI/CD Discipline](../docs/ci-cd-discipline.md) — Validation and proof lines +- [Architecture](../docs/architecture.md) — Domain-driven design patterns +- [Cooperation Paths](../docs/cooperation-paths.md) — All 8 contribution domains From 47f204a3c1f901bf0a38e83977adf93692b8388d Mon Sep 17 00:00:00 2001 From: George Rodafinos Date: Sat, 25 Oct 2025 21:21:10 -0700 Subject: [PATCH 6/7] feat(rewards): add RewardEvent entity with validation and tests - Add RewardEvent TypeScript interface with all required fields - Add Zod validation schemas for event creation and validation - Add comprehensive unit tests (happy path + error cases) - Add helper functions: getSPWeight, calculatePRSize, generateDedupKey - SP weights: pr_merged_small=5, medium=10, large=20, docs=8, review=3, triage=2, bug_fix=15 Follows TogetherOS domain-driven design patterns. Foundation for Rewards Module Phase A implementation. --- packages/types/__tests__/rewards.test.ts | 357 +++++++++++++++++++++++ packages/types/src/rewards.ts | 148 ++++++++++ packages/validators/src/rewards.ts | 152 ++++++++++ 3 files changed, 657 insertions(+) create mode 100644 packages/types/__tests__/rewards.test.ts create mode 100644 packages/types/src/rewards.ts create mode 100644 packages/validators/src/rewards.ts diff --git a/packages/types/__tests__/rewards.test.ts b/packages/types/__tests__/rewards.test.ts new file mode 100644 index 00000000..fbc0ee55 --- /dev/null +++ b/packages/types/__tests__/rewards.test.ts @@ -0,0 +1,357 @@ +// packages/types/__tests__/rewards.test.ts +// TogetherOS Rewards Module - Entity Validation Tests + +import { describe, it, expect } from 'vitest' +import { + createRewardEventSchema, + rewardEventSchema, + memberRewardBalanceSchema, + badgeSchema, + memberBadgeSchema, + isValidEventType, + getSPWeight, + calculatePRSize, + generateDedupKey, +} from '@togetheros/validators/rewards' + +describe('createRewardEventSchema', () => { + describe('valid inputs', () => { + it('accepts valid PR merge event', () => { + const input = { + memberId: '550e8400-e29b-41d4-a716-446655440000', + event_type: 'pr_merged_small', + context: { + pr_number: 123, + repo: 'TogetherOS', + lines_changed: 42, + }, + source: 'github', + } + + const result = createRewardEventSchema.safeParse(input) + expect(result.success).toBe(true) + }) + + it('accepts valid docs contribution event', () => { + const input = { + memberId: '550e8400-e29b-41d4-a716-446655440001', + event_type: 'docs_contribution', + context: { + pr_number: 456, + repo: 'TogetherOS', + }, + source: 'github', + timestamp: new Date('2025-01-25T10:00:00Z'), + } + + const result = createRewardEventSchema.safeParse(input) + expect(result.success).toBe(true) + }) + + it('accepts minimal valid event', () => { + const input = { + memberId: '550e8400-e29b-41d4-a716-446655440002', + event_type: 'code_review', + context: {}, + source: 'manual', + } + + const result = createRewardEventSchema.safeParse(input) + expect(result.success).toBe(true) + }) + }) + + describe('validation errors', () => { + it('rejects invalid member ID', () => { + const input = { + memberId: 'not-a-uuid', + event_type: 'pr_merged_small', + context: {}, + source: 'github', + } + + const result = createRewardEventSchema.safeParse(input) + expect(result.success).toBe(false) + if (!result.success) { + expect(result.error.issues[0].message).toContain('UUID') + } + }) + + it('rejects invalid event type', () => { + const input = { + memberId: '550e8400-e29b-41d4-a716-446655440000', + event_type: 'invalid_type', + context: {}, + source: 'github', + } + + const result = createRewardEventSchema.safeParse(input) + expect(result.success).toBe(false) + }) + + it('rejects empty source', () => { + const input = { + memberId: '550e8400-e29b-41d4-a716-446655440000', + event_type: 'pr_merged_small', + context: {}, + source: '', + } + + const result = createRewardEventSchema.safeParse(input) + expect(result.success).toBe(false) + }) + + it('rejects negative PR number', () => { + const input = { + memberId: '550e8400-e29b-41d4-a716-446655440000', + event_type: 'pr_merged_small', + context: { pr_number: -1 }, + source: 'github', + } + + const result = createRewardEventSchema.safeParse(input) + expect(result.success).toBe(false) + }) + + it('rejects negative lines changed', () => { + const input = { + memberId: '550e8400-e29b-41d4-a716-446655440000', + event_type: 'pr_merged_small', + context: { lines_changed: -10 }, + source: 'github', + } + + const result = createRewardEventSchema.safeParse(input) + expect(result.success).toBe(false) + }) + }) +}) + +describe('rewardEventSchema', () => { + it('accepts complete reward event', () => { + const event = { + id: '550e8400-e29b-41d4-a716-446655440000', + memberId: '550e8400-e29b-41d4-a716-446655440001', + event_type: 'pr_merged_medium', + sp_weight: 10, + context: { + pr_number: 123, + repo: 'TogetherOS', + lines_changed: 150, + }, + source: 'github', + dedup_key: 'github::pr:123::repo:TogetherOS', + timestamp: new Date(), + status: 'processed', + processedAt: new Date(), + } + + const result = rewardEventSchema.safeParse(event) + expect(result.success).toBe(true) + }) + + it('rejects invalid SP weight', () => { + const event = { + id: '550e8400-e29b-41d4-a716-446655440000', + memberId: '550e8400-e29b-41d4-a716-446655440001', + event_type: 'pr_merged_medium', + sp_weight: 0, // Must be positive + context: {}, + source: 'github', + dedup_key: 'test', + timestamp: new Date(), + status: 'processed', + } + + const result = rewardEventSchema.safeParse(event) + expect(result.success).toBe(false) + }) + + it('rejects invalid status', () => { + const event = { + id: '550e8400-e29b-41d4-a716-446655440000', + memberId: '550e8400-e29b-41d4-a716-446655440001', + event_type: 'pr_merged_medium', + sp_weight: 10, + context: {}, + source: 'github', + dedup_key: 'test', + timestamp: new Date(), + status: 'invalid_status', + } + + const result = rewardEventSchema.safeParse(event) + expect(result.success).toBe(false) + }) +}) + +describe('memberRewardBalanceSchema', () => { + it('accepts valid balance', () => { + const balance = { + memberId: '550e8400-e29b-41d4-a716-446655440000', + total: 100, + available: 70, + allocated: 30, + updatedAt: new Date(), + } + + const result = memberRewardBalanceSchema.safeParse(balance) + expect(result.success).toBe(true) + }) + + it('rejects mismatched totals', () => { + const balance = { + memberId: '550e8400-e29b-41d4-a716-446655440000', + total: 100, + available: 70, + allocated: 20, // Should be 30 + updatedAt: new Date(), + } + + const result = memberRewardBalanceSchema.safeParse(balance) + expect(result.success).toBe(false) + if (!result.success) { + expect(result.error.issues[0].message).toContain('available + allocated') + } + }) + + it('rejects negative values', () => { + const balance = { + memberId: '550e8400-e29b-41d4-a716-446655440000', + total: 100, + available: -10, + allocated: 110, + updatedAt: new Date(), + } + + const result = memberRewardBalanceSchema.safeParse(balance) + expect(result.success).toBe(false) + }) +}) + +describe('badgeSchema', () => { + it('accepts valid badge', () => { + const badge = { + id: '550e8400-e29b-41d4-a716-446655440000', + name: 'First PR', + description: 'Merged your first pull request', + icon: '🎉', + criteria: 'Merge at least one pull request', + category: 'milestone', + } + + const result = badgeSchema.safeParse(badge) + expect(result.success).toBe(true) + }) + + it('rejects invalid category', () => { + const badge = { + id: '550e8400-e29b-41d4-a716-446655440000', + name: 'First PR', + description: 'Merged your first pull request', + icon: '🎉', + criteria: 'Merge at least one pull request', + category: 'invalid', + } + + const result = badgeSchema.safeParse(badge) + expect(result.success).toBe(false) + }) + + it('rejects short name', () => { + const badge = { + id: '550e8400-e29b-41d4-a716-446655440000', + name: 'AB', // Too short + description: 'Merged your first pull request', + icon: '🎉', + criteria: 'Merge at least one pull request', + category: 'milestone', + } + + const result = badgeSchema.safeParse(badge) + expect(result.success).toBe(false) + }) +}) + +describe('helper functions', () => { + describe('isValidEventType', () => { + it('returns true for valid types', () => { + expect(isValidEventType('pr_merged_small')).toBe(true) + expect(isValidEventType('docs_contribution')).toBe(true) + expect(isValidEventType('bug_fix')).toBe(true) + }) + + it('returns false for invalid types', () => { + expect(isValidEventType('invalid_type')).toBe(false) + expect(isValidEventType('')).toBe(false) + expect(isValidEventType('pr_merged')).toBe(false) + }) + }) + + describe('getSPWeight', () => { + it('returns correct weights', () => { + expect(getSPWeight('pr_merged_small')).toBe(5) + expect(getSPWeight('pr_merged_medium')).toBe(10) + expect(getSPWeight('pr_merged_large')).toBe(20) + expect(getSPWeight('docs_contribution')).toBe(8) + expect(getSPWeight('code_review')).toBe(3) + expect(getSPWeight('issue_triage')).toBe(2) + expect(getSPWeight('bug_fix')).toBe(15) + }) + }) + + describe('calculatePRSize', () => { + it('returns small for < 50 lines', () => { + expect(calculatePRSize(0)).toBe('small') + expect(calculatePRSize(25)).toBe('small') + expect(calculatePRSize(49)).toBe('small') + }) + + it('returns medium for 50-199 lines', () => { + expect(calculatePRSize(50)).toBe('medium') + expect(calculatePRSize(100)).toBe('medium') + expect(calculatePRSize(199)).toBe('medium') + }) + + it('returns large for >= 200 lines', () => { + expect(calculatePRSize(200)).toBe('large') + expect(calculatePRSize(500)).toBe('large') + expect(calculatePRSize(1000)).toBe('large') + }) + }) + + describe('generateDedupKey', () => { + it('generates key from PR context', () => { + const key = generateDedupKey('github', { + pr_number: 123, + repo: 'TogetherOS', + }) + expect(key).toBe('github::pr:123::repo:TogetherOS') + }) + + it('generates key from issue context', () => { + const key = generateDedupKey('github', { + issue_number: 456, + repo: 'TogetherOS', + }) + expect(key).toBe('github::issue:456::repo:TogetherOS') + }) + + it('generates key from source only', () => { + const key = generateDedupKey('manual', {}) + expect(key).toBe('manual') + }) + + it('generates consistent keys', () => { + const key1 = generateDedupKey('github', { + pr_number: 123, + repo: 'TogetherOS', + }) + const key2 = generateDedupKey('github', { + pr_number: 123, + repo: 'TogetherOS', + }) + expect(key1).toBe(key2) + }) + }) +}) diff --git a/packages/types/src/rewards.ts b/packages/types/src/rewards.ts new file mode 100644 index 00000000..6740e2f2 --- /dev/null +++ b/packages/types/src/rewards.ts @@ -0,0 +1,148 @@ +// packages/types/src/rewards.ts +// TogetherOS Rewards Module - Core Entity Definitions + +/** + * Event types that trigger Support Point rewards + */ +export type RewardEventType = + | 'pr_merged_small' // < 50 lines + | 'pr_merged_medium' // 50-200 lines + | 'pr_merged_large' // > 200 lines + | 'docs_contribution' // Documentation updates + | 'code_review' // PR review completed + | 'issue_triage' // Issue labeled/prioritized + | 'bug_fix' // Bug fix merged + +/** + * Domain-specific context for reward events + */ +export interface EventContext { + // GitHub-specific + pr_number?: number + issue_number?: number + repo?: string + lines_changed?: number + + // Generic metadata + [key: string]: string | number | boolean | undefined +} + +/** + * Core reward event entity + * Represents a single contribution that earns Support Points + */ +export interface RewardEvent { + /** Unique identifier (UUID v4) */ + id: string + + /** Member who earned the reward */ + memberId: string + + /** Type of contribution */ + event_type: RewardEventType + + /** Support Points awarded (calculated from event_type) */ + sp_weight: number + + /** Domain-specific context */ + context: EventContext + + /** Event source (e.g., 'github', 'manual') */ + source: string + + /** Deduplication key (source + context) */ + dedup_key: string + + /** When the event occurred */ + timestamp: Date + + /** Processing status */ + status: 'pending' | 'processed' | 'failed' + + /** When event was processed */ + processedAt?: Date +} + +/** + * Member's Support Points balance + */ +export interface MemberRewardBalance { + /** Member ID */ + memberId: string + + /** Total SP earned (all time) */ + total: number + + /** Available SP (not allocated to proposals) */ + available: number + + /** SP allocated to active proposals */ + allocated: number + + /** Last updated timestamp */ + updatedAt: Date +} + +/** + * Badge achievement definition + */ +export interface Badge { + /** Unique badge ID */ + id: string + + /** Display name */ + name: string + + /** Description of achievement */ + description: string + + /** Icon (emoji or URL) */ + icon: string + + /** Criteria to earn badge */ + criteria: string + + /** Badge category */ + category: 'contribution' | 'milestone' | 'special' +} + +/** + * Member's earned badges + */ +export interface MemberBadge { + /** Member ID */ + memberId: string + + /** Badge ID */ + badgeId: string + + /** When badge was earned */ + earnedAt: Date + + /** Related event ID (if applicable) */ + eventId?: string +} + +/** + * Input for creating a new reward event + */ +export interface CreateRewardEventInput { + memberId: string + event_type: RewardEventType + context: EventContext + source: string + timestamp?: Date +} + +/** + * SP weight mapping for event types + */ +export const SP_WEIGHTS: Record = { + pr_merged_small: 5, + pr_merged_medium: 10, + pr_merged_large: 20, + docs_contribution: 8, + code_review: 3, + issue_triage: 2, + bug_fix: 15, +} diff --git a/packages/validators/src/rewards.ts b/packages/validators/src/rewards.ts new file mode 100644 index 00000000..7d707a5f --- /dev/null +++ b/packages/validators/src/rewards.ts @@ -0,0 +1,152 @@ +// packages/validators/src/rewards.ts +// TogetherOS Rewards Module - Zod Validation Schemas + +import { z } from 'zod' +import type { RewardEventType, SP_WEIGHTS } from '@togetheros/types/rewards' + +/** + * Reward event type enum schema + */ +export const rewardEventTypeSchema = z.enum([ + 'pr_merged_small', + 'pr_merged_medium', + 'pr_merged_large', + 'docs_contribution', + 'code_review', + 'issue_triage', + 'bug_fix', +]) + +/** + * Event context schema + * Flexible object for domain-specific metadata + */ +export const eventContextSchema = z.object({ + pr_number: z.number().int().positive().optional(), + issue_number: z.number().int().positive().optional(), + repo: z.string().min(1).max(200).optional(), + lines_changed: z.number().int().nonnegative().optional(), +}).catchall(z.union([z.string(), z.number(), z.boolean()])) + +/** + * Create reward event input schema + * Validates input for creating new reward events + */ +export const createRewardEventSchema = z.object({ + memberId: z.string().uuid('Member ID must be a valid UUID'), + event_type: rewardEventTypeSchema, + context: eventContextSchema, + source: z.string().min(1).max(50), + timestamp: z.coerce.date().optional(), +}) + +/** + * Type inference from schema + */ +export type CreateRewardEventInput = z.infer + +/** + * Full reward event schema (including generated fields) + */ +export const rewardEventSchema = z.object({ + id: z.string().uuid(), + memberId: z.string().uuid(), + event_type: rewardEventTypeSchema, + sp_weight: z.number().int().positive(), + context: eventContextSchema, + source: z.string().min(1).max(50), + dedup_key: z.string().min(1), + timestamp: z.coerce.date(), + status: z.enum(['pending', 'processed', 'failed']), + processedAt: z.coerce.date().optional(), +}) + +/** + * Type inference from schema + */ +export type RewardEvent = z.infer + +/** + * Member reward balance schema + */ +export const memberRewardBalanceSchema = z.object({ + memberId: z.string().uuid(), + total: z.number().int().nonnegative(), + available: z.number().int().nonnegative(), + allocated: z.number().int().nonnegative(), + updatedAt: z.coerce.date(), +}).refine( + (data) => data.total === data.available + data.allocated, + { + message: 'Total SP must equal available + allocated', + path: ['total'], + } +) + +/** + * Badge schema + */ +export const badgeSchema = z.object({ + id: z.string().uuid(), + name: z.string().min(3).max(50), + description: z.string().min(10).max(500), + icon: z.string().min(1).max(200), + criteria: z.string().min(10).max(500), + category: z.enum(['contribution', 'milestone', 'special']), +}) + +/** + * Member badge schema + */ +export const memberBadgeSchema = z.object({ + memberId: z.string().uuid(), + badgeId: z.string().uuid(), + earnedAt: z.coerce.date(), + eventId: z.string().uuid().optional(), +}) + +/** + * Validation helper: Check if event type is valid + */ +export function isValidEventType(type: string): type is RewardEventType { + return rewardEventTypeSchema.safeParse(type).success +} + +/** + * Validation helper: Get SP weight for event type + */ +export function getSPWeight(eventType: RewardEventType): number { + const weights: Record = { + pr_merged_small: 5, + pr_merged_medium: 10, + pr_merged_large: 20, + docs_contribution: 8, + code_review: 3, + issue_triage: 2, + bug_fix: 15, + } + return weights[eventType] +} + +/** + * Validation helper: Calculate PR size category + */ +export function calculatePRSize(linesChanged: number): 'small' | 'medium' | 'large' { + if (linesChanged < 50) return 'small' + if (linesChanged < 200) return 'medium' + return 'large' +} + +/** + * Validation helper: Generate deduplication key + */ +export function generateDedupKey(source: string, context: Record): string { + // Create stable key from source + relevant context fields + const keyParts = [source] + + if (context.pr_number) keyParts.push(`pr:${context.pr_number}`) + if (context.issue_number) keyParts.push(`issue:${context.issue_number}`) + if (context.repo) keyParts.push(`repo:${context.repo}`) + + return keyParts.join('::') +} From 5f63b7d7a3fb6ba1b5bc18c8a7735b74898a6061 Mon Sep 17 00:00:00 2001 From: George Rodafinos Date: Sun, 26 Oct 2025 13:37:58 -0700 Subject: [PATCH 7/7] docs(rewards): restructure skill documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move reward-builder-skill.md → rewards-module/SKILL.md - Reorganize into module-based structure --- docs/skills/reward-builder-skill.md | 1170 --------------------------- docs/skills/rewards-module/SKILL.md | 532 ++++++++++++ 2 files changed, 532 insertions(+), 1170 deletions(-) delete mode 100644 docs/skills/reward-builder-skill.md create mode 100644 docs/skills/rewards-module/SKILL.md diff --git a/docs/skills/reward-builder-skill.md b/docs/skills/reward-builder-skill.md deleted file mode 100644 index fee0fdfd..00000000 --- a/docs/skills/reward-builder-skill.md +++ /dev/null @@ -1,1170 +0,0 @@ -# Reward Builder Skill - -## Purpose - -This skill helps maintainers and contributors build the TogetherOS Reward System module efficiently. It provides templates, patterns, and guidance for creating well-structured, contributor-friendly issues and implementing features that follow TogetherOS principles. - ---- - -## Target Users - -- **Maintainers:** Breaking down module into issues, reviewing PRs -- **Contributors:** First-time and experienced developers building features -- **Project Leads:** Planning sprints and prioritizing work - ---- - -## Skill Capabilities - -### 1. Issue Creation Templates -### 2. Code Implementation Patterns -### 3. Testing Strategies -### 4. PR Review Checklists -### 5. Documentation Standards - ---- - -## 1. Issue Creation Templates - -### Template: Entity Definition - -```markdown -## Title -Define [EntityName] Entity - -## Description -Create the core domain model for [entity purpose]. - -## Acceptance Criteria -- [ ] TypeScript interface defined in `packages/types/src/rewards.ts` -- [ ] All required fields documented with JSDoc comments -- [ ] Validation logic for field constraints -- [ ] Unit tests cover edge cases -- [ ] Type exports added to index.ts - -## Technical Details - -**File:** `packages/types/src/rewards.ts` - -**Interface Structure:** -\`\`\`typescript -interface [EntityName] { - id: string // UUID - [field]: [type] // [description] - // ... more fields -} -\`\`\` - -**Validation Rules:** -- [Rule 1] -- [Rule 2] - -## Related Files -- `packages/validators/src/rewards.ts` (Zod schemas) -- `docs/modules/rewards.md` (specification reference) - -## Size -`size:XS` (1-2 hours) - -## Labels -`good-first-issue`, `module:rewards`, `type:entity` - -## Help Available -Ask questions in Discussions #88 or tag @[maintainer] -``` - ---- - -### Template: Repository Implementation - -```markdown -## Title -Implement [RepositoryName] with In-Memory Storage - -## Description -Create repository interface and in-memory implementation for [entity]. - -## Acceptance Criteria -- [ ] Interface defined in `packages/rewards-domain/repos/[Name]Repo.ts` -- [ ] In-memory implementation in `InMemory[Name]Repo.ts` -- [ ] CRUD operations implemented (create, find, update, delete) -- [ ] Fixture seed data for testing -- [ ] Unit tests achieve 90%+ coverage -- [ ] Repository exports added to index.ts - -## Technical Details - -**Interface Methods:** -\`\`\`typescript -export interface [Name]Repo { - create(input: Create[Name]Input): Promise<[Name]> - findById(id: string): Promise<[Name] | null> - list(filters: [Name]Filters): Promise<[Name][]> - update(id: string, updates: Partial<[Name]>): Promise<[Name]> - delete(id: string): Promise -} -\`\`\` - -**In-Memory Implementation:** -- Use `Map` for storage -- Implement filtering logic -- Handle not-found cases gracefully - -## Files to Create -- `packages/rewards-domain/repos/[Name]Repo.ts` (interface) -- `packages/rewards-domain/repos/InMemory[Name]Repo.ts` (implementation) -- `packages/rewards-domain/repos/__tests__/[Name]Repo.test.ts` (tests) - -## Dependencies -- Requires: [EntityName] entity defined -- Blocks: API handlers for [entity] - -## Size -`size:S` (2-4 hours) - -## Labels -`module:rewards`, `type:repository` - -## Testing Guidance -See "Repository Testing Pattern" in this skill document. -``` - ---- - -### Template: API Endpoint - -```markdown -## Title -Create [METHOD] /api/rewards/[route] Endpoint - -## Description -Implement API endpoint for [action description]. - -## Acceptance Criteria -- [ ] Handler created in `apps/api/src/modules/rewards/handlers/[name].ts` -- [ ] Zod schema validates input -- [ ] Repository integration works -- [ ] Error handling covers all cases (401, 403, 422, 500) -- [ ] Contract tests pass -- [ ] API documented in rewards.md spec - -## Technical Details - -**Endpoint:** `[METHOD] /api/rewards/[route]` - -**Request Schema:** -\`\`\`typescript -const [name]Schema = z.object({ - // fields -}) -\`\`\` - -**Response (Success):** -\`\`\`typescript -[status] [Status Text] -{ - // response body -} -\`\`\` - -**Error Responses:** -- `401 Unauthorized` — Missing/invalid auth -- `403 Forbidden` — Insufficient permissions -- `422 Unprocessable Entity` — Validation failed -- `500 Internal Server Error` — Unexpected failure - -## Files to Create/Modify -- `apps/api/src/modules/rewards/handlers/[name].ts` -- `packages/validators/src/rewards.ts` (add schema) -- `apps/api/src/modules/rewards/handlers/__tests__/[name].test.ts` - -## Dependencies -- Requires: [Repository] implementation -- Requires: [Entity] definition - -## Size -`size:S` (2-4 hours) - -## Labels -`module:rewards`, `type:api-endpoint` - -## Testing Guidance -See "API Contract Testing Pattern" in this skill document. -``` - ---- - -### Template: GitHub Integration - -```markdown -## Title -Implement GitHub [EventType] Webhook Handler - -## Description -Handle [event type] webhooks from GitHub and create reward events. - -## Acceptance Criteria -- [ ] Webhook handler receives and validates GitHub payload -- [ ] Event data extracted correctly -- [ ] GitHub user mapped to TogetherOS member -- [ ] RewardEvent created with correct weight -- [ ] Signature verification implemented -- [ ] Deduplication prevents double-counting -- [ ] Integration tests pass with sample payloads - -## Technical Details - -**Webhook Event:** `[github_event_name]` - -**Payload Structure:** -\`\`\`typescript -interface [EventName]Payload { - // GitHub webhook payload fields -} -\`\`\` - -**Event Mapping:** -\`\`\`typescript -{ - event_type: '[reward_event_type]', - context: { - // extracted context - }, - weight: [calculated_weight] -} -\`\`\` - -**Weight Calculation:** -[Describe logic] - -## Files to Create/Modify -- `apps/api/src/modules/rewards/handlers/githubWebhook.ts` -- `apps/api/src/modules/rewards/lib/calculateWeight.ts` -- `apps/api/src/modules/rewards/__tests__/githubWebhook.test.ts` - -## Sample Payloads -Create test fixtures in `packages/rewards-fixtures/github/` - -## Size -`size:M` (4-6 hours) - -## Labels -`module:rewards`, `type:integration`, `priority:high` - -## Security Considerations -- Verify webhook signature using GitHub secret -- Validate payload structure before processing -- Rate limit webhook endpoint -``` - ---- - -### Template: UI Component - -```markdown -## Title -Create [ComponentName] UI Component - -## Description -Build [component purpose] for member profiles/dashboard. - -## Acceptance Criteria -- [ ] Component created in `packages/ui/src/rewards/[ComponentName].tsx` -- [ ] Props interface defined and documented -- [ ] All states handled (loading, empty, error, success) -- [ ] Accessible (keyboard nav, ARIA labels, screen readers) -- [ ] Storybook stories for all states -- [ ] Tailwind styling follows design system -- [ ] Responsive design (mobile, tablet, desktop) - -## Technical Details - -**Component API:** -\`\`\`typescript -interface [ComponentName]Props { - [prop]: [type] - // ... more props -} -\`\`\` - -**States to Handle:** -\`\`\`typescript -type ComponentState = - | { status: 'loading' } - | { status: 'empty' } - | { status: 'error'; error: Error } - | { status: 'success'; data: [Type] } -\`\`\` - -**Storybook Stories:** -- Default -- Loading -- Empty -- Error -- With Data (multiple scenarios) - -## Files to Create -- `packages/ui/src/rewards/[ComponentName].tsx` -- `packages/ui/src/rewards/[ComponentName].stories.tsx` -- `packages/ui/src/rewards/__tests__/[ComponentName].test.tsx` - -## Design Reference -[Link to design mockup if available] - -## Size -`size:M` (4-6 hours) - -## Labels -`module:rewards`, `type:ui-component` - -## Accessibility Checklist -See "UI Component Accessibility" section in this skill. -``` - ---- - -## 2. Code Implementation Patterns - -### Pattern: Entity Definition - -```typescript -// packages/types/src/rewards.ts - -/** - * Represents a contribution event that earns rewards. - * - * Events are immutable records of actions taken by members - * that contribute value to the cooperative. - */ -export interface RewardEvent { - /** Unique identifier (UUID v4) */ - id: string - - /** Member who performed the action */ - actor_id: string - - /** Type of contribution event */ - event_type: RewardEventType - - /** When the event occurred (ISO 8601) */ - timestamp: Date - - /** Domain-specific metadata */ - context: EventContext - - /** Origin system (github, forum, bridge) */ - source: string - - /** Support Points value */ - weight: number - - /** Processing status */ - status: 'pending' | 'processed' | 'rejected' - - /** When event was processed */ - processed_at?: Date -} - -/** - * Contribution event types across all cooperation domains. - */ -export type RewardEventType = - // Code & Infrastructure - | 'pr_merged' - | 'pr_reviewed' - | 'issue_created' - | 'issue_triaged' - | 'bug_fixed' - | 'docs_contribution' - // Add more as needed - -/** - * Domain-specific context for events. - * Structure varies by event_type. - */ -export interface EventContext { - // GitHub-specific - pr_number?: number - pr_size?: 'small' | 'medium' | 'large' - files_changed?: number - lines_changed?: number - repository?: string - - // Extensible for other domains - [key: string]: any -} -``` - -**Best Practices:** -- ✅ Use JSDoc comments for all interfaces and fields -- ✅ Define union types explicitly (no `string` for enums) -- ✅ Make optional fields explicit with `?` -- ✅ Use Date objects, not strings (convert on boundaries) -- ✅ Keep entities pure (no framework dependencies) - ---- - -### Pattern: Repository Interface - -```typescript -// packages/rewards-domain/repos/RewardEventRepo.ts - -import { RewardEvent, RewardEventType, EventContext } from '@togetheros/types' - -/** - * Repository interface for managing reward events. - * - * Implementations can use in-memory storage, databases, - * or external services while maintaining the same contract. - */ -export interface RewardEventRepo { - /** - * Create a new reward event. - * - * @throws {ValidationError} If input invalid - * @throws {DuplicateError} If event already exists - */ - create(input: CreateRewardEventInput): Promise - - /** - * Find event by unique ID. - * - * @returns Event if found, null otherwise - */ - findById(id: string): Promise - - /** - * List events for a specific member. - * - * @param memberId - Member UUID - * @param filters - Optional filtering criteria - * @returns Array of matching events - */ - findByMember(memberId: string, filters?: EventFilters): Promise - - /** - * Find all pending (unprocessed) events. - * - * Used by reward processing job to calculate balances. - */ - findPending(): Promise - - /** - * Mark event as processed. - * - * Called after Support Points calculated and awarded. - */ - markProcessed(id: string): Promise - - /** - * Check if event already exists. - * - * Prevents duplicate rewards for same action. - */ - checkDuplicate(source: string, context: EventContext): Promise -} - -/** - * Input for creating a new reward event. - */ -export interface CreateRewardEventInput { - actor_id: string - event_type: RewardEventType - source: string - context: EventContext -} - -/** - * Filters for querying events. - */ -export interface EventFilters { - event_types?: RewardEventType[] - date_range?: { start: Date; end: Date } - status?: 'pending' | 'processed' | 'rejected' - limit?: number -} -``` - -**Best Practices:** -- ✅ Define clear interface boundaries -- ✅ Use async/await (Promises) for all methods -- ✅ Document error conditions with @throws -- ✅ Keep methods focused (single responsibility) -- ✅ Use descriptive parameter names - ---- - -### Pattern: In-Memory Repository - -```typescript -// packages/rewards-domain/repos/InMemoryRewardEventRepo.ts - -import { RewardEvent, RewardEventType } from '@togetheros/types' -import { RewardEventRepo, CreateRewardEventInput, EventFilters } from './RewardEventRepo' -import { generateId } from '../lib/uuid' -import { calculateWeight } from '../lib/calculateWeight' - -/** - * In-memory implementation of RewardEventRepo. - * - * Used for testing and MVP phase before database integration. - * NOT suitable for production (data lost on restart). - */ -export class InMemoryRewardEventRepo implements RewardEventRepo { - private events: Map = new Map() - - async create(input: CreateRewardEventInput): Promise { - // Check for duplicates - const isDupe = await this.checkDuplicate(input.source, input.context) - if (isDupe) { - throw new Error('Event already exists') - } - - // Create event - const event: RewardEvent = { - id: generateId(), - actor_id: input.actor_id, - event_type: input.event_type, - timestamp: new Date(), - context: input.context, - source: input.source, - weight: calculateWeight(input.event_type, input.context), - status: 'pending', - } - - this.events.set(event.id, event) - return event - } - - async findById(id: string): Promise { - return this.events.get(id) || null - } - - async findByMember(memberId: string, filters?: EventFilters): Promise { - let results = Array.from(this.events.values()) - .filter(e => e.actor_id === memberId) - - // Apply filters - if (filters?.event_types) { - results = results.filter(e => filters.event_types!.includes(e.event_type)) - } - - if (filters?.date_range) { - results = results.filter(e => - e.timestamp >= filters.date_range!.start && - e.timestamp <= filters.date_range!.end - ) - } - - if (filters?.status) { - results = results.filter(e => e.status === filters.status) - } - - // Apply limit - const limit = filters?.limit || 100 - return results.slice(0, limit) - } - - async findPending(): Promise { - return Array.from(this.events.values()) - .filter(e => e.status === 'pending') - } - - async markProcessed(id: string): Promise { - const event = this.events.get(id) - if (event) { - event.status = 'processed' - event.processed_at = new Date() - } - } - - async checkDuplicate(source: string, context: EventContext): Promise { - // Simple duplicate check based on source + key context fields - const key = this.generateDuplicateKey(source, context) - - return Array.from(this.events.values()).some(e => - this.generateDuplicateKey(e.source, e.context) === key - ) - } - - private generateDuplicateKey(source: string, context: EventContext): string { - // Create unique key from source + relevant context - // Adjust based on event type - if (context.pr_number) { - return `${source}:pr:${context.pr_number}` - } - if (context.issue_number) { - return `${source}:issue:${context.issue_number}` - } - // Fallback: stringify entire context - return `${source}:${JSON.stringify(context)}` - } -} -``` - -**Best Practices:** -- ✅ Implement full interface (no partial implementations) -- ✅ Handle edge cases (nulls, empty arrays, not found) -- ✅ Add private helper methods for clarity -- ✅ Document limitations (e.g., "not for production") -- ✅ Use Map/Set for efficient lookups - ---- - -### Pattern: Zod Validation Schema - -```typescript -// packages/validators/src/rewards.ts - -import { z } from 'zod' - -/** - * Schema for creating a reward event via API. - */ -export const createRewardEventSchema = z.object({ - actor_id: z.string().uuid('Invalid member UUID'), - event_type: z.enum([ - 'pr_merged', - 'pr_reviewed', - 'issue_created', - 'issue_triaged', - 'bug_fixed', - 'docs_contribution', - ]), - source: z.string().min(1), - context: z.record(z.any()), // Flexible for different event types -}) - -export type CreateRewardEventInput = z.infer - -/** - * Schema for PR merge context. - */ -export const prMergeContextSchema = z.object({ - pr_number: z.number().int().positive(), - pr_size: z.enum(['small', 'medium', 'large']), - files_changed: z.number().int().nonnegative(), - lines_changed: z.number().int().nonnegative(), - repository: z.string(), -}) - -/** - * Schema for filtering events. - */ -export const eventFiltersSchema = z.object({ - event_types: z.array(z.string()).optional(), - date_range: z.object({ - start: z.coerce.date(), - end: z.coerce.date(), - }).optional(), - status: z.enum(['pending', 'processed', 'rejected']).optional(), - limit: z.number().int().positive().max(100).optional(), -}) - -export type EventFilters = z.infer -``` - -**Best Practices:** -- ✅ Use descriptive error messages -- ✅ Validate all inputs at API boundaries -- ✅ Use z.infer to generate TypeScript types -- ✅ Separate schemas for different contexts -- ✅ Set reasonable limits (max array size, string length) - ---- - -### Pattern: API Handler - -```typescript -// apps/api/src/modules/rewards/handlers/createEvent.ts - -import { createRewardEventSchema } from '@togetheros/validators' -import { RewardEventRepo } from '@togetheros/rewards-domain/repos' - -/** - * Handle POST /api/rewards/events - * - * Creates a new reward event from external systems. - */ -export async function createEvent( - request: Request, - repo: RewardEventRepo -): Promise { - try { - // Parse and validate input - const body = await request.json() - const data = createRewardEventSchema.parse(body) - - // Create event - const event = await repo.create(data) - - // Return success - return Response.json( - { - id: event.id, - weight: event.weight, - processed: event.status === 'processed', - }, - { status: 201 } - ) - } catch (error) { - // Handle validation errors - if (error instanceof z.ZodError) { - return Response.json( - { - error: { - code: 'VALIDATION_ERROR', - message: 'Invalid input', - details: error.errors, - } - }, - { status: 422 } - ) - } - - // Handle duplicate errors - if (error.message === 'Event already exists') { - return Response.json( - { - error: { - code: 'EVENT_ALREADY_PROCESSED', - message: 'Event with this source and context already exists', - } - }, - { status: 409 } - ) - } - - // Handle unexpected errors - console.error('Error creating reward event:', error) - return Response.json( - { - error: { - code: 'INTERNAL_ERROR', - message: 'An unexpected error occurred', - } - }, - { status: 500 } - ) - } -} -``` - -**Best Practices:** -- ✅ Validate input with Zod schemas -- ✅ Handle all error types explicitly -- ✅ Return appropriate HTTP status codes -- ✅ Use consistent error response format -- ✅ Log errors for debugging (never expose internals to client) - ---- - -## 3. Testing Strategies - -### Unit Test Pattern: Entity - -```typescript -// packages/rewards-domain/__tests__/RewardEvent.test.ts - -import { describe, it, expect } from 'vitest' -import { createRewardEvent } from '../lib/createRewardEvent' - -describe('RewardEvent', () => { - it('creates event with valid input', () => { - const event = createRewardEvent({ - actor_id: 'member-123', - event_type: 'pr_merged', - source: 'github', - context: { pr_number: 42 } - }) - - expect(event.id).toBeDefined() - expect(event.actor_id).toBe('member-123') - expect(event.status).toBe('pending') - }) - - it('calculates weight for small PR', () => { - const event = createRewardEvent({ - event_type: 'pr_merged', - context: { pr_size: 'small' } - }) - - expect(event.weight).toBe(5) - }) - - it('calculates weight for medium PR', () => { - const event = createRewardEvent({ - event_type: 'pr_merged', - context: { pr_size: 'medium' } - }) - - expect(event.weight).toBe(10) - }) - - it('throws on invalid actor_id', () => { - expect(() => createRewardEvent({ - actor_id: 'not-a-uuid', - event_type: 'pr_merged' - })).toThrow('Invalid member UUID') - }) -}) -``` - ---- - -### Unit Test Pattern: Repository - -```typescript -// packages/rewards-domain/repos/__tests__/RewardEventRepo.test.ts - -import { describe, it, expect, beforeEach } from 'vitest' -import { InMemoryRewardEventRepo } from '../InMemoryRewardEventRepo' - -describe('RewardEventRepo', () => { - let repo: InMemoryRewardEventRepo - - beforeEach(() => { - repo = new InMemoryRewardEventRepo() - }) - - describe('create', () => { - it('creates event successfully', async () => { - const event = await repo.create({ - actor_id: 'member-123', - event_type: 'pr_merged', - source: 'github', - context: { pr_number: 42 } - }) - - expect(event.id).toBeDefined() - expect(event.actor_id).toBe('member-123') - }) - - it('prevents duplicate events', async () => { - await repo.create({ - source: 'github', - context: { pr_number: 42 } - }) - - await expect(repo.create({ - source: 'github', - context: { pr_number: 42 } - })).rejects.toThrow('Event already exists') - }) - }) - - describe('findByMember', () => { - it('returns all events for member', async () => { - await repo.create({ actor_id: 'member-123', ... }) - await repo.create({ actor_id: 'member-123', ... }) - await repo.create({ actor_id: 'member-456', ... }) - - const events = await repo.findByMember('member-123') - expect(events).toHaveLength(2) - }) - - it('filters by event type', async () => { - await repo.create({ event_type: 'pr_merged', ... }) - await repo.create({ event_type: 'pr_reviewed', ... }) - - const events = await repo.findByMember('member-123', { - event_types: ['pr_merged'] - }) - expect(events).toHaveLength(1) - expect(events[0].event_type).toBe('pr_merged') - }) - - it('respects limit', async () => { - for (let i = 0; i < 10; i++) { - await repo.create({ actor_id: 'member-123', ... }) - } - - const events = await repo.findByMember('member-123', { limit: 5 }) - expect(events).toHaveLength(5) - }) - }) -}) -``` - ---- - -### Contract Test Pattern: API - -```typescript -// apps/api/src/modules/rewards/__tests__/createEvent.test.ts - -import { describe, it, expect } from 'vitest' -import { createRewardEventSchema } from '@togetheros/validators' - -describe('POST /api/rewards/events', () => { - describe('input validation', () => { - it('accepts valid input', () => { - const result = createRewardEventSchema.safeParse({ - actor_id: '550e8400-e29b-41d4-a716-446655440000', - event_type: 'pr_merged', - source: 'github', - context: { pr_number: 42 } - }) - - expect(result.success).toBe(true) - }) - - it('rejects invalid actor_id', () => { - const result = createRewardEventSchema.safeParse({ - actor_id: 'not-a-uuid', - event_type: 'pr_merged', - source: 'github', - context: {} - }) - - expect(result.success).toBe(false) - }) - - it('rejects unknown event_type', () => { - const result = createRewardEventSchema.safeParse({ - actor_id: '550e8400-e29b-41d4-a716-446655440000', - event_type: 'unknown_type', - source: 'github', - context: {} - }) - - expect(result.success).toBe(false) - }) - }) -}) -``` - ---- - -## 4. PR Review Checklist - -### For Reviewers - -**Code Quality:** -- [ ] Follows TogetherOS code style and patterns -- [ ] No unnecessary complexity or premature optimization -- [ ] Functions are small and focused (single responsibility) -- [ ] Variable names are descriptive and clear -- [ ] Comments explain "why", not "what" - -**Testing:** -- [ ] Unit tests cover all code paths -- [ ] Contract tests validate API schemas -- [ ] Edge cases are tested (nulls, empty arrays, errors) -- [ ] Test coverage is >80% (aim for 90%+) - -**Documentation:** -- [ ] JSDoc comments on all exported functions/interfaces -- [ ] README updated if public API changed -- [ ] Module spec updated if behavior changed - -**TogetherOS Principles:** -- [ ] One tiny change per PR (smallest shippable increment) -- [ ] Docs-first: spec matches implementation -- [ ] Privacy-first: no PII exposure, IP hashing if needed -- [ ] Validation: Zod schemas validate all inputs - -**CI/CD:** -- [ ] PR includes proof lines in description -- [ ] All CI checks pass (ci/lint, ci/docs, ci/smoke) -- [ ] No linting errors or warnings -- [ ] Branch targets `Claude-1st-build` (not main) - -**Path Labels:** -- [ ] PR tagged with correct Cooperation Path -- [ ] Keywords listed in PR description - -**Git Hygiene:** -- [ ] Commit messages follow convention (type(scope): message) -- [ ] No merge commits (rebase preferred) -- [ ] Single focused change (not multiple unrelated changes) - ---- - -## 5. Documentation Standards - -### Module Spec Format - -Every module needs a comprehensive spec in `docs/modules/[module].md`: - -**Required Sections:** -1. **Overview** — Purpose, status, priority -2. **Why This Exists** — Problem/solution, outcomes -3. **Core Principles** — Non-negotiables -4. **Implementation Sequence** — Phases A/B/C/D -5. **Data Models** — Complete entity specifications -6. **API Contracts** — Request/response schemas -7. **UI Components** — Component specs (if applicable) -8. **Repository Pattern** — Interface + implementation guide -9. **Testing Strategy** — Unit/contract/integration patterns -10. **Definition of Done** — Acceptance checklist -11. **Contributing** — How developers can help -12. **Related KB Files** — Links to dependencies - ---- - -### JSDoc Standards - -```typescript -/** - * Brief one-line description of what this does. - * - * More detailed explanation if needed. Explain why this exists, - * what problem it solves, and any important constraints. - * - * @param paramName - Description of parameter - * @param optionalParam - Optional parameter description - * @returns Description of return value - * @throws {ErrorType} When this error occurs - * - * @example - * ```typescript - * const result = functionName('input') - * console.log(result) // Expected output - * ``` - */ -export function functionName( - paramName: string, - optionalParam?: number -): ReturnType { - // Implementation -} -``` - ---- - -### Inline Comment Guidelines - -**DO comment:** -- Why a specific approach was chosen -- Business logic or domain rules -- Complex algorithms or calculations -- Workarounds for known issues -- TODOs with context - -**DON'T comment:** -- What the code does (code should be self-documenting) -- Obvious operations -- Auto-generated comments - -**Examples:** - -✅ Good: -```typescript -// Use SHA-256 for deduplication to balance privacy and uniqueness -const key = createHash('sha256').update(data).digest('hex') - -// Cooldown prevents spam: max 5 PRs/day counted toward rewards -if (prCountToday >= 5) return -``` - -❌ Bad: -```typescript -// Create a hash -const key = createHash('sha256').update(data).digest('hex') - -// Check if greater than 5 -if (prCountToday >= 5) return -``` - ---- - -## Skill Usage Examples - -### Example 1: Creating Entity Definition Issue - -**Maintainer Task:** Break down "Event Model" into actionable issue - -**Use Skill:** -1. Open "1. Issue Creation Templates" → "Template: Entity Definition" -2. Fill in placeholders: - - `[EntityName]` → `RewardEvent` - - `[entity purpose]` → `contribution events that earn rewards` -3. Copy template to GitHub Issues -4. Add labels: `good-first-issue`, `module:rewards`, `type:entity`, `size:XS` -5. Assign to project board - -**Result:** Clear, actionable issue ready for contributor pickup - ---- - -### Example 2: Implementing Repository - -**Contributor Task:** Implement InMemoryRewardEventRepo - -**Use Skill:** -1. Read "2. Code Implementation Patterns" → "Pattern: Repository Interface" -2. Copy interface boilerplate -3. Read "Pattern: In-Memory Repository" -4. Implement methods following pattern -5. Read "3. Testing Strategies" → "Unit Test Pattern: Repository" -6. Write tests matching pattern -7. Submit PR with proof lines - -**Result:** High-quality implementation matching TogetherOS standards - ---- - -### Example 3: Reviewing PR - -**Maintainer Task:** Review PR for reward event creation - -**Use Skill:** -1. Open "4. PR Review Checklist" -2. Go through each section systematically -3. Leave specific feedback referencing patterns -4. If issues found, link to relevant skill sections -5. Approve when all boxes checked - -**Result:** Thorough, constructive review ensuring quality - ---- - -## Maintenance & Updates - -### When to Update This Skill - -- New issue type identified (add template) -- Code pattern evolves (update example) -- Test strategy improves (add new pattern) -- PR review catches common issue (add to checklist) -- Documentation standard changes (update guidelines) - -### Update Process - -1. Identify improvement needed -2. Update relevant section -3. Add example if helpful -4. Test with real issue/PR -5. Commit with clear message - ---- - -## Success Metrics - -**For Maintainers:** -- Time to create issue reduced from 20min → 5min -- Issue quality consistent across all created -- Fewer "what should I do?" questions - -**For Contributors:** -- First-time contributors ship PRs faster -- Code reviews have fewer rounds -- Tests follow standard patterns -- Documentation complete on first submission - -**For Project:** -- More contributors able to participate -- Higher quality contributions -- Faster feature delivery -- Better maintainability - ---- - -## Related Documentation - -- [Rewards Module Spec](../docs/modules/rewards.md) — Complete technical specification -- [Main KB](../docs/togetheros-kb.md) — Core principles and workflow -- [CI/CD Discipline](../docs/ci-cd-discipline.md) — Validation and proof lines -- [Architecture](../docs/architecture.md) — Domain-driven design patterns -- [Cooperation Paths](../docs/cooperation-paths.md) — All 8 contribution domains diff --git a/docs/skills/rewards-module/SKILL.md b/docs/skills/rewards-module/SKILL.md new file mode 100644 index 00000000..9f27217a --- /dev/null +++ b/docs/skills/rewards-module/SKILL.md @@ -0,0 +1,532 @@ +--- +name: rewards-module-builder +description: Automates development of TogetherOS Rewards module features. Use when building reward types, implementing validation, creating UI components, writing tests, or updating Rewards documentation. Handles end-to-end implementation from entity models through API handlers to frontend components. +--- + +# Rewards Module Builder + +This skill automates development of the TogetherOS Rewards module, a gamification system that recognizes contributions through badges, skill trees, and visual progression. + +## When to Use This Skill + +Use this skill when: +- Creating new reward types or badges +- Implementing reward validation logic +- Building reward UI components +- Writing tests for reward functionality +- Updating Rewards module documentation +- Connecting rewards to member actions + +## Core Concepts + +### Reward Types + +TogetherOS supports four reward categories: + +1. **Badges** - Achievement markers (e.g., "First PR Merged", "10 Mutual Aids") +2. **Skill Tree Nodes** - Path-specific progression (Builder, Community Heart, etc.) +3. **Visual States** - Member progression visualization (seed → seedling → young tree → majestic tree) +4. **Capability Unlocks** - Feature access gates (e.g., create proposals, organize events) + +### Domain-Driven Architecture + +Rewards module follows TogetherOS's standard domain-driven pattern: + +``` +apps/api/src/modules/rewards/ +├── entities/ # Domain models (Badge, SkillNode, etc.) +├── repos/ # Data access interfaces + in-memory implementations +├── handlers/ # API handlers (create, award, list) +└── fixtures/ # Test data + +apps/web/app/(platform)/profiles/[handle]/rewards/ +├── page.tsx # Member rewards view +└── components/ # Reward display components + +packages/types/src/rewards.ts # TypeScript interfaces +packages/validators/src/rewards.ts # Zod schemas +packages/ui/src/rewards/ # Shared reward components +``` + +## Implementation Workflow + +### 1. Define the Reward + +Start with clear specifications: +- **What triggers it?** (e.g., "merge 10 PRs") +- **What does it unlock?** (capabilities, recognition) +- **Which path?** (Builder, Community Heart, etc.) +- **Visual representation?** (icon, color, animation) + +### 2. Create Entity Model + +```typescript +// apps/api/src/modules/rewards/entities/Badge.ts +export class Badge { + constructor( + public id: string, + public name: string, + public description: string, + public icon: string, + public path: 'builder' | 'community_heart' | 'guided_contributor' | 'steady_cultivator', + public criteria: BadgeCriteria, + public createdAt: Date + ) {} + + static create(input: CreateBadgeInput): Badge { + // Validation logic + if (input.name.length < 3) { + throw new Error('Badge name must be at least 3 characters') + } + + return new Badge( + generateId(), + input.name, + input.description, + input.icon, + input.path, + input.criteria, + new Date() + ) + } + + canAward(memberActivity: MemberActivity): boolean { + // Check if criteria met + return this.criteria.check(memberActivity) + } +} +``` + +### 3. Implement Repository + +```typescript +// apps/api/src/modules/rewards/repos/BadgeRepo.ts +export interface BadgeRepo { + create(input: CreateBadgeInput): Promise + findById(id: string): Promise + listByPath(path: string): Promise + award(badgeId: string, memberId: string): Promise + getMemberBadges(memberId: string): Promise +} + +// apps/api/src/modules/rewards/repos/InMemoryBadgeRepo.ts +export class InMemoryBadgeRepo implements BadgeRepo { + private badges = new Map() + private awards = new Map() // memberId -> badgeIds + + async create(input: CreateBadgeInput): Promise { + const badge = Badge.create(input) + this.badges.set(badge.id, badge) + return badge + } + + async award(badgeId: string, memberId: string): Promise { + const badge = await this.findById(badgeId) + if (!badge) throw new Error('Badge not found') + + const memberBadges = this.awards.get(memberId) || [] + if (!memberBadges.includes(badgeId)) { + memberBadges.push(badgeId) + this.awards.set(memberId, memberBadges) + } + } + + async getMemberBadges(memberId: string): Promise { + const badgeIds = this.awards.get(memberId) || [] + return Promise.all( + badgeIds.map(id => this.findById(id)).filter(b => b !== null) + ) + } +} +``` + +### 4. Create Zod Schemas + +```typescript +// packages/validators/src/rewards.ts +import { z } from 'zod' + +export const createBadgeSchema = z.object({ + name: z.string().min(3).max(50), + description: z.string().min(10).max(200), + icon: z.string().emoji().or(z.string().url()), + path: z.enum(['builder', 'community_heart', 'guided_contributor', 'steady_cultivator']), + criteria: z.object({ + type: z.enum(['pr_count', 'mutual_aid_count', 'event_organized', 'custom']), + threshold: z.number().positive(), + timeframe: z.enum(['all_time', 'monthly', 'yearly']).optional(), + }), +}) + +export const awardBadgeSchema = z.object({ + badgeId: z.string().uuid(), + memberId: z.string().uuid(), + reason: z.string().min(10).max(200), +}) +``` + +### 5. Implement API Handler + +```typescript +// apps/api/src/modules/rewards/handlers/awardBadge.ts +import { awardBadgeSchema } from '@togetheros/validators' +import { BadgeRepo } from '../repos' + +export async function awardBadge( + input: unknown, + repo: BadgeRepo +) { + const data = awardBadgeSchema.parse(input) + + // Check if badge exists + const badge = await repo.findById(data.badgeId) + if (!badge) { + return { error: { code: 'BADGE_NOT_FOUND', message: 'Badge does not exist' } } + } + + // Award badge + await repo.award(data.badgeId, data.memberId) + + // Log transaction (append-only NDJSON) + await logRewardTransaction({ + type: 'badge_awarded', + badgeId: data.badgeId, + memberId: data.memberId, + reason: data.reason, + timestamp: new Date().toISOString(), + }) + + return { success: true } +} +``` + +### 6. Build UI Component + +```typescript +// packages/ui/src/rewards/BadgeCard.tsx +import { Badge } from '@togetheros/types' + +interface BadgeCardProps { + badge: Badge + earnedAt?: Date + locked?: boolean +} + +export function BadgeCard({ badge, earnedAt, locked = false }: BadgeCardProps) { + return ( +
+ {/* Icon */} +
{badge.icon}
+ + {/* Name */} +

{badge.name}

+ + {/* Description */} +

{badge.description}

+ + {/* Earned date */} + {earnedAt && ( +

+ Earned {earnedAt.toLocaleDateString()} +

+ )} + + {/* Locked overlay */} + {locked && ( +
+ 🔒 +
+ )} +
+ ) +} +``` + +### 7. Write Tests + +```typescript +// apps/api/src/modules/rewards/__tests__/awardBadge.test.ts +import { describe, it, expect, beforeEach } from 'vitest' +import { InMemoryBadgeRepo } from '../repos/InMemoryBadgeRepo' +import { awardBadge } from '../handlers/awardBadge' + +describe('awardBadge', () => { + let repo: InMemoryBadgeRepo + + beforeEach(() => { + repo = new InMemoryBadgeRepo() + }) + + it('awards badge to member', async () => { + // Setup + const badge = await repo.create({ + name: 'First PR', + description: 'Merged your first PR', + icon: '🎉', + path: 'builder', + criteria: { type: 'pr_count', threshold: 1 }, + }) + + // Execute + const result = await awardBadge({ + badgeId: badge.id, + memberId: 'member-123', + reason: 'PR #42 merged', + }, repo) + + // Assert + expect(result.success).toBe(true) + + const memberBadges = await repo.getMemberBadges('member-123') + expect(memberBadges).toHaveLength(1) + expect(memberBadges[0].id).toBe(badge.id) + }) + + it('returns error for non-existent badge', async () => { + const result = await awardBadge({ + badgeId: 'fake-id', + memberId: 'member-123', + reason: 'Test', + }, repo) + + expect(result.error?.code).toBe('BADGE_NOT_FOUND') + }) + + it('prevents duplicate badge awards', async () => { + const badge = await repo.create({ + name: 'First PR', + description: 'Merged your first PR', + icon: '🎉', + path: 'builder', + criteria: { type: 'pr_count', threshold: 1 }, + }) + + // Award twice + await awardBadge({ badgeId: badge.id, memberId: 'member-123', reason: 'First' }, repo) + await awardBadge({ badgeId: badge.id, memberId: 'member-123', reason: 'Second' }, repo) + + // Should only have one + const memberBadges = await repo.getMemberBadges('member-123') + expect(memberBadges).toHaveLength(1) + }) +}) +``` + +### 8. Create Fixtures + +```typescript +// packages/fixtures/src/badges.ts +export const badgeFixtures = [ + { + id: 'badge-first-pr', + name: 'First PR Merged', + description: 'Congratulations on your first merged pull request!', + icon: '🎉', + path: 'builder' as const, + criteria: { type: 'pr_count' as const, threshold: 1 }, + }, + { + id: 'badge-10-prs', + name: '10 PRs Strong', + description: 'You've merged 10 pull requests. Impressive!', + icon: '💪', + path: 'builder' as const, + criteria: { type: 'pr_count' as const, threshold: 10 }, + }, + { + id: 'badge-first-mutual-aid', + name: 'Helping Hand', + description: 'Completed your first mutual aid transaction', + icon: '🤝', + path: 'community_heart' as const, + criteria: { type: 'mutual_aid_count' as const, threshold: 1 }, + }, +] +``` + +## Transaction Logging + +All reward awards must be logged to NDJSON: + +```typescript +// Log format +{ + "id": "uuid", + "timestamp": "2025-01-15T10:30:00Z", + "event_type": "reward_awarded", + "metadata": { + "reward_type": "badge", + "reward_id": "badge-first-pr", + "member_id": "member-123", + "reason": "PR #42 merged", + "member_handle": "alice_organizer" + } +} +``` + +Store logs in: `logs/rewards/transactions-YYYY-MM-DD.ndjson` + +## Visual Progression System + +Members progress through visual states based on contributions: + +```typescript +export type VisualState = 'seed' | 'seedling' | 'young_tree' | 'majestic_tree' + +export function calculateVisualState(contributionScore: number): VisualState { + if (contributionScore < 10) return 'seed' + if (contributionScore < 50) return 'seedling' + if (contributionScore < 200) return 'young_tree' + return 'majestic_tree' +} + +export function getContributionScore(member: Member): number { + let score = 0 + + // PR contributions + score += member.prsMerged * 5 + + // Mutual aid + score += member.mutualAidTransactions * 3 + + // Proposals created + score += member.proposalsCreated * 10 + + // Events organized + score += member.eventsOrganized * 15 + + return score +} +``` + +## Capability Unlocks + +Rewards can unlock new features: + +```typescript +export interface CapabilityUnlock { + capability: 'create_proposal' | 'organize_event' | 'moderate' | 'steward' + requirements: { + badges?: string[] + contributionScore?: number + paths?: string[] + } +} + +export function checkCapability( + member: Member, + capability: string +): boolean { + const unlock = CAPABILITY_UNLOCKS[capability] + if (!unlock) return false + + // Check badges + if (unlock.requirements.badges) { + const hasBadges = unlock.requirements.badges.every(badgeId => + member.badges.some(b => b.id === badgeId) + ) + if (!hasBadges) return false + } + + // Check contribution score + if (unlock.requirements.contributionScore) { + const score = getContributionScore(member) + if (score < unlock.requirements.contributionScore) return false + } + + // Check paths + if (unlock.requirements.paths) { + const hasPath = unlock.requirements.paths.some(path => + member.archetypes.includes(path) + ) + if (!hasPath) return false + } + + return true +} +``` + +## Documentation Updates + +After implementing a reward, update: + +1. **Module spec**: `docs/modules/rewards.md` - Add reward type to list +2. **Data models**: `packages/types/src/rewards.ts` - Export new interfaces +3. **Fixtures**: `packages/fixtures/src/badges.ts` - Add example data +4. **STATUS**: `docs/STATUS_v2.md` - Bump progress marker + +## Common Patterns + +### Auto-Award on Activity + +```typescript +// In governance handler after PR merge +export async function handlePRMerge(prId: string, memberId: string) { + // ... merge logic ... + + // Check for badge eligibility + const member = await memberRepo.findById(memberId) + const prCount = await getPRCount(memberId) + + if (prCount === 1) { + await awardBadge({ + badgeId: 'badge-first-pr', + memberId, + reason: `PR #${prId} merged`, + }, badgeRepo) + } +} +``` + +### Display Badge Progress + +```typescript +export function BadgeProgress({ badge, member }: Props) { + const progress = calculateProgress(badge, member) + + return ( +
+ +
+
+
+
+

+ {progress}% complete +

+
+
+ ) +} +``` + +## Validation Checklist + +Before submitting PR: + +- [ ] Entity model includes validation logic +- [ ] Repository interface defined with in-memory implementation +- [ ] Zod schemas created with proper constraints +- [ ] API handler validates input and handles errors +- [ ] UI component handles all states (loading, empty, error, success) +- [ ] Unit tests cover happy path and error cases +- [ ] Fixture data added for testing +- [ ] NDJSON transaction logging implemented +- [ ] Documentation updated (module spec, data models, STATUS) +- [ ] `./scripts/validate.sh` passes with: + - `LINT=OK` + - `VALIDATORS=GREEN` + - `SMOKE=OK` + +## References + +For detailed patterns and examples, see: +- **Reward Builder Guide**: `docs/dev/reward-module-guide.md` - Comprehensive templates and workflows +- **Data Models**: `packages/types/src/rewards.ts` - Complete type definitions +- **Social Economy**: Knowledge base document on gamification and progression systems