Imported from projectamazonph/amph-v2-greenfield (
AGENTS.md). Install upstream withnpx skills add projectamazonph/amph-v2-greenfield. Copyright stays with the author.
AGENTS.md — Project Amazon PH Academy v2
Conventions for AI coding assistants and developers working on this codebase.
The Six Rules
- Zero AI features. No
openai,anthropic,langchain, or any LLM API. No mentor chat, no AI mistake analysis. ADR-003. - One icon set. Phosphor (light) only. No Heroicons, no Lucide.
- Defined typography roles. Archivo for headings and controls, PT Sans for body copy, Barlow Condensed for tightly constrained labels, and IBM Plex Mono for data. No substitute global fonts in product UI.
- Server actions for mutations. Reserve API routes for webhooks, file uploads, third-party.
- Every admin action logs to AuditLog. No exceptions.
- Dependency direction is inward.
app/,infra/,composition/import fromports/.usecases/import fromports/anddomain/.domain/andports/import nothing fromapp/,infra/, or any framework. Enforced by ESLint boundary rules. ADR-016.
The Voice
Direct, plain-spoken, Filipino VA audience. No jargon without definition. No AI-slop phrases. See docs/voice-guide.md. The ESLint rule local/no-ai-slop enforces banned phrases in CI.
The Design System
Amazon PH simulator system. Dense, scannable, and operational, with a navy shell, cool work surfaces, white cards, Amazon Orange action hierarchy, and restrained elevation. No glassmorphism, gradient orbs, decorative print effects, or decorative blurs. See docs/design-brief.md.
UI Components — Astryx
Complex components (Table, Dialog, Toolbar, SideNav, TopNav, Select, Typeahead, Pagination, MultiSelector, DatePicker, Toast, Skeleton) come from Astryx (@astryxdesign/core). Brand wrappers live in src/components/ui/. Rule of thumb: if AMPH does not have it, build it on Astryx; if AMPH already has it, use the AMPH component.
// Check what's available in the AMPH component library first
import { Button, Card, Input, Badge } from "@/components/ui";
// Only reach for Astryx for components AMPH does not have
import { Table } from "@astryxdesign/core/Table";
The AMPH Astryx theme is in src/themes/amph-theme.ts. It extends neutralTheme with the Amazon Orange, navy-shell, cool-surface, semantic, and restrained-elevation token ramp. Theme is applied via <Providers> in src/app/layout.tsx — every page gets it automatically.
Before writing new UI, run pnpm exec astryx build "<idea>" for a composition kit, then pnpm exec astryx component <Name> for the full API. Never swizzle an Astryx component unless a brand requirement cannot be achieved via theme override.
Token guardrail: valid defineTheme tokens: keys are --color-* (accent, background, text, border, success/error/warning), --spacing-0 through --spacing-12, --shadow-sm/md/lg, --radius-*. Do NOT use --shadow-low/med/high (removed in S-3, audit 2026-08-20, child #407), --spacing-16/20, or --color-info — TypeScript will reject them. The --shadow-sm/md/lg scale is the canonical shadow scale, defined in src/app/globals.css and used by every caller.
Component inventory (as of 2026-09):
src/components/ui/— 22 brand primitives (Button, Card, Input, Badge, Breadcrumb, CommandPalette, ConfirmDialog, EmptyState, MobileNavToggle, PrintButton, RouteError, ScrollToTop, Skeleton, SubmitButton, Toast, etc.)src/components/astryx/— 10 admin tables (AdminAuditLogTable, AdminBadgesTable, AdminCoursesTable, AdminDiscountCodesTable, AdminLiveClassesTable, AdminPaymentsTable, AdminRefundsTable, AdminResourcesTable, AdminSimulatorsTable, AdminUsersTable)src/components/admin/— 11 admin-specific components (AdminSubPageHeader, ConfirmSubmitButton, ImpersonationBanner, NavSidebar, QuizEditor, TopBar, UserCard, etc.)src/components/student/— 2 shell components (StudentShell, StudentSidebar)src/components/tools/— 13 simulator components (BidElevatorForm, BidElevatorResult, CampaignBuilderForm, FormativeScoreNotice, KeywordResearchForm, ListingAuditForm, SimulatorCoachGuide, SimulatorModeToggle, SimulatorNextRep, SimulatorPageHeader, StrTriageForm, etc.)src/components/lesson/— 6 active lesson primitives (SelfCheck, TradeOffTable, ProcessDiagram, PitfallCallout, TrancheOneVisuals, TrancheTwoVisuals, TrancheThreeVisuals, VisualLessonBlock) + directive plugin for MDX fences
The Architecture
Five layers, dependency direction always inward:
app/ → usecases/ → ports/ ← infra/
domain/ (no imports from anywhere else)
domain/— entities, value objects, pure business rules. Nonext, noprisma, nonode-fetch.ports/— interfaces only. Every method returnsPromise<Result<T, E>>.usecases/— one class per use case. Constructor-injected ports.infra/— adapters implementing ports. Prisma repos, PayMongo gateway, Resend sender, React PDF renderer, Sentry tracer, Pino logger.app/— Next.js App Router. Server components by default. Server actions are 5-line shims to usecases. Route handlers exist only for webhooks and third-party callbacks.composition/— the DI container. The one place that knows concrete types.
See docs/build-spec.md and docs/decisions.md (ADRs 001–022).
The Database
PostgreSQL (dev + production). Schema uses no SQLite-specific features. Every mutable table has deletedAt, createdById, updatedById. See docs/db-schema.md. Current schema: 37 models, 20+ migrations.
The Business Layer
PayMongo for payments (one-time, Philippine peso, GCash/Maya/card/bank). Three pricing tiers. Refund window 7 days. Tax-compliant receipts. BIR invoicing behind INVOICING_ENABLED flag. Card installments (3/6/12 month) behind INSTALLMENTS_ENABLED flag. See docs/business-layer.md.
The Admin Panel
/admin/* gated by requireAdmin(). Every route has search, filter, pagination. Every mutation is audited. See docs/admin-backend.md.
Admin sections implemented:
- Dashboard (
/admin) - Users (
/admin/users,/admin/users/[id]) - Courses (
/admin/courses, nested modules/lessons/prerequisites) - Payments (
/admin/payments,/admin/payments/[id], CSV export) - Refunds (
/admin/refunds,/admin/refunds/[orderId]) - Simulators (
/admin/simulators, scenario CRUD + versions + calibration) - Live Classes (
/admin/live-classes) - Discount Codes (
/admin/discount-codes) - Badges (
/admin/badges) - Audit Log (
/admin/audit-log, CSV export) - Settings (
/admin/settings, TOTP setup, site settings) - Email Templates (
/admin/email-templates) - Assignments (
/admin/assignments) - Certificates (
/admin/certificates)
The Curriculum
Lessons live in content/curriculum/modules/ (MDX). Quiz fixtures in content/curriculum/quiz-questions.json. scripts/seed-all-content.mjs, run as node scripts/seed-all-content.mjs, is what writes those paths into the database, lessons and quizzes both. Nothing publishes on deploy. So a committed content change is not live until the seeder runs against the intended database. Target structure: three courses (PPC Foundations, Accelerated Mastery, Ultimate Transformation), a framing inherited from the parent repo projectamazonph/amph-v2; what ships is inventoried in content/CURRICULUM-INDEX.md (13 modules, 45 lessons, 13 module quizzes). Voice: docs/voice-guide.md. Reference lessons as written today: content/curriculum/modules/0-onboarding/0.1-welcome.mdx and content/curriculum/modules/1-foundations/1.1-read-ppc-data-before-you-change-it.mdx. An earlier version of this paragraph pointed at docs/CURRICULUM-REDESIGN.md, docs/0-1-welcome-to-amph.md and docs/1-1-read-ppc-data-before-you-change-it.md, none of which was ever committed to this repository.
Active lesson primitives (Modules 0–5): SelfCheck (interactive radio-group), TradeOffTable, ProcessDiagram, PitfallCallout rendered via :::trade-off{}, :::process{}, :::callout{} MDX fences. Directive plugin in src/lib/mdx/directive-plugin.ts. Validation via scripts/validate-lesson-production.ts --strict.
Voice stabilization (STORY-107): Phase 3 complete across Modules 2–8. Dropped > **Analogy:**, > **Tip:**, > **Watch out:**, > **Key Takeaway:** blockquote headers; converted to inline prose. USD → PHP normalization (~50:1 rate). Body sentences ≤30 words.
Public claims contract: content/curriculum/public-claims.json + contract test validates landing page counts against actual MDX lessons and planned minutes.
Code Style
- TypeScript strict. No
any. Define types or useunknownwith narrowing. - Server components by default.
'use client'only when needed. - No
console.login committed code. Use the structured logger (src/infra/observability/PinoLogger.ts). - No comments that restate the code. Comment the why, not the what.
- File names:
kebab-case.tsfor non-component files,PascalCase.tsxfor components. - Money is never a
number. Use theMoneyvalue object (src/domain/values/Money.ts). - Errors cross boundaries as
Result<T, E>, not thrown exceptions. Throw only for programmer errors (invariant violations).Resultlives atsrc/domain/shared/Result.ts. - Every port has at least one fake implementation for tests. No mocking the real adapter.
Testing
- Vitest for unit + integration.
- Playwright for E2E.
- Tests are collected from
src/**/__tests__/**/*.test.ts(x),tests/**/*.test.ts(x)andsrc/eslint-rules/**/*.test.jsonly (seevitest.config.ts:11-17). Afoo.test.tsleft besidefoo.tsoutside a__tests__/folder is never run bypnpm testor CI, which is how the PayMongo adapter test sat uncollected until it moved intosrc/infra/payment/__tests__/. UsebuildTestContainer()fromsrc/composition/container.test.tsfor usecase tests. - Coverage thresholds enforced in CI are global, not per-directory: 80% lines, 70% branches, 80% functions, 80% statements (
vitest.config.ts:29-32). Measured onmain, 2026-09-23: 83.91% lines, 74.37% branches, 82.52% functions, 82.28% statements. - Domain functions: 100% branch coverage. They are pure; there is no excuse.
- Every use case has tests with a fake gateway, fake repos, and a
FixedClock. - Current test counts: 5,208 unit and integration tests passing across 538 collected files, 3 skipped (2 of those files hold one skipped sample render each), measured on CI 2026-09-23. Playwright: 6 journeys in
tests/e2e/critical-journeys.spec.ts, 20test()blocks across 5 spec files.
Commits
- Conventional commits:
feat:,fix:,refactor:,docs:,test:,chore:. - One concern per commit. Don't mix refactor + feature.
- Reference story IDs:
feat(admin): user list table (STORY-027). - Always
git commitafter work. Never leave uncommitted changes.
Branching
main— production-readyfeat/*— feature branchesfix/*— bugfix branches- Branch off
main, PR back tomain. - Squash merge.
CI Requirements (build fails if any of these fail)
pnpm tsc --noEmit— zero type errorspnpm lint— zero ESLint errors (includeslocal/no-ai-slopand boundary rules)pnpm test— all tests passpnpm test:coverage— coverage above thresholdpnpm test:e2e— Playwright suite passespnpm build— production build succeeds- Lighthouse CI — performance budget met (re-enabled via
output: 'standalone'per ADR-022) gitleaks detect— no secrets in diff- Architecture compliance — 669+ boundary/contract tests pass
File Dependency Chain
src/lib/ ← Pure utilities (Result, Money, format). No deps.
↑
src/domain/ ← Entities + value objects + business rules. No external deps.
↑
src/ports/ ← Interfaces. Depend on domain types only.
↑
src/usecases/ ← Orchestration. Depend on ports + domain.
↑
src/infra/ ← Adapters. Implement ports. Depend on Prisma, PayMongo, etc.
↑
src/composition/ ← DI container. Wires infra into usecases.
↑
src/app/ ← Next.js routes + server actions. Thin.
↑
src/components/ui/ ← AMPH brand UI primitives (Button, Card, Input, Badge). Depend on app, lib.
src/components/astryx/ ← Astryx-based components (Table, Dialog, Toolbar, etc.). Depend on ui, app, lib.
src/components/admin/ ← Admin-specific components (NavSidebar, QuizEditor, etc.). Depend on ui, astryx, app, lib.
src/components/student/ ← Student shell components (StudentShell, StudentSidebar). Depend on ui, app, lib.
src/components/tools/ ← Simulator components (BidElevatorForm, etc.). Depend on ui, astryx, app, lib.
src/components/lesson/ ← Active lesson primitives (SelfCheck, TradeOffTable, etc.). Depend on ui, lib.
Lower layers must not import from higher layers. The ESLint boundary rule blocks this at lint time. ADR-016.
SOLID Contract
The five SOLID principles are enforced by the directory structure, not by code review:
- S (SRP): one class per file. Use cases orchestrate; they do not implement IO. Repositories own one table each.
- O (OCP): new payment gateway = new adapter implementing
PaymentGateway. New simulator = new domain module + registry entry. No edits to the orchestrator. - L (LSP): every port has a
Fake*implementation insrc/infra/*/fake/. The fake and the real must honor the same postconditions, documented in the port's JSDoc. - I (ISP): repositories are split per use case, not one god
PrismaClient.EnrollmentRepositoryis notUserRepository. - D (DIP):
domain/andusecases/never import fromnext,prisma,paymongo,resend, or@sentry/*. ESLint blocks it.
See docs/build-spec.md for the full contract and docs/decisions.md ADR-013 for the rationale.
Don't Do
- Don't add dependencies without updating
package.jsonandpnpm-lock.yaml. - Don't use
fetchdirectly in components. Use server actions. - Don't store secrets in code. Use env vars.
- Don't commit
.env*files..env.exampleis allowed. - Don't use emojis in code or commit messages.
- Don't use em-dashes. Use periods, commas, parentheses.
- Don't write generic AI-slop copy. The ESLint rule catches most, but read
voice-guide.mdfor the full rules. - Don't ship code without tests for new features (admin and business layer are mandatory).
- Don't ignore the AuditLog. Every admin mutation logs.
- Don't import
prisma,next/cache,paymongo,resend, or@sentry/*fromsrc/domain/,src/usecases/, orsrc/ports/. The ESLint boundary rule will fail the build. - Don't use
numberfor money. Use theMoneyvalue object. - Don't throw exceptions across layer boundaries. Return
Result.err(...). - Don't add a 6th simulator by editing the tools page. Add a domain module and a registry entry.
- Don't mock the real Prisma client in tests. Use
InMemory*Repositoryfromsrc/infra/db/inmemory/.
On Errors
When something breaks:
- Read the actual error. Don't guess.
- Reproduce in the smallest possible test.
- Fix root cause, not symptom.
- Add a test that would have caught this.
- Commit fix + test together.
Adding a New Feature (Recipe)
- Model the domain. Add entities and value objects in
src/domain/<feature>/. No imports fromapp/orinfra/. Write tests. - Define the port(s). Add interfaces in
src/ports/<concern>/. Document postconditions. Write aFake*implementation. - Write the use case. Add a class in
src/usecases/<feature>/. Constructor-inject the ports. UseResult<T, E>. Test withbuildTestContainer(). - Implement the adapter (if needed). In
src/infra/<concern>/. Wrap the real SDK. Map to/from domain types. - Wire it. Add to
src/composition/container.ts. Add tobuildTestContainer()if relevant. - Expose it. Add a server action in
src/app/actions/<feature>.ts(5 lines: parse, call, return) or a page insrc/app/(dashboard)/.... - Add a story.
docs/stories/STORY-XXX.md. Acceptance criteria. Definition of Done.
Guardrails for AI Agents
This section is derived from real audit findings. Each rule below is a defect that shipped or almost shipped.
Pre-flight checklist (run before opening a PR)
- Verify the gap exists. Grep the source for the alleged missing piece. Many "planned" features are already implemented — don't re-build what's there.
- Read the story file. If
docs/stories/STORY-XXX.mdalready exists, read its## Statusblock. Trust it unless the source contradicts it. - Check
docs/STUDENT-FEATURE-GAP-ANALYSIS.md. It is the live audit of student-facing gaps. If your work is in there as "fixed", update the doc in the same PR. - Find the canonical owner of the file path. Use
git log --follow <file>to see who last touched it and the PR that introduced the current pattern. Match that style. - Check the corresponding port. If you add a method to a repository, confirm the port in
src/ports/declares it, theFake*adapter insrc/infra/<concern>/fake/implements it, andbuildTestContainer()wires the real adapter.
Hard Rules for Adding or Editing Code
Routes and server actions
- Every new student-facing route lives under
src/app/<feature>/page.tsx. Use the App Router conventions already present (loading.tsx,error.tsx,__tests__/). - Every new mutation lives as a server action in
src/app/actions/<feature>.action.ts, not as an API route. The only API routes are webhooks and third-party callbacks (Rule 4). - Server actions must call a use case, not implement business logic. The action is a 5-line shim: parse, call, return.
- New student-facing pages must register with the loading-skeleton coverage target (64/64). Add
loading.tsxto every new page directory.
Database changes
- Every mutable Prisma model needs
deletedAt,createdById,updatedById. No exceptions. - Never edit an existing migration. Append a new one under
prisma/migrations/<timestamp>_<description>/migration.sql. - Never call
new PrismaClient()outsidesrc/infra/database/prisma.ts. UsebuildContainer(). - Money is never
number. UseMoney.of(amount, "PHP")in domain code.
Auth and sessions
- New auth code must use
getSessionUserId()fromsrc/lib/auth.ts, not a hand-rolled cookie parse. - Session-touching use cases must validate against the
sessionstable, not just the JWT signature. JWT-only auth is a P1 audit finding. - Any new impersonation path must capture the admin's original token and replant it on restore. Don't sign the admin out.
Audit logging
- Every admin mutation goes through
RecordAuditLog. Add the call inside the use case, not in the action. - New audit actions extend the
AuditActionenum atsrc/domain/values/AuditAction.ts. Don't use string literals.
Simulator rules (these have shipped several P1s)
- Every graded action passes the real
userIdfromgetSessionUserId(). Never hardcode"system"or any literal. - Adding a simulator requires a new entry in
buildSimulatorRegistry.ts, not a new branch insrc/app/tools/. - Simulator scores are formative. Never label them "certified" or "hiring ready" in copy.
Admin pages
- Every
/admin/*route is gated byrequireAdmin(). - Every list page has search, filter, and pagination.
- Every mutation is audited (see above).
- New admin forms use the AMPH
@/components/uiprimitives — never raw HTML inputs.
Student-facing copy
- Voice: direct, plain-spoken, Filipino VA audience. No AI-slop phrases.
- No em-dashes in copy or commit messages.
- Forms have real labels, not just placeholders.
- Money displays in PHP (
₱). Never$. NeverUSD.
Things to Never Do (extended list)
- Don't add a use case without an
Fake*adapter wired intobuildTestContainer(). Tests will silently use the real adapter. - Don't add a port method without updating its JSDoc postconditions. Future agents will misuse the contract.
- Don't ship a new page with
loading.tsxmissing. Hardcode a skeleton import —import { Skeleton } from "@/components/ui/Skeleton". - Don't bypass the AuditLog on a "one-off" admin action. There are no one-offs.
- Don't add
pnpm installpackages without updatingpackage.jsonandpnpm-lock.yaml. PR will fail CI. - Don't write
// @ts-expect-errorto make a build pass. Fix the type or ask. - Don't duplicate content from
docs/stories/STORY-XXX.mdinto the PR description. Link to the file. - Don't open a PR with uncommitted changes in the working tree.
- Don't rebase by force-pushing
main. PRs merge via squash. - Don't put emoji in code, commits, or PR descriptions.
- Don't ship a
feat:commit that includes arefactor:of unrelated files. - Don't add a 6th simulator by editing
src/app/tools/page.tsx. Add a domain module + registry entry (this is already in## Don't Do; restated here). - Don't write story docs that mark work as "Planned" when the source already ships it. Run a grep first.
Story doc maintenance
Every time you change a student-facing feature, in the same PR update:
- The corresponding
docs/stories/STORY-XXX.md## Statusblock. FEATURES.mdstatus column for the affected feature.CHANGELOG.mdwith a one-line entry.docs/STUDENT-FEATURE-GAP-ANALYSIS.mdif the change closes a verified gap.
Audit verification pattern
For "is this already built?" questions, prefer this pattern over guessing:
# Does the route exist?
ls src/app/<feature>
# Is it wired in the container?
grep -rn '<Feature>Repository' src/composition/
# Does the use case exist?
ls src/usecases/<Feature>*.ts
# Is there a test?
ls src/usecases/__tests__/<Feature>*.test.ts
If all four pass, the feature ships. If 1-3 pass, build the missing pieces. If 0 pass, start from the recipe in "Adding a New Feature".
When you're not sure
- Run
grep -r '<keyword>' src/for the implementation status. - Read the story doc if one exists.
- Read
docs/STUDENT-FEATURE-GAP-ANALYSIS.md. - Read the "Remaining known limitations" section of
STATE.mdand the "Known gaps" section ofCLAUDE.md. (This step used to namedocs/audit-2026-07-27-completeness-review.md, removed on 2026-09-14 bye1f7352.) - Read
docs/sprint-plan.md. - Read
docs/decisions.md(ADRs). - Read
docs/SHIPPED-AND-REMAINING.md. - If still uncertain, do not invent. Mark the PR draft and ask.
Memoria Protocol
This repo uses Memoria for cross-agent context. Tag memories with:
project:amph-v2phase:1(analysis),2(planning),3(solutioning),4(implementation),5(enrichment)agent:dusk(this instance)
Other agents (Atlas on phone OpenClaw, Vader on phone Hermes) share the same memoria server. Leave notes for them on handoffs.
Astryx v0.1.8 · 153 components
CLI: run every command as pnpm exec astryx <cmd> (shown below as astryx ...).
SETUP (once, in your app entry e.g. main.tsx) — without these, components render unstyled: import "@astryxdesign/core/reset.css"; import "@astryxdesign/core/astryx.css";
WORKFLOW — discover, don't guess. Before writing UI:
astryx build "<idea>"— START HERE: returns a kit (closest [page] + [block]s + [component]s). No args = full playbook.astryx template <name> [--skeleton]— scaffold the [page]/[block]s it named, or study their layout. Templates are reference code.astryx component <Name>— props + examples for every component you use.
RULES:
- No — components do all layout/spacing. Full page → AppShell; sidebar nav → SideNav.
- Frame first: pick the shell (AppShell / Layout+LayoutPanel) and budget regions in px BEFORE writing content (
astryx docs layout). - Dense data = rows (Table, List/Item) edge-to-edge — never Card-wrapped list items. Card = dashboard widgets, galleries, settings groups only.
- Status → StatusDot/Token; Badge only for counts and enumerated states, never decoration.
- Custom styling: component props first; else style/className with tokens — var(--color-|--spacing-|--radius-*). No raw hex/px. (No StyleX/Tailwind compiler here — don't use xstyle/utility classes.)
- Tokens for every value (
astryx docs tokens). Brand/accent viaastryx theme— never override --color-* in :root. - SELF-CHECK before you finish: re-read the file and replace any raw / layout, imported .css/@apply, or hardcoded value (#hex, 16px) with the component or a token (var(--color-|--spacing-|…)). If unsure a component/prop exists, run
astryx component <Name>/astryx search "<thing>"; don't hand-roll CSS.
MORE CLI: search "" find any component / hook / doc / template / block component --list 153 components by category template --list page + block recipes docs color, elevation, icons, illustrations, internationalization, layout, migration, motion, principles, shape, spacing, styling, theme, tokens, typography swizzle eject component source for deep customization upgrade --apply run after any @astryxdesign/core bump
