|
Download AGENTS.md from Atmint/cpns: direct link, hf CLI and curl.
- Browser
- Download file 18.3 kB
-
https://huggingface.co/spaces/Atmint/cpns/resolve/main/AGENTS.md
- Command line
-
hf download hf://spaces/Atmint/cpns/AGENTS.md
-
curl -L -o AGENTS.md https://huggingface.co/spaces/Atmint/cpns/resolve/main/AGENTS.md
18.3 kB
| ο»Ώ# AGENTS.md β Pram Project Guide | |
| > This file is for AI agents. Read this first before making any changes. | |
| --- | |
| ## 1. Project Identity | |
| - **Name:** `pram` β AI-powered multi-language test practice platform. | |
| - **Repo type:** Turborepo monorepo (Bun workspaces). | |
| - **Workspace packages:** `apps/*`, `packages/*`. | |
| - **Runtime & package manager:** **Bun** (`packageManager: "bun@1.3.11"`). | |
| - Do **NOT** use `pnpm`, `npm`, or `yarn` unless explicitly required by a global tool. | |
| --- | |
| ## 2. Technology Stack | |
| | Layer | Technology | Notes | | |
| |-------|------------|-------| | |
| | Frontend | React 19 + Vite + TanStack Router | File-based routing. **Not Next.js.** | | |
| | Backend | Hono + tRPC | Entry: `apps/server/src/index.ts`. Port 3000. | | |
| | DB Engine | PostgreSQL | Managed via Drizzle. | | |
| | DB ORM | Drizzle ORM | **Not Prisma.** | | |
| | Auth | Better-Auth | Email/password + OAuth. | | |
| | UI | shadcn/ui (shared) | Lives in `packages/ui`. Imported as `@pram/ui/components/*`. | | |
| | AI | OpenAI-compatible API | User-managed API keys via Settings UI. | | |
| | Build | Turborepo + `tsdown` | Server compiled with `tsdown`. | | |
| --- | |
| ## 3. Architecture Overview | |
| ``` | |
| pram/ | |
| βββ apps/ | |
| β βββ web/ # Frontend (Vite, TanStack Router) β Port 5173 | |
| β βββ server/ # Backend (Hono, tRPC) β Port 3000 | |
| βββ packages/ | |
| β βββ ui/ # Shared shadcn/ui components & styles | |
| β βββ api/ # tRPC routers & business logic | |
| β βββ auth/ # Better-Auth configuration | |
| β βββ db/ # Drizzle schema, queries, migrations | |
| β βββ ai/ # AI generation: prompts, schemas, agentic pipeline | |
| β βββ env/ # Shared environment validation | |
| β βββ config/ # Shared TypeScript configs | |
| ``` | |
| - **Internal imports** use the `@pram/*` workspace namespace. | |
| - Do **NOT** create cross-imports between `apps/*` directly; route through `packages/*`. | |
| --- | |
| ## 4. Essential Commands | |
| Use these exact commands. Do not substitute with `pnpm`/`npm`/`npx` equivalents. | |
| ```bash | |
| # Install dependencies | |
| bun install | |
| # Development | |
| bun run dev # Start all apps (web + server) | |
| bun run dev:web # Start web app only | |
| bun run dev:server # Start server only | |
| # Type checking | |
| bun run check-types # Check all packages | |
| # Database (Drizzle) | |
| bun run db:push # Push schema changes to PostgreSQL | |
| bun run db:studio # Open Drizzle Studio UI | |
| bun run db:migrate # Run migrations | |
| bun run db:generate # Generate migration files | |
| bun run db:seed # Seed reference data (exam types, etc.) | |
| bun run db:start # Start local DB (if configured) | |
| bun run db:stop # Stop local DB | |
| # Testing | |
| bun test # Run all tests | |
| bun run turbo test # Run via turbo pipeline | |
| # Build | |
| bun run build # Build all packages | |
| ``` | |
| --- | |
| ## 5. Common Pitfalls β DO NOT | |
| - β **Do not use `pnpm` / `npm` / `yarn`** for package management or running scripts. | |
| - β **Do not assume Docker or Docker Compose** exists in this repo. There are no Dockerfiles or compose files. | |
| - β **Do not assume Next.js**. This project uses **Vite + TanStack Router**, not Next.js App Router or Pages Router. | |
| - β **Do not assume Prisma**. Database layer is **Drizzle ORM**. | |
| - β **Do not hardcode API keys** in source code. AI keys are user-managed via the Settings UI. | |
| - β **Do not add global CSS** in `apps/web` without checking `packages/ui/src/styles/globals.css` for existing design tokens and CSS variables. | |
| - β **Do not skip heading levels**. After `<h1>`, the next heading must be `<h2>`, not `<h3>` (or lower). Never have two `<h1>` on one page. | |
| - β **Do not create custom modal/dialog components**. Use shadcn `<Dialog>` from `@pram/ui/components/dialog` β it provides built-in focus trap, escape key, scroll lock, and ARIA. | |
| - β **Do not use `any`** in frontend TypeScript. Enforced by eslint `@typescript-eslint/no-explicit-any: "error"` in `apps/web/eslint.config.mjs`. | |
| --- | |
| ## 6. UI / Styling Guidelines | |
| - Design tokens and global styles live in **`packages/ui/src/styles/globals.css`**. | |
| - shadcn/ui primitives are shared via **`packages/ui`**. | |
| - Import components like this: | |
| ```tsx | |
| import { Button } from "@pram/ui/components/button"; | |
| ``` | |
| - To add new shadcn primitives, run from the **project root**: | |
| ```bash | |
| npx shadcn@latest add accordion dialog -c packages/ui | |
| ``` | |
| - App-specific blocks (non-shared) should be added directly in `apps/web/src/components/`. | |
| - DevTools (TanStack Router, React Query) are **production-gated** via `import.meta.env.DEV` β do not remove the condition. | |
| - App-level error boundary is in `__root.tsx` using `<ErrorFallback>` β route-level error boundaries via `errorComponent` route option. | |
| - Lazy-loaded route components use the `.lazy.tsx` pattern: route file is thin, heavy component lives in `components/routes/*Page.tsx` wrapped in `Suspense`. | |
| ### Sticky Detail Page Header Pattern | |
| Detail pages with long scrollable content (e.g. `package.$id.index.tsx`, `attempt.$id.tsx`) use a **sticky top header** pattern: | |
| ```tsx | |
| <div className="min-h-screen pb-32 bg-[var(--warm-cream)]"> | |
| {/* Sticky header */} | |
| <div className="sticky top-0 z-20 bg-[var(--warm-cream)]/95 backdrop-blur-sm border-b border-[var(--oat-border)] shadow-sm"> | |
| <div className="px-6 md:px-12 lg:px-16 max-w-4xl mx-auto pt-4 pb-4"> | |
| {/* Compact breadcrumb β text-xs, truncated title */} | |
| {/* Title row (left) + action buttons (right) */} | |
| {/* Stats row β text-xs */} | |
| </div> | |
| </div> | |
| {/* Scrollable content */} | |
| <div className="px-6 md:px-12 lg:px-16 max-w-4xl mx-auto pt-8"> | |
| ... | |
| </div> | |
| </div> | |
| ``` | |
| Rules: | |
| - Primary CTA ("Mulai Latihan", "Coba Lagi") and secondary actions (Edit, Share) live in the sticky header β **never only at the bottom** of a long page. | |
| - Breadcrumb inside the header is `text-xs` and truncates the current page title to `max-w-[240px]`. | |
| - On small screens (`sm:` breakpoint), button text labels hide (`hidden sm:inline`) leaving only icons. | |
| ### Public/Private Toggle Hover Pattern | |
| Any button that toggles visibility between public and private must reveal **intent on hover** (what will happen, not what it currently is): | |
| ```tsx | |
| <button | |
| onClick={onTogglePublic} | |
| aria-label={isPublic ? "Jadikan privat" : "Jadikan publik"} | |
| className={`group ... ${isPublic | |
| ? "bg-[var(--matcha-300)] text-[var(--matcha-800)] hover:bg-[var(--pomegranate-400)]/15 hover:text-[var(--pomegranate-600)]" | |
| : "bg-[var(--slushie-500)]/15 text-[var(--slushie-800)] hover:bg-[var(--matcha-300)] hover:text-[var(--matcha-800)]" | |
| }`} | |
| > | |
| <MaterialIcon name={isPublic ? "public" : "lock"} className="text-xs group-hover:hidden" /> | |
| <MaterialIcon name={isPublic ? "lock" : "public"} className="text-xs hidden group-hover:inline" /> | |
| <span className="group-hover:hidden">{isPublic ? "Publik" : "Privat"}</span> | |
| <span className="hidden group-hover:inline">{isPublic ? "Jadikan Privat" : "Jadikan Publik"}</span> | |
| </button> | |
| ``` | |
| This pattern is used in `QuestionCard.tsx` and `PackageCard.tsx`. | |
| ### Component Folder Conventions | |
| Large "card" components for list items are extracted into their own folder and file, **not** inlined inside the page component: | |
| | Domain | Card component | Used in | | |
| |--------|---------------|---------| | |
| | Questions | `apps/web/src/components/bank/QuestionCard.tsx` | `BankPage.tsx` | | |
| | Packages | `apps/web/src/components/packages/PackageCard.tsx` | `PackagesPage.tsx` | | |
| When a list item card grows beyond ~40 lines of JSX, extract it. Pass all state through props; the card must be stateless (no data fetching). | |
| ### CalloutCard | |
| `apps/web/src/components/bank/CalloutCard.tsx` is a reusable banner for "N items are private, publish them all" prompts. Use the `label` prop to customise the noun: | |
| ```tsx | |
| <CalloutCard count={privateCount} label="soal" onPublishAll={handlePublishAllPrivate} /> | |
| <CalloutCard count={privateCount} label="paket" onPublishAll={handlePublishAllPrivate} /> | |
| ``` | |
| `count` should come from a **dedicated query with `limit: 1`** (fetches only the `total` field), **not** from the locally accumulated infinite-scroll array β those counts are partial and misleading. | |
| --- | |
| ## 5b. Route Shell & Layout Modes | |
| ### Route Shell System | |
| Every route declares its layout via `staticData` from `apps/web/src/lib/route-shell.ts`. The root layout (`__root.tsx`) reads this to decide which shell to render: | |
| | Shell | When to use | What renders | | |
| |-------|-------------|-------------| | |
| | `routeShell.app` | Authenticated app pages | Sidebar + `<main id="main-content">` + SkipLink | | |
| | `routeShell.public` | Landing, login, auth flows, 404 | Bare `min-h-screen` wrapper, no sidebar | | |
| | `routeShell.fullscreen` | Test-taking, immersive UIs | `h-screen overflow-y-auto`, no sidebar | | |
| Usage in a route file: | |
| ```tsx | |
| import { routeShell } from "@/lib/route-shell"; | |
| export const Route = createFileRoute("/my-route")({ | |
| staticData: routeShell.app, // or .public / .fullscreen | |
| ... | |
| }); | |
| ``` | |
| **Rules:** | |
| - **Always** set `staticData` on every route. Routes without it default to `"app"` shell (sidebar shown). | |
| - Auth routes (`/login`, `/forgot-password`, `/verify-email`, `/setup-avatar`) use `routeShell.public`. | |
| - Test routes (`/package/$id/take`, `/package/$id/attempt/$attemptId`) use `routeShell.fullscreen`. | |
| - Admin routes (`/admin`, `/admin/*`) use `routeShell.fullscreen`. | |
| ### Route Structure Conventions | |
| - **`/`** β Public landing page (`index.tsx` + `index.lazy.tsx`). Redirects logged-in users to `/dashboard`. | |
| - **`/dashboard`** β Authenticated home page (`dashboard.tsx`). Redirects unauthenticated users to `/login`. | |
| - **`/landing`** β Legacy redirect β `/` (keep for backward compat, do not remove). | |
| - **`/$.tsx`** β Catch-all 404 route. Uses `routeShell.public` so no sidebar appears. Renders `<NotFoundPage>`. | |
| - **Do NOT** put a `notFoundComponent` in `__root.tsx`. The 404 is handled exclusively by the splat route `$.tsx`. | |
| --- | |
| ## 5c. Frontend Accessibility Conventions | |
| - **Skip link**: Root layout in `__root.tsx` has a hidden `<SkipLink>` that appears on Tab press, linking to `#main-content` on the `<main>` element. | |
| - **Icon-only buttons**: Every `<button>` or `<Button>` with only an icon (no visible text) **must** have `aria-label`. The icon element (`<MaterialIcon>`, `<span>`) should NOT have `aria-hidden="true"` unless decorative. | |
| - **Heading hierarchy**: Never skip levels (`h1 β h2 β h3`, never `h1 β h3`). Exactly one `<h1>` per page. Heading level corresponds to semantic nesting, not visual size. | |
| - **Form labels**: Every `<Input>`, `<select>`, `<textarea>` needs an associated `<Label>` (with `htmlFor` + `id`) OR an `aria-label` attribute. Search inputs, OTP digits, and icon-only filter buttons are common offenders. | |
| - **Dialogs**: All modals use the shadcn `<Dialog>` component (from `@pram/ui/components/dialog`), which provides: | |
| - Focus trap inside dialog | |
| - Escape key to close | |
| - Scroll lock on body | |
| - Focus restoration to trigger element on close | |
| - ARIA `role="dialog"`, `aria-modal`, `aria-labelledby` | |
| - **Custom tabs/accordions**: Must have proper ARIA roles (`role="tablist"`, `role="tab"`, `aria-selected`, `tabindex` for roving tabindex) and arrow key handlers. Prefer shadcn `<Tabs>` where possible; use manual ARIA + `handleRovingKeyDown` helper only when tabs have close buttons or other non-standard elements. | |
| --- | |
| ## 7. Environment & Secrets | |
| - Server environment variables are loaded from **`apps/server/.env`**. | |
| - Required variables typically include: | |
| - `DATABASE_URL` (PostgreSQL connection string) | |
| - `BETTER_AUTH_SECRET` | |
| - `BETTER_AUTH_URL` | |
| - AI provider API keys are **NOT** stored in `.env`. Users configure their own keys via the Settings page in the UI. | |
| --- | |
| ## 8. Database Schema Conventions | |
| - Schema files live in **`packages/db/src/schema/`**. | |
| - Use `pgTable` from Drizzle. Primary keys are usually `uuid` with `.defaultRandom()`. | |
| - Add indexes for frequently queried foreign keys. | |
| - When adding new tables or columns, run `bun run db:push` to sync the local database. | |
| --- | |
| ## 9. AI Generation Context | |
| - AI logic is centralized in **`packages/ai/`**. | |
| - Key files: | |
| - `src/schemas.ts` β Zod schemas for questions, generation input, and output. | |
| - `src/prompts.ts` β Prompt builders for quick mode. | |
| - `src/agentic.ts` β Multi-step agentic generation pipeline (passage β validate β questions β self-check). | |
| - Adding a new question format requires updates to: | |
| 1. `packages/ai/src/schemas.ts` (enum + discriminated union) | |
| 2. `packages/ai/src/prompts.ts` (format description) | |
| 3. `packages/ai/src/agentic.ts` (format schema string) | |
| 4. `packages/api/src/routers/attempt.ts` (answer normalization) | |
| 5. `apps/web/src/lib/generate-constants.ts` (format metadata + allowed exams) | |
| 6. `apps/web/src/lib/exam-constants.ts` (format string list) | |
| --- | |
| ## 10. Backend Conventions (tRPC / Hono) | |
| ### Error Handling | |
| - **Always use English** error messages in tRPC routers. | |
| - Use helper functions from `packages/api/src/lib/errors.ts`: | |
| ```ts | |
| import { throwNotFound, throwForbidden, throwBadRequest } from "@pram/api/lib/errors"; | |
| assertOwnership(row, userId, "Question"); // throws NotFound or Forbidden | |
| ``` | |
| - Never throw raw `new Error("...")` in tRPC routers. | |
| ### Pagination | |
| - Use shared schema + helper for all list endpoints: | |
| ```ts | |
| import { paginationSchema, paginateDefaults } from "@pram/api/lib/pagination"; | |
| .input(z.object({ search: z.string().optional(), ...paginationSchema?.shape }).optional()) | |
| const { limit, offset } = paginateDefaults(input); | |
| ``` | |
| - Return shape: `{ items: rows, total }` (or domain-specific name like `{ questions: rows, total }` for legacy compatibility). | |
| ### Authorization (Ownership) | |
| - Use `assertOwnership(row, userId, resourceName)` from `packages/api/src/lib/ownership.ts`. | |
| - Prefer explicit ownership query + assert over embedding `creatorUserId` in `.where()` for mutations that need a clear 404 vs 403. | |
| ### Visibility (Public/Private) | |
| - Use `buildVisibilityCondition(table, userId)` from `packages/api/src/lib/visibility.ts` for list queries. | |
| - Default behavior: guests see only public; authenticated users see public + their own private. | |
| ### Logging | |
| - Winston logger is available in tRPC context via `ctx.logger`. | |
| - Use `logger.info()`, `logger.error()`, etc. for business events and errors. | |
| - Hono HTTP request logging is handled automatically by `hono/logger` in `apps/server/src/index.ts`. | |
| ### Router Structure | |
| - Shared helpers live in `packages/api/src/lib/`. | |
| - Routers live in `packages/api/src/routers/`. | |
| - Register new routers in `packages/api/src/routers/index.ts`. | |
| ## 11. Admin Routes & DataTable | |
| ### Admin Route Structure | |
| - Admin routes are flat files: `routes/admin.users.tsx`, `routes/admin.credits.tsx`, etc. | |
| - Wrapped by `routes/admin.tsx` layout shell (sidebar + admin role check). | |
| - All admin tRPC procedures live in `packages/api/src/routers/admin.ts` and use `adminProcedure` (extends `protectedProcedure` with role check). | |
| - Mutations use `audit()` helper for audit logging to `adminAuditLog` table. | |
| ### DataTable Component (Design Reference) | |
| - Admin table routes should eventually use a `<DataTable<T>>` component from `components/admin/DataTable.tsx`. | |
| - API design: generic `<T>`, column defs with `accessorKey`/`accessorFn`/`cell` render prop, `actions` render prop, opt-in pagination, server-side sorting. | |
| - Search bars, filter pills, and status pill buttons live **above** the DataTable in the route component (not inside it). | |
| - **Do NOT** use DataTable for card-based layouts (e.g., Featured editor's pick) β keep card layouts as-is. | |
| ### Current Migration Status | |
| - `admin.users.tsx` β table-based, good candidate for DataTable | |
| - `admin.jobs.tsx` β table-based, good candidate | |
| - `admin.moderation.tsx` β table-based, good candidate | |
| - `admin.credits.tsx` β transaction table, good candidate | |
| - `admin.featured.tsx` β card layout, keep as-is | |
| - `admin.index.tsx` β dashboard stats cards, keep as-is | |
| --- | |
| ## 11b. Git & Workflow | |
| - Do **NOT** run `git commit`, `git push`, `git rebase`, or force-push unless the user explicitly asks for it. | |
| - Do **NOT** create `README.md` or documentation files unless explicitly requested. | |
| - Keep changes minimal and focused on the task at hand. | |
| - Follow existing code style in the file you are editing. | |
| --- | |
| ## 12. Testing | |
| ### Test Runner | |
| - **Bun test** (`bun test`) β built-in, Jest/Vitest compatible API (`describe`, `it`, `expect`). | |
| - Turbo task `test` sudah dikonfigurasi. | |
| ### Test Location | |
| - Unit tests: `src/__tests__/*.test.ts` di masing-masing package. | |
| - Integration tests: `packages/api/src/__tests__/*.integration.test.ts` (pakai PGlite in-memory DB). | |
| ### Integration DB (PGlite) | |
| - **PGlite** (`@electric-sql/pglite`) β PostgreSQL WASM in-memory. | |
| - Strategy: `mock.module("@pram/db")` intercept DB saat dynamic import untuk inject PGlite-based drizzle instance. | |
| - Setup helper: `packages/api/src/__tests__/test-setup.ts` β skema 14 tabel via raw SQL + seed data. | |
| - Schema diimport dari `../../../db/src/schema` (langsung, bukan via `@pram/db`) untuk hindari env validation side-effect. | |
| - Env vars di-mock via `mock.module("@pram/env/server")` sebelum dynamic import. | |
| ### tRPC Router Testing | |
| - Gunakan `router.createCaller({ session, auth })` untuk memanggil procedure. | |
| - Protected procedure: `session: { user: { id }, expiresAt: new Date() }`. | |
| - Public procedure: `session: null`. | |
| ### Known Limitations | |
| - **Rate limiter** (`checkRateLimit` di `attempt.ts`) pakai in-memory `Map`. Gunakan user ID berbeda per test atau `Bun.sleep()` untuk menghindari rate limit blocker. | |
| - **Timer validation** (`finish`) butuh β₯5 detik elapsed sejak `start` β pakai `Bun.sleep()` + `{ timeout: 30000 }` pada `it()`. | |
| - **Env vars Wajib** untuk di-mock saat integration test: `DATABASE_URL`, `BETTER_AUTH_SECRET` (β₯32 chars), `BETTER_AUTH_URL`, `CORS_ORIGIN`, `API_KEY_ENCRYPTION_KEY` (β₯32 chars), `REDIS_URL`, `SMTP_HOST`, `SMTP_USER`, `SMTP_PASS`, `SMTP_FROM`. | |
| ### TDD Convention | |
| - TDD: tulis test dulu, lihat fail, baru implementasi. | |
| - Untuk existing code yang belum ada test: tulis test yang verifikasi behavior saat ini. | |
| - Fungsi internal yang perlu di-test secara unit harus di-`export`. | |
| --- | |
| _Last updated: 2026-05-25_ | |