Skip to content

Latest commit

 

History

History
325 lines (223 loc) · 11.5 KB

File metadata and controls

325 lines (223 loc) · 11.5 KB

AGENTS.md

This guide is intended for AI agents (Copilot, Cursor, Claude, Gemini etc.) working in the Domus repository.


🛡️ Anti-Hallucination Protocol

  1. Strict Scope: Code only what is explicitly requested. No extra files, no unsolicited features.
  2. Stop & Ask: If anything is unclear, stop and ask. Zero assumptions.
  3. Brief: Keep explanations concise and focused.
  4. Read First: State which AGENTS.md section and quote the relevant issue instruction before coding.
  5. Verify Imports: Only import from paths that explicitly exist in the codebase or issue.
  6. ALWAYS follow AGENTS.md and focus on the current issue only.

Agent Self-Verification Checklist

MANDATORY: Before providing any code response, verify your implementation against this checklist.

  • No any: Ensure no explicit any is used. Use unknown, generics, or search @domus/core for the correct type.
  • No shadcn edits: NEVER touch files in apps/dash/src/shared/ui/shadcn/. Perform customizations by creating wrapper components or extending styles in other directories.
  • Result Pattern: Confirm all services return Result<T> via ok()/fail() and never throw.
  • Server Actions: Return Go-style Result<T> via [data, error] tuple and never throw.
  • Type-Only Imports: Check that all type imports use import type.
  • TSDoc: Verify all exported symbols have a clear TSDoc summary.
  • Soft Delete: Ensure deletedAt IS NULL is handled in database queries.
  • Auth Responsibility: Check authentication in Proxy/Action and authorization in Service.
  • Thin Exports: Ensure route components are logic-less exports.
  • Naming: Use entity-only names for services/repositories (no .service.ts).
  • Zod Best Practices: Use top-level Zod functions (z.uuid(), z.uuidv7(), z.email(), z.url()) instead of chained string methods (z.string().uuid(), etc.) to support better tree-shaking and follow Zod 4 conventions.

Product Context

Domus is a Progressive Web App (PWA) for digitizing the administration of Christ the King Parish Barong Tongkok — covering attendance tracking for organizational activities, financial bookkeeping, and parish membership management.

See → docs/prd.md


Architecture & Tech Stack

Monolith built on Next.js 16 App Router within a Turborepo monorepo and PNPM as package manager. All business logic is isolated in packages/core (Clean Architecture). The main app at apps/dash follows Feature-Sliced Design (FSD).

See → docs/tdd.md — System Overview · Tech Stack · Monorepo Architecture


Conventions

Naming

See → docs/tdd.md — Naming Conventions

Key rules for file naming:

  • Service files: name by entity only, no suffix — service/parishioner.ts ✅, service/parishioner.service.ts
  • Repository files: name by entity only, no suffix — repository/parishioner.ts ✅, repository/parishioner.repository.ts
  • React component files: PascalCase (e.g., ParishionerCard.tsx)
  • All other files: kebab-case as usual

Clean Architecture (packages/core)

See → docs/tdd.md — Clean Architecture

Result Pattern

Services must not throw exceptions. All services return Result<T> via ok() / fail().

See → docs/tdd.md — Result Pattern

Server Actions

Server Actions must not throw exceptions — always return Result<T> (Go-style tuple) from @domus/core. Actions are responsible for:

  1. Fetching session using getAuthSession().
  2. Handling errors from getAuthSession() and returning fail(error).
  3. Constructing AuthContext from session.
  4. Calling service and returning its Result.

See → docs/tdd.md — Error Handling in Server Actions

Enums

Use the const object + as const pattern. Do not use TypeScript enum.

See → docs/tdd.md — Enum Conventions

Thin Exports in app/

All files inside app/ (pages and route handlers) must only contain thin exports — no wrappers, no logic.

See → docs/tdd.md — apps/dash Structure

TypeScript Best Practices

These rules are enforced via Biome and apply across all packages and apps in the monorepo.

No Explicit any

Never use any explicitly. Use proper types, generics, or unknown instead.

// ✅ Correct
function parse(input: unknown): string { ... }

// ❌ Wrong
function parse(input: any): string { ... }

Use import type for Type-Only Imports

Always use import type when importing types or interfaces. This ensures they are erased at compile time and never included in the bundle.

// ✅ Correct
import type { Parishioner } from '@domus/core/entity/parishioner'
import type { AuthContext } from '@domus/core/entity/auth-context'

// ❌ Wrong
import { Parishioner } from '@domus/core/entity/parishioner'

Exception: If a module export is used both as a value and a type in the same file, a regular import is acceptable.

No Unused Imports

Remove all unused imports. Biome will error on any import that is not referenced in the file.

// ✅ Correct — all imports are used
import { ok, fail } from '@domus/core/utils'
import type { Result } from '@domus/core/utils'

// ❌ Wrong — CoreError is imported but never used
import { ok, fail } from '@domus/core/utils'
import type { Result } from '@domus/core/utils'
import { CoreError } from '@domus/core/error'

Organize Imports

Imports are auto-organized by Biome via assist.actions.source.organizeImports. No manual ordering needed — just run biome check --write.


Testing

Framework

  • Vitest — test runner for all packages.
  • vitest-mock-extended — for mocking interfaces and classes.

File Placement

Test files are colocated with the source file they test, using the .spec.ts suffix.

packages/core/src/error/core-error.ts
packages/core/src/error/error.spec.ts   ✅

packages/core/src/service/parishioner.service.ts
packages/core/src/service/parishioner.spec.ts   ✅

Mocking with vitest-mock-extended

Use mock() to create typed mocks of repository interfaces:

import { mock } from 'vitest-mock-extended'
import type { IParishionerRepository } from '../contract/parishioner'

const repo = mock<IParishionerRepository>()

repo.findById.mockResolvedValue({
  id: '1',
  fullName: 'Budi Santoso',
  // ...
})

Service Test Example

// packages/core/src/service/parishioner.spec.ts
import { describe, expect, it } from 'vitest'
import { mock } from 'vitest-mock-extended'
import type { IParishionerRepository } from '../contract/parishioner'
import { NotFoundError } from '../error'
import { createParishionerService } from './parishioner'

describe('ParishionerService', () => {
  const repo = mock<IParishionerRepository>()
  const service = createParishionerService(repo)

  it('should return parishioner when found', async () => {
    repo.findById.mockResolvedValue({ id: '1', fullName: 'Budi Santoso', userId: null, /* ... */ })

    const [result, error] = await service.getParishioner('1')

    expect(error).toBeNull()
    expect(result?.fullName).toBe('Budi Santoso')
  })

  it('should return NotFoundError when parishioner does not exist', async () => {
    repo.findById.mockResolvedValue(null)

    const [result, error] = await service.getParishioner('999')

    expect(result).toBeNull()
    expect(error).toBeInstanceOf(NotFoundError)
  })
})

Rules

  • Always mock repository interfaces — never use real database connections in unit tests.
  • Each describe block focuses on one service or module.
  • Test both the happy path and error paths (e.g. NotFoundError, ForbiddenError).

Documentation

All exported symbols must have TSDoc comments. This applies across all layers — entity/, contract/, service/, repository/, error/, utils/.

What to document

Symbol Required
Exported functions & class methods
Exported classes & interfaces
Exported Zod schemas & inferred types
Exported constants & enums
Interface method signatures
Internal helpers / private methods
index.ts re-exports
Obvious getters where name + type is self-explanatory

Format

Use TSDoc (/** */). Always include a one-line summary. Add @param, @returns, and @throws only when they add meaningful context beyond what the type signature already communicates.

// ✅ Exported service function — document it
/**
 * Retrieves a parishioner by their ID.
 *
 * @param id - The parishioner's unique identifier.
 * @returns `ok(parishioner)` if found, `fail(NotFoundError)` if not.
 */
export async function getParishioner(id: string): Promise<Result<Parishioner>> { ... }

// ✅ Exported interface — document the interface and non-obvious methods
/**
 * Repository contract for parishioner data access.
 */
export interface IParishionerRepository {
  findById(id: string): Promise<Parishioner | null>

  /**
   * Finds a parishioner linked to a specific user account.
   * Returns `null` if the user has no associated parishioner record.
   */
  findByUserId(userId: string): Promise<Parishioner | null>
}

// ✅ Exported Zod schema — document it
/**
 * Zod schema and inferred type for a parishioner entity.
 */
export const ParishionerEntity = z.object({ ... })

// ❌ index.ts re-export — skip
export * from './parishioner'

// ❌ Internal helper — skip
function buildWhereClause(id: string) { ... }

Rules

  • Write in English.
  • Keep comments concise — one clear sentence is better than a long paragraph.
  • Do not restate the type signature in prose (e.g. avoid "takes a string and returns a promise").
  • Do not add @throws — Domus services use the Result pattern, not exceptions.

Domain Model & Database Schema

See → docs/erd.md

Key notes:

  • All tables use soft delete via the deletedAt field. Always include WHERE deletedAt IS NULL in default queries. → docs/erd.md — Soft Delete Strategy
  • Table names: snake_case. Fields/columns: camelCase. Drizzle schema constants: camelCase.

Roles & Access Control

Domus uses a hybrid role model — global roles and scoped roles per organization.

See → docs/tdd.md — Roles & Access Control · docs/prd.md — Users & Roles


Authentication

Google OAuth via Better Auth. Only users with accountStatus: approved can access the application.

Auth responsibility is divided as follows:

  • Next.js Proxy (proxy.ts) — session validation, accountStatus check, routing by status
  • Server Action — retrieve session, construct AuthContext, pass to service
  • Service — permission check using AuthContext for write/domain-specific ops

Auth check: permission in service, authentication in NextJS Proxy + Server Action.

See → docs/tdd.md — Authentication


Deployment & Infrastructure

See → docs/tdd.md — Deployment & Infrastructure


License

MIT — see → docs/tdd.md — License