Skip to content

Commit 9a2b67b

Browse files
merge: sync latest main
2 parents 1f28a18 + e3303c7 commit 9a2b67b

721 files changed

Lines changed: 150903 additions & 19578 deletions

File tree

Some content is hidden

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

.agents/rules/data-access.md

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,45 @@
22

33
## Principle
44

5-
All database access **MUST** go through a **service** (`packages/business/`) or **repository** (`packages/database/src/repositories/`). No app-layer code (`apps/builder`, `apps/worker`) may import `db` from `@chatbotx.io/database/client` and execute queries directly.
5+
The chain is: **action / API handler → service (`packages/business/`) repository (`packages/database/src/repositories/`) → DB**. No app-layer code (`apps/builder`, `apps/worker`, `integrations/`) may import `db` from `@chatbotx.io/database/client` and execute queries directly. The one exception is a **pure read with zero business logic** — see the carve-out below.
66

77
## Why
88

99
- **Centralized logic:** Business rules, cache invalidation, and event emission stay in one place instead of being scattered across actions, queries, and workers.
1010
- **Testability:** Services and repositories can be mocked at a clear boundary.
1111
- **Sharding readiness:** The message table is already sharded; future tables may follow. Services and repositories abstract the routing logic away from callers.
12-
- **Consistency:** Multiple consumers (builder actions, worker handlers, oRPC endpoints) reuse the same data logic instead of duplicating it.
12+
- **Consistency:** Multiple consumers (builder actions, worker handlers, oRPC endpoints, public API tokens) reuse the same data logic instead of duplicating it.
13+
- **Public API / MCP surfaces need the same guarantees as the UI.** A workspace-token caller and a signed-in member hitting the same resource must run the same validation, cache invalidation, and event emission — which only happens if both call the same service method.
1314

14-
## Allowed layers
15+
## Per-layer responsibilities
1516

16-
| Layer | May import `db` directly? | Role |
17-
|-------|---------------------------|------|
18-
| `packages/database/src/repositories/*` | Yes | Raw query logic, shard routing |
19-
| `packages/business/src/*` | Yes | Business logic, cache, events, orchestrates repositories |
20-
| `apps/builder/src/features/*/actions/` | **No** | Call a service from `@chatbotx.io/business` |
21-
| `apps/builder/src/features/*/queries/` | **No** | Call a service or repository |
22-
| `apps/builder/src/features/*/api/` | **No** | Call a service or repository |
23-
| `apps/worker/src/**` | **No** | Call a service or repository |
24-
| `integrations/**` | **No** | Call a service or repository |
17+
| Layer | May import `db`? | Owns |
18+
|-------|---|------|
19+
| `packages/database/src/repositories/*` | Yes | Raw where-builders, joins, pagination, shard routing. **Never** cache invalidation, event emission, or validation. |
20+
| `packages/business/src/*` | Yes | Validation, orchestration across repositories, cache invalidation, events, audit, quota checks, optional `tx?: DatabaseClient` passthrough. **Never** imports from `apps/` or `integrations/`. |
21+
| `apps/builder/src/features/*/actions/` | **No** | Parse input → call a service method → map the result/error for the client. |
22+
| `apps/builder/src/features/*/queries/` | **No** | See the `.query.ts` contract below. |
23+
| `apps/builder/src/features/*/api/` | **No** | Resolve session context into plain params, call the same service method the private path uses. |
24+
| `apps/worker/src/**` | **No** | Call a service or repository. |
25+
| `integrations/**` | **No** | Call a service or repository. |
26+
27+
**Repository-from-app-layer exception:** a pure read with zero business logic (no cache, no validation, no shape mapping beyond selecting columns) may call a repository directly from the app layer. This is the exception, not the default — reach for a service first, and only fall back to a bare repository call when there's genuinely nothing for a service to add.
28+
29+
## The `.query.ts` file contract
30+
31+
A file under `apps/builder/src/features/*/queries/` (`get-x.query.ts`, `list-x.queries.ts`) is a thin request adapter over one or more services. It:
32+
33+
- **MAY** read session context (current user, member permissions) and turn it into plain params (`accessScope`, `canViewEmailAndPhone`, `restrictToAssignedUserId`, …) passed into a service call.
34+
- **MAY** map a service result onto the builder's response/UI shape.
35+
- **MAY** compose several services for one screen.
36+
- **MUST NOT** hold where-builders, joins, pagination logic, count/caching strategy, or anything a worker or the public API would also need — that belongs in the service (orchestration) or repository (raw query), not duplicated per caller.
37+
- **MUST NOT** import `db` — call a service (or, for the pure-read exception above, a repository).
38+
39+
**A session-free read is called straight from the handler; it needs no query file at all.** Only add a `.query.ts` file when there is real builder-side session-context work (permission scope resolution, response shaping) to adapt.
40+
41+
## Public API and private paths share one service method
42+
43+
The public API (workspace-token) handler and the private action/query adapter for the same operation **must call the same service method**. Only the app layer resolves the caller's permission scope (member permissions vs. an unscoped token) and passes it into the service as plain data (`scope`/`accessScope`) — the service itself never knows whether the caller was a signed-in member or a token. Do not write a second, parallel implementation of the same logic for the public path "because it's simpler" — that is exactly the duplication this layering exists to prevent.
2544

2645
## How to add new data access
2746

@@ -30,7 +49,7 @@ All database access **MUST** go through a **service** (`packages/business/`) or
3049
- `packages/business/src/<domain>/service.ts` — class extending `BaseService`
3150
- `packages/business/src/<domain>/index.ts` — re-export the singleton
3251
- Add the export to `packages/business/src/index.ts`
33-
3. **For pure query helpers** that don't carry business logic (e.g., shard-routed reads), a repository in `packages/database/src/repositories/<domain>/` is acceptable.
52+
3. **For pure query helpers** that don't carry business logic (e.g., shard-routed reads, a where-builder shared across callers), a repository in `packages/database/src/repositories/<domain>/` is acceptable — but the service is still the thing the app layer calls; the app layer reaches the repository directly only under the pure-read exception above.
3453
4. **Services accept an optional `tx?: DatabaseClient`** parameter so callers can pass a transaction handle.
3554

3655
## Existing exceptions
@@ -41,7 +60,9 @@ Many older features still import `db` directly in actions and queries. These are
4160

4261
Before marking a task done:
4362

44-
- [ ] No new `import { db } from "@chatbotx.io/database/client"` in `apps/` or `integrations/`
63+
- [ ] No new `import { db } from "@chatbotx.io/database/client"` in `apps/` or `integrations/` (outside the pure-read repository exception)
4564
- [ ] No new `import ... from "@chatbotx.io/database/schema"` with direct query execution in `apps/` or `integrations/`
4665
- [ ] All DB mutations go through a service method
47-
- [ ] All DB reads go through a service or repository method
66+
- [ ] All DB reads go through a service (or, for a pure read with zero business logic, a repository)
67+
- [ ] A public API handler and its private-path equivalent call the same service method, with only the caller's scope differing
68+
- [ ] `.query.ts` files hold no where-builders, pagination, or count logic — that lives in the service/repository

.agents/skills/builder-ui-i18n/SKILL.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ All user-facing strings must use translations. Do not hardcode labels,
2727
placeholders, button text, empty states, tab names, toasts, or dialog copy in
2828
builder UI.
2929

30-
Primary files:
31-
32-
- `apps/builder/messages/en.json`
33-
- `apps/builder/messages/vi.json`
30+
Source of truth: `apps/builder/messages/en.json`. **All 20 locale files in
31+
`apps/builder/messages/` must carry every key**`apps/builder`'s own `lint` script is
32+
`i18n:check --source en --locales messages` (`apps/builder/package.json:11-12`), which runs
33+
in CI's Lint job. Adding a key to `en.json` + `vi.json` only will fail the parity check.
3434

3535
Before adding keys, check existing `fields.*`, common actions, table labels, and
36-
feature namespaces. Add both English and Vietnamese values for new keys.
36+
feature namespaces.
3737

3838
Typical component pattern:
3939

.agents/skills/business-data-access/SKILL.md

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ description: >-
1212
Use this skill whenever code outside `packages/business` or
1313
`packages/database/src/repositories` needs database-backed behavior.
1414

15+
## The chain
16+
17+
`action | API handler → service (packages/business) → repository
18+
(packages/database/src/repositories) → DB`. This is the wording to use —
19+
never "service **or** repository" as if they were interchangeable
20+
alternatives for the app layer. The app layer calls a service; the service
21+
may call a repository. See `.agents/rules/data-access.md` for the full rule
22+
and the per-layer responsibility table.
23+
1524
## Boundary Rule
1625

1726
Do not add direct database imports in:
@@ -20,23 +29,44 @@ Do not add direct database imports in:
2029
- `apps/worker`
2130
- `integrations`
2231

23-
These layers call services from `@chatbotx.io/business` or repositories from
24-
`@chatbotx.io/database/repositories`. Legacy direct `db` imports are exceptions,
25-
not examples to copy.
32+
These layers call services from `@chatbotx.io/business`. Legacy direct `db`
33+
imports are exceptions, not examples to copy. The one narrow exception is a
34+
**pure read with zero business logic** — no cache, no validation, no
35+
cross-table composition — which may call a repository from
36+
`@chatbotx.io/database/repositories` directly; this is the exception, not
37+
the default, so reach for a service first.
2638

2739
Allowed direct `db` usage:
2840

2941
- `packages/business/src/**`
3042
- `packages/database/src/repositories/**`
3143

44+
## Service responsibilities
45+
46+
A service owns:
47+
48+
- Input validation and authorization-adjacent checks (e.g. quota, ownership).
49+
- Orchestration across one or more repositories.
50+
- Cache invalidation (`this.invalidateCacheTags(...)`).
51+
- Event emission (`emit*` from `@chatbotx.io/events`, `@chatbotx.io/event-bus`).
52+
- Audit records (`this.audit(...)`).
53+
- An optional `tx?: DatabaseClient` passthrough so callers can compose it into
54+
their own transaction.
55+
- **Never** imports from `apps/` or `integrations/` — a service has no idea
56+
who is calling it (a builder action, a worker job, a public API handler).
57+
3258
## Choosing Service vs Repository
3359

3460
Use a business service when the method has business semantics, authorization
3561
adjacent constraints, cache invalidation, event emission, composition across
3662
tables, or is reused by app and worker code.
3763

3864
Use a repository when the method is a low-level persistence concern such as
39-
shard routing, specialized pagination, or reusable raw query mechanics.
65+
shard routing, specialized pagination, a where-builder shared across callers,
66+
or reusable raw query mechanics. **Repositories are raw only** — no cache
67+
invalidation, no event emission, no validation. If a query needs any of
68+
those, it belongs behind a service method that calls the repository, not in
69+
the repository itself.
4070

4171
## Service Pattern
4272

@@ -101,16 +131,72 @@ Use the `drizzle-database` skill for schema, relation, and migration work.
101131

102132
## App Layer Usage
103133

104-
Builder feature queries/actions should call services:
134+
### Session-free read: no query file needed
135+
136+
`tagService.list` needs nothing from the request session — the builder calls
137+
it directly from wherever it's needed (a page, another query), with no
138+
`.query.ts` adapter in between:
105139

106140
```typescript
107141
import { tagService } from "@chatbotx.io/business"
108142

109-
export const listTags = async (params: { workspaceId: string }) => {
110-
return tagService.list({ workspaceId: params.workspaceId })
143+
const { data } = await tagService.list({ workspaceId })
144+
```
145+
146+
If you find yourself writing a one-line pass-through query file that only
147+
forwards its arguments to a service, delete the file and call the service
148+
directly instead.
149+
150+
### Session-context read: a thin `.query.ts` adapter
151+
152+
`get-contact.query.ts` needs the current member's permission scope before it
153+
can call the service — that's the shape a query file exists for:
154+
155+
```typescript
156+
// apps/builder/src/features/contacts/queries/get-contact.query.ts
157+
import { contactService } from "@chatbotx.io/business"
158+
import { requireContactPermissionScope } from "../permissions"
159+
160+
export async function getContact(input: { workspaceId: string; id: string }) {
161+
const accessScope = await requireContactPermissionScope(input.workspaceId)
162+
const contact = await contactService.findDetailOrFail({
163+
workspaceId: input.workspaceId,
164+
id: input.id,
165+
accessScope,
166+
})
167+
return maskIfNeeded(contact, accessScope)
168+
}
169+
```
170+
171+
The query file's only job is: resolve session context → plain params → call
172+
the service → shape the response. It holds no where-builders, no pagination,
173+
no count strategy — see `.agents/rules/data-access.md` for the full
174+
`.query.ts` contract.
175+
176+
### Public API and private paths share one service method
177+
178+
An unscoped workspace-token caller and a signed-in member both resolve to
179+
`contactService.list({ ...input, scope })` — the only difference is what
180+
`scope` the app layer resolved (`undefined` for the token, a permission
181+
scope for the member):
182+
183+
```typescript
184+
// Public API handler (workspace token — unscoped)
185+
.handler(async ({ context, input }) =>
186+
await contactService.list({ ...input, workspaceId: context.workspace.id }),
187+
)
188+
189+
// Private query adapter (signed-in member — scoped)
190+
export async function listContacts(input: ListContactsRequest) {
191+
const scope = await requireContactPermissionScope(input.workspaceId)
192+
return await contactService.list({ ...input, scope })
111193
}
112194
```
113195

196+
Never write a second implementation of the list/count/filter logic for the
197+
public path — both callers must converge on the same service method so a bug
198+
fix or a new filter only has to happen once.
199+
114200
Workers and integrations follow the same boundary.
115201

116202
## Verification

.agents/skills/chatbotx-basecode/SKILL.md

Lines changed: 22 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,53 +14,38 @@ not a replacement for reading adjacent code.
1414

1515
## Project Shape
1616

17-
ChatbotX is a pnpm workspace + Turborepo monorepo.
17+
ChatbotX is a pnpm workspace + Turborepo monorepo. The authoritative layout table is
18+
**`AGENTS.md` → "Repository layout"** — read it there rather than trusting a second copy.
19+
To see what actually exists right now:
1820

19-
```
20-
apps/
21-
builder/ Next.js app: product UI, oRPC/OpenAPI, route handlers
22-
worker/ BullMQ/Kafka background jobs
23-
realtime/ PartyKit realtime server
24-
cli/ chatbotx-cli
25-
mcp-server/ MCP tools generated from OpenAPI
26-
27-
packages/
28-
business/ service layer and business orchestration
29-
database/ Drizzle schema, relations, repositories, migrations
30-
ui/ shared UI components
31-
public-apis/ typed public API client
32-
sdk/ integration contracts and shared schemas
33-
worker-config/ queue names, job payloads, BullMQ queues
34-
flow-config/ flow/node/step config schemas
35-
ai, events, redis, kafka, filesystem, mail, imports, analytics, ...
36-
37-
integrations/
38-
messenger, whatsapp, zalo, tiktok, telegram, webchat, smtp, openai, google-sheets, ...
21+
```bash
22+
ls apps packages integrations
3923
```
4024

4125
## Skill Router
4226

43-
- Builder feature, page, action, query, or public route: use `feature-scaffold`.
44-
- oRPC or OpenAPI endpoint: use `orpc-api`.
45-
- Database schema, relations, migration, repository: use `drizzle-database`.
46-
- Service layer, app data-access boundary: use `business-data-access`.
47-
- UI component work, forms, tables, translations: use `builder-ui-i18n`.
48-
- Worker, BullMQ, Kafka, scheduled job: use `worker-development`.
49-
- Channel integration or webhook behavior: use `integration-channel`.
50-
- Flow step or state-based routing: use `flow-step-development`.
51-
- CLI, MCP server, generated public client: use `public-api-tooling`.
52-
- Dev server, build, lint, package management: use `turborepo-workflow`.
27+
The canonical task → skill routing table is **`CLAUDE.md` → "Skill → task mapping"**. Read
28+
it and pick the skill that matches the task; it lists every skill in `.agents/skills/`.
29+
30+
Two routing notes that table does not spell out:
31+
32+
- CLI, MCP server, and the generated public client all follow the public oRPC surface — use
33+
`orpc-api`.
34+
- A broad request usually decomposes into several skills (e.g. a new feature with a table and
35+
a queue = `feature-scaffold` + `drizzle-database` + `worker-development`). Read each before
36+
writing that layer, not all of them up front.
5337

5438
## Basecode Scan Checklist
5539

5640
1. Read the nearest `package.json`, route/module files, and sibling features.
57-
2. Identify the owning layer before editing:
58-
- UI/app orchestration: `apps/builder`
59-
- business rules: `packages/business`
60-
- raw database queries: `packages/database/src/repositories`
41+
2. Identify the owning layer before editing — the chain is
42+
`action | API handler → service → repository → DB`:
43+
- UI/app orchestration (calls a service, never `db`): `apps/builder`
44+
- business rules, cache invalidation, events (calls a repository): `packages/business`
45+
- raw database queries, shard routing: `packages/database/src/repositories`
6146
- schema/migrations: `packages/database`
62-
- async processing: `apps/worker` + `packages/worker-config`
63-
- external channel protocol: `integrations/<channel>`
47+
- async processing (calls a service, never `db`): `apps/worker` + `packages/worker-config`
48+
- external channel protocol (calls a service, never `db`): `integrations/<channel>`
6449
3. Search for a similar feature and mirror naming, imports, error handling, and tests.
6550
4. Check `.agents/rules/*` for local invariants, especially data access and git.
6651
5. Keep changes scoped to the user request; do not refactor legacy exceptions unless required.

0 commit comments

Comments
 (0)