24 KiB
AGENTS.md
This file defines mandatory development rules for AI Agents in this project. Unless the user explicitly requests a deviation, these rules must be followed.
1. Basic Principles
- Scope: the repository root and all subdirectories
- Priority: explicit user instructions > this file > default implementation habits
- If these rules conflict with the user's request: follow the user's request first, and note the deviation in the change summary
2. Fixed Technology Stack
- Backend:
Golang+Gin+GORM+github.com/mlogclub/simple - Database: must be compatible with both
SQLiteandMySQL - Frontend:
Next.js(App Router)+React+shadcn/ui+Tailwind CSS - Frontend package manager:
pnpm
3. Directory Conventions
.
├── cmd/
│ ├── server/
│ ├── migration/
│ └── generator/
├── internal/
│ ├── bootstrap/
│ ├── builders/
│ ├── handlers/
│ │ ├── api/
│ │ ├── dashboard/
│ │ └── third/
│ ├── middleware/
│ ├── migration/
│ ├── models/
│ ├── repositories/
│ ├── services/
│ └── pkg/
│ ├── config/
│ ├── dto/
│ ├── enums/
│ ├── errorsx/
│ ├── httpx/
│ ├── logx/
│ └── utils/
├── web/
└── docs/
4. Backend Layering
The backend must follow one-way dependencies: models -> repositories -> services -> handlers
models: only define entities and table mappingsrepositories: only encapsulate data accessservices: handle business rules, transaction orchestration, and aggregation logichandlers: only parse parameters, check permissions, call services, and wrap responses
Forbidden:
- Handlers directly calling repositories
- Returning GORM models directly to the frontend
- Writing business orchestration in models or repositories
4.1 Full Layer Flow (models -> repositories -> services -> handlers -> builders)
This section is an executable refinement of the layering rules: each layer must do only what belongs to that layer. Data should flow around DTOs, GORM details should be concentrated in repositories, transaction boundaries should be concentrated in services, and response assembly should be concentrated in builders/handlers.
4.1.1 Dependency Direction (Required)
Only the following one-way dependencies are allowed:
models-> must not depend on any business layerrepositories-> may depend onmodelsand base libraries (gorm/simple/sqls)services-> may depend onrepositories,models, andenums/errorsx/utils; responsible for transactions and business orchestrationbuilders-> may depend onmodelsanddto/response; if necessary, may depend on a small number ofservicesto supplement display fields, but aggregation in the service layer is preferredhandlers-> may depend onservices,builders,pkg/dto/request,pkg/httpx/params, andpkg/httpxresponse wrappers
Reverse dependencies are forbidden:
repositoriesmust not depend onservices/handlers/buildersmodelsmust not depend onrepositories/services/handlers/buildershandlersmust not depend onrepositories(they must go through services)
4.1.2 Data Shape and Flow (Recommended Standard)
A typical CRUD/business action data flow:
- The handler reads parameters (
query/body/form/path), performs permission checks, and calls the service - The service executes business rules (validation, idempotency, state machines, aggregation), starts a transaction when needed, and calls the repository
- The repository only performs data reads/writes (
CRUD + queries) and returnsmodelsor necessary aggregate structures - builders map
models/aggregate results intoresponse DTO - The handler returns
httpx.WriteJSON(...)
Strong constraints:
- Handler inputs use request DTOs
- Handler outputs use response DTOs
- Models must not be returned directly to the frontend
4.1.3 Per-Layer Allow/Forbid Checklist
models (Entity Layer)
- Allowed
- Field definitions, table names, index/constraint tags, associations (GORM tags)
- Lightweight constants/enum field types (prefer
internal/pkg/enums)
- Forbidden
- Business methods (for example, rule checks such as
CanDispatch()belong in services) - DB access, transactions, complex calculations
- Business methods (for example, rule checks such as
repositories (Data Access Layer)
- Allowed
- CRUD:
Get/Take/Find/FindOne/FindPageBy.../Create/Update/Updates/UpdateColumn/Delete - Reusable query-related methods:
FindByUserID,CountByStatus,FindActiveBy... - Anything that is a data-access detail belongs here (SQL conditions, sorting, pagination, locks)
- CRUD:
- Forbidden
- Business orchestration (cross-table workflows, state transitions, event publishing, etc.)
- Permission checks or login-state checks
- Directly assembling response DTOs (DTO mapping belongs in builders/handlers)
Repository best practices:
- Prefer unified primary-key read/write methods:
Get/Updates/Delete, avoiding repeatedid = ?logic in services - Prefer query conditions through
sqls.Cnd/sqls.NewCnd() - Repository method signatures should consistently accept
db *gorm.DB(supporting bothsqls.DB()andctx.Tx)
services (Business Layer)
- Allowed
- Business rules: parameter normalization, cross-entity validation, state machines, idempotency, concurrency semantics
- Aggregation: combining results from multiple repositories when needed
- Transaction orchestration:
sqls.WithTransaction(func(ctx *sqls.TxContext) error { ... }) - Domain-object preparation before calling builders (when builders stay cleaner as pure mappers)
- Forbidden
- Handler responsibilities: parameter parsing, HTTP details, response wrapping
- Repository responsibilities: scattered GORM queries (unless it is a one-off complex SQL query that is not worth extracting)
Service best practices:
- Open transactions only where atomicity is required, and ensure every DB operation inside the transaction uses
ctx.Tx - Call repositories from services; do not mix repository usage with direct GORM calls in a way that splits style and ownership
builders (Output Construction Layer)
Purpose: convert models (or service aggregate results) into response DTO, avoiding repetitive mapping boilerplate in handlers.
- Allowed
- Pure
Model -> ResponseDTOmapping - Time formatting and enum label filling when needed
- Batch builders:
BuildXxxList([]models.Xxx) []response.Xxx
- Pure
- Forbidden
- DB access (builders should not query the database)
- Permission checks, transactions, complex business processes
Recommended builder form:
- Location:
internal/builders/*_builder.go - Methods:
BuildXxx(item *models.Xxx) *response.Xxx/BuildXxxList(list []models.Xxx) []response.Xxx
handlers (API Layer)
- Allowed
- Parameter parsing:
params.ReadJSON/ReadForm/NewPagedSqlCnd/GetInt64... - Permissions:
AuthService.GetAuthPrincipal/RequirePermission/HasPermission - Calling services, calling builders, and wrapping with
httpx.WriteJSON
- Parameter parsing:
- Forbidden
- Direct repository calls
- Directly returning models
- Writing business orchestration inside handlers (for example, "write A, then write B")
4.1.4 Transaction Best Practices
Transaction boundaries should be decided by the service layer. Principles:
- A transaction is required (
sqls.WithTransaction) for:- Multiple write SQL statements (for example, updating the main table and writing a log/event/relation table)
- Read-modify-write flows that require consistency (must not break under concurrency)
- Writes across multiple repositories that must be atomic
- A transaction is not required for:
- A single write SQL statement (one
Create/Updates/UpdateColumn/Delete) - A single write SQL statement plus pure calculation/parameter cleanup
- A single write SQL statement (one
Rules inside a transaction:
- Inside a transaction, all DB calls must use
ctx.Tx(passctx.Txas the repository method'sdbargument) - Do not mix in
sqls.DB()inside a transaction (it escapes the transaction)
4.1.5 Standard Endpoint Skeleton (Example)
// Handler: parameters/permissions/response
func XxxUpdate(ctx *gin.Context) {
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionXxxUpdate)
if err != nil {
httpx.WriteJSON(ctx, err)
return
}
req := request.UpdateXxxRequest{}
if err := params.ReadJSON(ctx, &req); err != nil {
httpx.WriteJSON(ctx, err)
return
}
if err := services.XxxService.UpdateXxx(req, operator); err != nil {
httpx.WriteJSON(ctx, err)
return
}
httpx.WriteJSON(ctx, nil)
}
// Service: business rules + transaction orchestration + repository calls
func (s *xxxService) UpdateXxx(req request.UpdateXxxRequest, operator *dto.AuthPrincipal) error {
current := repositories.XxxRepository.Get(sqls.DB(), req.ID)
if current == nil {
return errorsx.InvalidParam("object does not exist")
}
// Single write SQL statement: no transaction required
return repositories.XxxRepository.Updates(sqls.DB(), req.ID, map[string]any{
"name": strings.TrimSpace(req.Name),
"update_user_id": operator.UserID,
"update_user_name": operator.Username,
"updated_at": time.Now(),
})
}
// Builder: Model -> ResponseDTO
func BuildXxx(item *models.Xxx) *response.Xxx {
if item == nil {
return nil
}
return &response.Xxx{
id: item.ID,
// ...
}
}
5. simple Usage Conventions
- Prefer
sqls.Cndfor query conditions - Prefer
internal/pkg/httpx/paramsfor parameter binding - Use
internal/pkg/httpx.WriteJSONfor all HTTP responses - Write transaction boundaries must follow 4.1.4 Transaction Best Practices (avoid slogan-style rules such as "always open a transaction even for a single write SQL statement")
6. Database Compatibility Rules
- Use compatible field types:
varchar,text,int,bigint,datetime - Primary keys must consistently use
int64 - Avoid database-private syntax and dialect-specific features
- Keep time storage and parsing strategies consistent; MySQL must use
parseTime=True
7. Code Generation and Migration
7.1 Code Generation
- Entry point:
cmd/generator/generator.go - Command:
task generator - Generation library:
github.com/mlogclub/codegen - Registration method:
codegen.GetGenerateStruct(&models.XXX{}) - Generated files should be placed in the
generateddirectory and named*_gen.go - Generated code is only responsible for basic CRUD; business logic must be written manually in services/handlers
Standard process:
- Define or modify the model
- Register it in the generator
- Run
task generator - Add business logic in the handwritten layers
- Run tests and self-checks
7.2 Migration
- DDL changes do not go through
internal/migration/runner.goby default - New tables, table changes, and index changes are handled uniformly through
sqls.DB().AutoMigrate(models.Models...) internal/migration/runner.gois only for DML: initial data, backfills, repairs, remapping, etc.- Migrations must be idempotent, and
versionmust increase monotonically - Execution order: run
AutoMigratefirst, thenmigration.Migrate(...)
8. API Conventions
8.1 DTOs and Responses
- Separate DTOs: define
requestandresponseseparately - JSON fields must consistently use
camelCase - Do not leak underlying SQL errors directly
- Error code ranges:
1000-1999parameter errors2000-2999business errors3000-3999authentication/authorization errors5000-5999system errors
8.2 Path Layers
/api/dashboard/*: business dashboard APIs/api/third/*: third-party platform callback/call APIs/api/*: open APIs
Do not add version prefixes such as /api/v1.
8.3 Dashboard API Style
- Prefer flat resource paths, such as
/api/dashboard/project - Prefer
/list,/create,/update, and/deletefor list/create/update/delete - Prefer passing query conditions through
queryorbody - Avoid path params except for detail endpoints
- Detail endpoints may use
GET /api/dashboard/project/{id} - Prefer filtering subordinate resources through ordinary parameters such as
projectIdandepisodeId; deep nested routes are discouraged
8.4 Route Registration
- The Gin engine is created uniformly in
internal/bootstrap/server.go, and middleware is also registered there in order - Routes should be split into grouping functions in
internal/bootstrap/routes.goandinternal/bootstrap/*_routes.go - Business dashboard routes are registered through
dashboardGroup := app.Group("/api/dashboard", middleware.AuthMiddleware) - Open APIs should be organized under
/api/*groups by domain, and third-party callbacks under/api/third/* - Inside groups, mount handlers explicitly through
group.GET/POST/PUT/DELETE/Any(...) - Do not create a separate top-level
app.Group("/api/dashboard/xxx")for each resource - Authentication and authorization middleware should preferably be mounted at the
/api/dashboardor/api/adminlayer
8.5 Gin Explicit Route Rules
This project uses explicit Gin routes and does not use framework automatic routing. Handler method names are only for code organization; final URLs are determined by the paths registered in internal/bootstrap/*_routes.go.
- Route mounting example:
func registerDashboardQuickReplyRoutes(group *gin.RouterGroup) {
group.GET("/:id", dashboard.QuickReplyGetBy)
group.Any("/list", dashboard.QuickReplyList)
group.POST("/create", dashboard.QuickReplyPostCreate)
group.POST("/update", dashboard.QuickReplyPostUpdate)
group.POST("/delete", dashboard.QuickReplyPostDelete)
}
- With the registration above, the resource base path is determined by the outer
dashboardGroup.Group("/quick-reply"); the final full paths are/api/dashboard/quick-reply/list,/api/dashboard/quick-reply/{id}, etc. - Handler names should keep the existing readable prefixes:
XxxList,XxxGetBy,XxxPostCreate,XxxPostUpdate,XxxPostDelete - Handler names do not create routes; before adding an endpoint, the corresponding
register...Routesfunction must be modified - The HTTP method must be determined by the Gin registration method:
- List queries: prefer
group.Any("/list", XxxList), used by the frontend asGET /list - Detail queries: prefer
group.GET("/:id", XxxGetBy) - Write APIs: consistently use
group.POST("/create|/update|/delete", XxxPost...) - Business actions: use explicit paths, such as
group.POST("/send_message", ConversationPostSend_message)
- List queries: prefer
- Path params should be used only for detail endpoints or strong path-semantics scenarios; ordinary filters should continue using query/body
Common correct mappings in the current project:
registerDashboardUserRoutes(dashboardGroup.Group("/user"))group.GET("/:id", dashboard.UserGetBy)->GET /api/dashboard/user/{id}group.Any("/list", dashboard.UserList)->ANY /api/dashboard/user/listgroup.POST("/create", dashboard.UserPostCreate)->POST /api/dashboard/user/creategroup.POST("/update", dashboard.UserPostUpdate)->POST /api/dashboard/user/updategroup.POST("/delete", dashboard.UserPostDelete)->POST /api/dashboard/user/delete
registerDashboardConversationRoutes(dashboardGroup.Group("/conversation"))group.GET("/:id", dashboard.ConversationGetBy)->GET /api/dashboard/conversation/{id}group.Any("/list", dashboard.ConversationList)->ANY /api/dashboard/conversation/listgroup.Any("/message/list", dashboard.ConversationMessage_list)->ANY /api/dashboard/conversation/message/listgroup.POST("/send_message", dashboard.ConversationPostSend_message)->POST /api/dashboard/conversation/send_message
Easy mistakes:
- Do not assume adding an
XxxListmethod automatically creates a/listroute; it must be explicitly registered in the routes file - Do not register detail endpoints as
/detail; the current convention isGET /:id - Do not casually add deeply nested routes; subordinate resources should preferably be filtered by ordinary parameters such as
projectIdandconversationId - If an API contract requires an underscore path, write the underscore path directly in the Gin route, for example
group.POST("/send_message", ...) - Before adding a handler method, first write the corresponding Gin route registration and confirm that the final URL matches the frontend contract
8.6 Handler Conventions
-
One handler file per resource, located at
internal/handlers/{api|dashboard|third}/*_handler.go -
Handler functions should use the uniform form:
func XxxPostCreate(ctx *gin.Context) -
Recommended method names:
XxxList(ctx *gin.Context)XxxGetBy(ctx *gin.Context)XxxPostCreate(ctx *gin.Context)XxxPostUpdate(ctx *gin.Context)XxxPostDelete(ctx *gin.Context)- Business actions may extend this pattern with names such as
XxxPostTest(ctx *gin.Context)orXxxPostGenerate(ctx *gin.Context)
-
Requirements for paginated
Listendpoints:- Use
params.NewPagedSqlCnd(...) - Declare each filter field explicitly through
params.QueryFilter - Prefer default sorting with
.Desc("id"); special sorting requires an explicit reason - Prefer
FindPageByCnd(...)in the service layer - Perform DTO mapping in the handler layer; do not return model lists directly
- Use
-
Paginated responses must use:
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
-
Paginated
datastructure must be:data.resultsdata.page.pagedata.page.limitdata.page.total
-
Detail endpoints should preferably return
httpx.WriteJSON(ctx, dto) -
Delete endpoints should preferably return
httpx.WriteJSON(ctx, nil) -
JSON bodies should preferably be read with
params.ReadJSON -
Form parameters should preferably be read with
params.ReadForm -
Single parameters may be retrieved with
params.GetInt64,params.GetInt64Arr,params.Get, etc. -
Pagination and query parameters should preferably use
params.NewPagedSqlCnd -
Authenticated users should be retrieved through
services.AuthService.GetAuthPrincipal(ctx)orRequirePermission(ctx, ...) -
Permission checks should consistently use
services.AuthService.HasPermission(...)orRequirePermission(...) -
Authentication/authorization failures should consistently return
httpx.WriteJSON(ctx, err) -
Errors such as
gorm.ErrRecordNotFoundshould be converted into clear business messages -
When returning backend data, logic that converts data into response DTOs may be placed under
internal/builders
8.7 Enum Definitions
- System constants should be defined uniformly under
/internal/pkg/enums - Model statuses should preferably use
Statusfrom/internal/pkg/enums/enums.go; only add a new status enum when it does not meet the requirement - Enums shared by backend and frontend must follow docs/design/specs/backend-frontend-enum-ast-spec.md
- Shared backend/frontend enums may only be defined in the backend; the frontend must generate results with
task enums, and handwritten duplicate business enums are forbidden
9. Go Code Standards
- Logs must consistently use the standard library
log/slog - New logs must not introduce other logging libraries
- Log fields should preferably use structured key-value pairs
- New Go code must consistently use
any; do not add newinterface{} - Run
gofmtafter modifying Go code
10. Frontend Standards
10.1 Project Facts
- Frontend directory:
web - Framework:
Next.js 16+ App Router - Page directory:
web/app/* - Component directory:
web/components/* - shadcn/ui base component directory:
web/components/ui/* - Utility directories:
web/lib/*,web/hooks/* - Alias:
@/* - Style entry:
web/app/globals.css - shadcn config:
web/components.json
10.2 Components and Pages
- Prefer base components from
shadcn/ui - If an existing
shadcn/uicomponent covers the use case, do not duplicate an equivalent base component - If missing base components such as
dialog,textarea, orselectare truly needed for business logic, install them according to the standard process instead of hand-writing substitutes - Do not modify
web/components/ui/* - Business components should live in
web/components/*or the corresponding business directory - API calls must be uniformly encapsulated in the service layer; do not scatter raw
fetchcalls in pages - Frontend business APIs must be called through service methods under
web/lib/api/*; rawfetchmust not be used directly inpage.tsx, business components, or stores web/lib/api/client.tsis the default request entry point; new business APIs should preferably reuserequest()instead of implementing another request client- When the backend returns the unified
JsonResult, the frontend must handlesuccess,errorCode,message, anddataconsistently; success must not be determined only by HTTP status - Business code must not parse
JsonResult.data, assemble generic error handling, or hand-write auth-refresh logic by itself; these concerns must be centralized in the common request wrapper - Requests that require login state must reuse the unified wrapper with auth headers,
3000/3002token refresh, and login-expiration cleanup; do not handle these separately at the page layer - Direct use of low-level
fetchis allowed only for third-party external services, binary downloads, SSE/streaming responses, WebSocket handshakes, or other cases not yet supported by the unified wrapper; such usage must include a code comment explaining the reason
10.3 shadcn Usage Process
- First confirm that
web/components.jsonexists; if it exists, do not runinitagain - Commands must be run from the
webdirectory - Dependencies must be installed with
pnpm - Prefer adding new base components with:
cd web && pnpm dlx shadcn@latest add buttoncd web && pnpm dlx shadcn@latest add button dialog form table
10.4 Next.js Conventions
- Prefer App Router
- Add
"use client"explicitly when client state or side effects are required - Pages and layouts should follow the
layout.tsxandpage.tsxconventions - Checks should preferably reuse existing scripts:
dev,build,start,lint,format,typecheck
10.5 Enum Management
- All frontend enums should be defined uniformly in
web/lib/enums.ts - Enums are defined by the backend; frontend enums are generated with
task enums
10.6 Dashboard List and Form Baseline
- Dashboard CRUD pages should preferably follow:
docs/design/specs/frontend-list-form-best-practice.md - Baseline example:
web/app/dashboard/quick-replies - Default to a two-layer structure:
page.tsxmanages the list and state,_components/edit.tsxmanages the dialog form - Forms should default to:
react-hook-form+zod+web/components/ui/field.tsx - API calls should stay in the page layer or service layer; form components should not call APIs directly
- After adding or modifying dashboard list/form pages, the AI Agent must first self-check compliance with that document, then run
cd web && pnpm typecheck
10.7 Other Frontend Standards
- All frontend display times must be formatted as
yyyy-MM-dd HH:mm:ss; preferably useformatDateTimefromweb/lib/utils.ts - Dropdown components should not use the shadcn
selectcomponent; use the shadcncomboboxcomponent instead. The project has a general dropdown wrapper atweb/components/option-combobox.tsx; use it where possible. - If data is used inside a component, the component should load it itself as much as possible instead of receiving it from outside. Preserve component independence.
11. Pre-Commit Checklist
After each change, at minimum confirm:
- There are no cross-layer calls or reverse dependencies
- Write operations have clear transaction boundaries
- Responses still follow the unified
JsonResultstructure - Compatibility with both SQLite and MySQL is preserved
- Necessary tests were added, at least covering core service paths
gofmtwas run for Go changes- Frontend changes passed at least
pnpm lintorpnpm typecheckfrom thewebdirectory