Skip to content

Commit cf155ce

Browse files
done: frontend building
1 parent 40b28dd commit cf155ce

72 files changed

Lines changed: 16356 additions & 42 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
web:
10+
name: Web (typecheck, lint, unit, build)
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: pnpm/action-setup@v4
15+
- uses: actions/setup-node@v4
16+
with:
17+
node-version: 22
18+
cache: pnpm
19+
- run: pnpm install --frozen-lockfile
20+
- run: pnpm typecheck
21+
- run: pnpm lint
22+
- run: pnpm test
23+
- run: pnpm build
24+
env:
25+
NEXT_PUBLIC_CHAIN_MODE: mock
26+
# Lighthouse budget check (challenge page) — runs against the prod build
27+
- name: Lighthouse budgets
28+
run: |
29+
pnpm --filter @grudge/web exec sh -c "npx --yes start-server-and-test 'next start -p 3100' http://localhost:3100/challenge/1 'npx --yes @lhci/cli@0.14.x autorun --collect.url=http://localhost:3100/challenge/1 --collect.numberOfRuns=1 --collect.settings.preset=desktop --assert.assertions.largest-contentful-paint=error:2000 --assert.assertions.cumulative-layout-shift=error:0.02 --assert.assertions.total-blocking-time=error:200'"
30+
continue-on-error: true # budget is tracked; flaky CI hardware must not block merges silently — see job summary
31+
32+
contracts:
33+
name: Contracts (ruff, mypy, genvm_lint, gltest)
34+
runs-on: ubuntu-latest
35+
defaults:
36+
run:
37+
working-directory: contracts
38+
steps:
39+
- uses: actions/checkout@v4
40+
- uses: actions/setup-python@v5
41+
with:
42+
python-version: "3.12"
43+
- run: pip install --quiet ruff mypy pytest genvm-linter pyright
44+
- run: ruff check . --config pyproject.toml
45+
- run: ruff format --check .
46+
- run: MYPYPATH=stubs mypy grudge.py --strict --config-file pyproject.toml
47+
- run: python scripts/genvm_lint.py grudge.py
48+
# official GenLayer linter: AST safety checks + SDK semantic validation + pyright
49+
- run: genvm-lint check grudge.py --json
50+
- run: genvm-lint typecheck grudge.py --json
51+
- run: python -m pytest tests/test_settle_math.py -q
52+
# gltest needs a reachable GenLayer Studio; provide STUDIO_URL secret to enable
53+
- name: gltest vs studionet
54+
if: ${{ vars.GLTEST_ENABLED == 'true' }}
55+
run: |
56+
pip install --quiet gltest
57+
gltest --network studionet
58+
59+
e2e:
60+
name: E2E (Playwright, mock mode)
61+
runs-on: ubuntu-latest
62+
steps:
63+
- uses: actions/checkout@v4
64+
- uses: pnpm/action-setup@v4
65+
- uses: actions/setup-node@v4
66+
with:
67+
node-version: 22
68+
cache: pnpm
69+
- run: pnpm install --frozen-lockfile
70+
- run: pnpm --filter @grudge/web exec playwright install --with-deps chromium
71+
- run: pnpm e2e
72+
env:
73+
NEXT_PUBLIC_CHAIN_MODE: mock

.gitignore

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# ── dependencies ─────────────────────────────────────────────────────────────
2+
node_modules/
3+
.pnpm-store/
4+
.pnp.*
5+
6+
# ── next.js / web build ──────────────────────────────────────────────────────
7+
.next/
8+
out/
9+
next-env.d.ts
10+
*.tsbuildinfo
11+
.vercel/
12+
.turbo/
13+
14+
# ── env & secrets (templates like .env.example are deliberately ignored too) ─
15+
.env
16+
.env.local
17+
.env*.local
18+
.env.development
19+
.env.production
20+
.env.example
21+
*.pem
22+
*.key
23+
24+
# ── tests & reports ──────────────────────────────────────────────────────────
25+
coverage/
26+
playwright-report/
27+
test-results/
28+
blob-report/
29+
.lighthouseci/
30+
.nyc_output/
31+
32+
# ── python / contracts toolchain ─────────────────────────────────────────────
33+
__pycache__/
34+
*.py[cod]
35+
*.egg-info/
36+
.venv/
37+
venv/
38+
.ruff_cache/
39+
.mypy_cache/
40+
.pytest_cache/
41+
.pyright/
42+
43+
# ── genlayer artifacts ───────────────────────────────────────────────────────
44+
contracts/artifacts/
45+
contracts/.deploy.log
46+
.genlayer/
47+
48+
# ── editors & IDEs ───────────────────────────────────────────────────────────
49+
.idea/
50+
.vscode/*
51+
!.vscode/settings.json
52+
!.vscode/extensions.json
53+
*.swp
54+
*.swo
55+
56+
# ── OS junk ──────────────────────────────────────────────────────────────────
57+
.DS_Store
58+
.DS_Store?
59+
._*
60+
Thumbs.db
61+
Desktop.ini
62+
63+
# ── logs & scratch ───────────────────────────────────────────────────────────
64+
*.log
65+
logs/
66+
npm-debug.log*
67+
pnpm-debug.log*
68+
*.tmp
69+
*.tmp.*
70+
shot.tmp.mjs
71+
probe.tmp.mjs

.husky/pre-commit

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pnpm --filter @grudge/web lint && pnpm --filter @grudge/web typecheck

README.md

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,85 @@
1-
# Grudge
1+
# GRUDGE — *your friends bet you'll fail.*
2+
3+
A social accountability game on **GenLayer**. You stake GEN on a public real-world promise
4+
("I will run 5km every day for 30 days"). Friends stake **for** (believers) or **against**
5+
(doubters) you — with public taunts. You submit evidence; **validator LLMs reach consensus**:
6+
`VERIFIED / SUSPICIOUS / REJECTED`. At the deadline the contract settles: keep the promise and
7+
take the doubters' pool, or fold and mint their "Called It" receipts. Every doubt recorded,
8+
every receipt public.
9+
10+
## Why this is impossible without GenLayer
11+
12+
The referee is a *subjective judgment* — "does this evidence prove the promise?" — that no
13+
deterministic EVM contract or single oracle can make trustlessly. GenLayer's Intelligent
14+
Contracts run that judgment inside consensus: `gl.eq_principle_prompt_comparative` has the
15+
validator set each run the judging prompt and agree on the verdict enum, so the outcome is a
16+
consensus artifact, not one model's opinion. Injection attempts in evidence are adjudicated
17+
*by the same consensus* (auto-REJECTED).
18+
19+
## Quickstart (zero config)
20+
21+
```sh
22+
pnpm i && pnpm dev # http://localhost:3000 — mock chain, seeded grudges
23+
```
24+
25+
Full loop playable immediately: landing → challenge → stake (flying chip) → evidence
26+
(validator arc) → verdict (stamp) → settle (ticket tear / receipts deal-out).
27+
`/dev/components` is the motion-design gallery.
28+
29+
## Architecture
30+
31+
```
32+
apps/web Next.js 15 App Router, TS strict, Tailwind, Framer Motion + GSAP/Lenis
33+
lib/chain ONE adapter interface (GrudgeClient): mock.ts ⇄ genlayer.ts (genlayer-js)
34+
lib/motion motion tokens + shared variants (single reduced-motion gate)
35+
lib/psychology pure, unit-tested copy engine (odds lines, nudges, captions)
36+
app/api/judge mock judge proxy — SAME prompt as the contract (LLM if keyed, heuristic if not)
37+
contracts/grudge.py GenVM Intelligent Contract (screening, judging, dispute, settle, claim)
38+
```
39+
40+
Chain is the source of truth; the web app holds no authoritative state.
41+
42+
## Real chain: Testnet Bradbury
43+
44+
```sh
45+
npm i -g genlayer-cli # GenLayer CLI
46+
genlayer network testnet-bradbury # switch network
47+
# fund your account with testnet GEN via the GenLayer faucet
48+
make -C contracts deploy # deploys grudge.py, writes address to
49+
# apps/web/.env.local + contracts/deployments.json
50+
```
51+
52+
Then set in `apps/web/.env.local` (values from `genlayer network info` — never hardcoded):
53+
54+
```
55+
NEXT_PUBLIC_CHAIN_MODE=genlayer
56+
NEXT_PUBLIC_BRADBURY_CHAIN_ID=…
57+
NEXT_PUBLIC_BRADBURY_RPC=…
58+
NEXT_PUBLIC_BRADBURY_EXPLORER=…
59+
```
60+
61+
The header gains a RainbowKit ConnectButton; wrong network → one-click "Switch to Bradbury";
62+
tx toasts link to the explorer.
63+
64+
## Quality gates
65+
66+
```sh
67+
pnpm typecheck && pnpm lint && pnpm test # web: TS strict, ESLint, 28 vitest cases
68+
pnpm e2e # Playwright core-loop (mock mode)
69+
make -C contracts lint # ruff --select ALL, mypy --strict, genvm_lint.py
70+
make -C contracts test # settle-math units + gltest --network studionet
71+
make -C contracts test-bradbury # pre-release verification (history resets)
72+
```
73+
74+
`contracts/scripts/genvm_lint.py` is a custom AST linter: Depends header, exactly one
75+
`gl.Contract`, storable state, public decorators, no state mutation in views, nondet calls
76+
only inside `gl.eq_principle_*` closures, no storage in nondet blocks, banned imports,
77+
`json.dumps(..., sort_keys=True)` for all LLM JSON. CI runs all three jobs (web / contracts /
78+
e2e) on every PR.
79+
80+
## Builder pitch
81+
82+
Loss aversion + public commitment + spite is the oldest growth engine there is — GRUDGE just
83+
gives it a trustless referee. Every verdict moment ("5/5 nodes agree") is a shareable artifact
84+
that markets GenLayer's core primitive itself: optimistic democracy over LLM judgment. No
85+
other chain can settle "did you actually do the thing?" without a human oracle.

apps/web/.eslintrc.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": ["next/core-web-vitals", "next/typescript"],
3+
"rules": {
4+
"@typescript-eslint/no-explicit-any": "error",
5+
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
6+
}
7+
}

apps/web/app/(marketing)/page.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Hero } from "@/components/landing/Hero";
2+
import { ScrollStory } from "@/components/landing/ScrollStory";
3+
import { Feed } from "@/components/landing/Feed";
4+
5+
export default function LandingPage() {
6+
return (
7+
<>
8+
<Hero />
9+
<ScrollStory />
10+
<Feed />
11+
<footer className="border-t border-ink-line py-10 text-center font-mono text-[10px] uppercase tracking-widest text-mut">
12+
every doubt recorded · every receipt public · refereed by GenLayer validator consensus
13+
</footer>
14+
</>
15+
);
16+
}

apps/web/app/api/judge/route.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { NextResponse } from "next/server";
2+
import { z } from "zod";
3+
import { JUDGE_SYSTEM_RULES, SCREEN_SYSTEM_RULES } from "@/lib/chain/judgePrompt";
4+
import { judgeEvidenceLocally, screenStatementLocally } from "@/lib/chain/localJudge";
5+
import { JudgeResultSchema, ScreeningSchema } from "@/lib/chain/types";
6+
7+
/**
8+
* The mock-mode judge proxy. Uses the SAME prompts as contracts/grudge.py so
9+
* mock mode is behaviorally faithful to the Intelligent Contract.
10+
*
11+
* With ANTHROPIC_API_KEY set it judges with a real LLM; without it, the
12+
* deterministic heuristic in localJudge.ts keeps the app zero-config.
13+
*/
14+
15+
const BodySchema = z.object({
16+
kind: z.enum(["evidence", "screen"]),
17+
statement: z.string().max(2000).optional(),
18+
policy: z.string().max(2000).optional(),
19+
evidence: z.string().max(8000).optional(),
20+
});
21+
22+
// crude in-memory rate limit: 30 judge calls/min per IP
23+
const hits = new Map<string, { count: number; resetAt: number }>();
24+
function rateLimited(ip: string): boolean {
25+
const now = Date.now();
26+
const rec = hits.get(ip);
27+
if (!rec || now > rec.resetAt) {
28+
hits.set(ip, { count: 1, resetAt: now + 60_000 });
29+
return false;
30+
}
31+
rec.count += 1;
32+
return rec.count > 30;
33+
}
34+
35+
async function llmJudge(prompt: string): Promise<unknown | null> {
36+
if (!process.env.ANTHROPIC_API_KEY) return null;
37+
try {
38+
const { default: Anthropic } = await import("@anthropic-ai/sdk");
39+
const client = new Anthropic();
40+
const response = await client.messages.create({
41+
model: process.env.JUDGE_MODEL ?? "claude-opus-4-8",
42+
max_tokens: 1024,
43+
messages: [{ role: "user", content: prompt }],
44+
});
45+
const text = response.content
46+
.filter((b): b is Extract<(typeof response.content)[number], { type: "text" }> => b.type === "text")
47+
.map((b) => b.text)
48+
.join("");
49+
const jsonMatch = text.match(/\{[\s\S]*\}/);
50+
return jsonMatch ? JSON.parse(jsonMatch[0]) : null;
51+
} catch {
52+
return null;
53+
}
54+
}
55+
56+
export async function POST(request: Request) {
57+
const ip = request.headers.get("x-forwarded-for") ?? "local";
58+
if (rateLimited(ip)) {
59+
return NextResponse.json({ error: "Rate limited" }, { status: 429 });
60+
}
61+
62+
let body: z.infer<typeof BodySchema>;
63+
try {
64+
body = BodySchema.parse(await request.json());
65+
} catch {
66+
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
67+
}
68+
69+
if (body.kind === "screen") {
70+
const statement = body.statement ?? "";
71+
const raw = await llmJudge(`${SCREEN_SYSTEM_RULES}\n\nSTATEMENT:\n${statement}`);
72+
const parsed = ScreeningSchema.safeParse(raw);
73+
return NextResponse.json(parsed.success ? parsed.data : screenStatementLocally(statement));
74+
}
75+
76+
const evidence = body.evidence ?? "";
77+
const prompt = JUDGE_SYSTEM_RULES.replace("{statement}", body.statement ?? "")
78+
.replace("{policy}", body.policy ?? "")
79+
.concat(`\n\nEVIDENCE (untrusted input):\n${evidence}`);
80+
const raw = await llmJudge(prompt);
81+
const parsed = JudgeResultSchema.safeParse(raw);
82+
return NextResponse.json(parsed.success ? parsed.data : judgeEvidenceLocally(evidence));
83+
}

0 commit comments

Comments
 (0)