create-mercato-app 0.5.1-develop.3045.b4b3320cc2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agentic/shared/AGENTS.md.template +40 -5
- package/agentic/shared/ai/qa/tests/playwright.config.ts +5 -0
- package/agentic/shared/ai/skills/auto-continue-pr-loop/SKILL.md +592 -0
- package/agentic/shared/ai/skills/auto-create-pr-loop/SKILL.md +749 -0
- package/agentic/shared/ai/skills/data-model-design/SKILL.md +91 -3
- package/agentic/shared/ai/skills/implement-spec/SKILL.md +9 -5
- package/agentic/shared/ai/skills/module-scaffold/SKILL.md +57 -7
- package/agentic/shared/ai/skills/spec-writing/SKILL.md +11 -2
- package/agentic/shared/ai/skills/spec-writing/references/spec-checklist.md +24 -7
- package/dist/agentic/shared/AGENTS.md.template +40 -5
- package/dist/agentic/shared/ai/qa/tests/playwright.config.ts +5 -0
- package/dist/agentic/shared/ai/skills/auto-continue-pr-loop/SKILL.md +592 -0
- package/dist/agentic/shared/ai/skills/auto-create-pr-loop/SKILL.md +749 -0
- package/dist/agentic/shared/ai/skills/data-model-design/SKILL.md +91 -3
- package/dist/agentic/shared/ai/skills/implement-spec/SKILL.md +9 -5
- package/dist/agentic/shared/ai/skills/module-scaffold/SKILL.md +57 -7
- package/dist/agentic/shared/ai/skills/spec-writing/SKILL.md +11 -2
- package/dist/agentic/shared/ai/skills/spec-writing/references/spec-checklist.md +24 -7
- package/dist/index.js +17 -5
- package/package.json +2 -3
- package/template/AGENTS.md +81 -2
- package/template/package.json.template +31 -30
- package/template/scripts/dev-reset.mjs +27 -0
- package/template/src/app/globals.css +5 -0
- package/template/src/i18n/de.json +2 -0
- package/template/src/i18n/en.json +2 -0
- package/template/src/i18n/es.json +2 -0
- package/template/src/i18n/pl.json +2 -0
|
@@ -16,7 +16,8 @@ Design entities, relationships, and manage the migration lifecycle following Ope
|
|
|
16
16
|
5. [Cross-Module References](#5-cross-module-references)
|
|
17
17
|
6. [Migration Lifecycle](#6-migration-lifecycle)
|
|
18
18
|
7. [Advanced Patterns](#7-advanced-patterns)
|
|
19
|
-
8. [
|
|
19
|
+
8. [Sensitive Data and Encryption Maps](#8-sensitive-data-and-encryption-maps)
|
|
20
|
+
9. [Anti-Patterns](#9-anti-patterns)
|
|
20
21
|
|
|
21
22
|
---
|
|
22
23
|
|
|
@@ -483,7 +484,89 @@ export class TicketHistory {
|
|
|
483
484
|
|
|
484
485
|
---
|
|
485
486
|
|
|
486
|
-
## 8.
|
|
487
|
+
## 8. Sensitive Data and Encryption Maps
|
|
488
|
+
|
|
489
|
+
When the developer asks for "we need this column encrypted", "store this securely", "this is PII", "GDPR", or "encryption at rest" — and whenever you are designing a column that will hold names, addresses, contact information, free-text notes about people, integration credentials, secrets, or any data subject to a data-processing agreement — use the framework's **encryption-maps mechanism**. Do NOT hand-roll AES, raw `crypto.subtle`, custom KMS calls, or "TODO encrypt later" stubs.
|
|
490
|
+
|
|
491
|
+
The mechanism gives you:
|
|
492
|
+
|
|
493
|
+
- Per-tenant Data Encryption Keys (DEKs) resolved through the configured KMS (Vault by default, env-fallback in dev).
|
|
494
|
+
- Declarative, per-entity, per-field encryption with optional deterministic-hash sibling columns for equality lookups (for example login by email).
|
|
495
|
+
- Boot-time auto-application: every enabled module's `defaultEncryptionMaps` is collected during `auth:setup` and applied when `TENANT_DATA_ENCRYPTION=yes`.
|
|
496
|
+
- A `findWithDecryption` / `findOneWithDecryption` read API that transparently decrypts on read.
|
|
497
|
+
|
|
498
|
+
### When encryption is mandatory
|
|
499
|
+
|
|
500
|
+
| Field example | Encrypt? |
|
|
501
|
+
|---|---|
|
|
502
|
+
| First name, last name, preferred name | Yes |
|
|
503
|
+
| Email, phone | Yes — usually with a `hashField` for lookups |
|
|
504
|
+
| Postal address (line 1/2, city, region, postal code, country) | Yes |
|
|
505
|
+
| Free-text comments / notes / activity bodies that mention people | Yes |
|
|
506
|
+
| Integration secrets, API keys, OAuth tokens, webhook signing keys | Yes |
|
|
507
|
+
| Document numbers (tax IDs, national IDs) | Yes |
|
|
508
|
+
| Status enums, counters, timestamps, FKs, currency codes | No |
|
|
509
|
+
| Public catalog metadata (product titles for a public storefront) | Usually no |
|
|
510
|
+
|
|
511
|
+
If you are unsure, default to encrypting and confirm with the user — re-introducing encryption later requires a backfill, but turning it off later is a single map edit.
|
|
512
|
+
|
|
513
|
+
### Declare the map in `<module>/encryption.ts`
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
import type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryption'
|
|
517
|
+
|
|
518
|
+
export const defaultEncryptionMaps: ModuleEncryptionMap[] = [
|
|
519
|
+
{
|
|
520
|
+
entityId: '<module_id>:<entity>', // matches the entity's table id (colon-separated)
|
|
521
|
+
fields: [
|
|
522
|
+
{ field: 'first_name' },
|
|
523
|
+
{ field: 'last_name' },
|
|
524
|
+
{ field: 'phone' },
|
|
525
|
+
// Sibling deterministic hash for equality lookups (e.g. login by email).
|
|
526
|
+
// Add a matching `<field>_hash varchar` column to the entity.
|
|
527
|
+
{ field: 'email', hashField: 'email_hash' },
|
|
528
|
+
],
|
|
529
|
+
},
|
|
530
|
+
]
|
|
531
|
+
|
|
532
|
+
export default defaultEncryptionMaps
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Read with decryption — never raw `em.find`
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
539
|
+
|
|
540
|
+
// Signature: (em, entityName, where, options?, scope?). MikroORM FindOptions go in slot 4
|
|
541
|
+
// (pass `undefined` if you have none), the decryption scope `{ tenantId, organizationId }` in slot 5.
|
|
542
|
+
const records = await findWithDecryption(em, '<Entity>', filter, undefined, { tenantId, organizationId })
|
|
543
|
+
const single = await findOneWithDecryption(em, '<Entity>', { id }, undefined, { tenantId, organizationId })
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
Calling `em.find` on an encrypted column returns ciphertext, breaks search, and silently leaks bug surface. The `findWithDecryption` family is the one entry point.
|
|
547
|
+
|
|
548
|
+
### Apply maps to existing tenants
|
|
549
|
+
|
|
550
|
+
```bash
|
|
551
|
+
yarn mercato entities seed-encryption --tenant <tenantId> [--organization <orgId>]
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
New tenants pick up the maps automatically during `auth:setup`. Toggling the **Encrypted** flag on a custom field via the admin UI also only applies to data written **after** the change — backfill historical plaintext rows by running `yarn mercato entities rotate-encryption-key --tenant <tenantId> --org <organizationId>` (without `--old-key` it skips already-encrypted fields and just encrypts plaintext). Use `yarn mercato entities decrypt-database` to roll back. For full UI flows and CLI options see <https://docs.open-mercato.dev/user-guide/encryption>.
|
|
555
|
+
|
|
556
|
+
### Vector search caveat
|
|
557
|
+
|
|
558
|
+
The `vector` module stores raw embeddings unencrypted in the vector store (e.g. pgvector). Even though the source text is decrypted only transiently to compute embeddings, treat the embeddings as sensitive: avoid embedding raw high-sensitivity text and rely on disk-level / managed-database encryption-at-rest for the vector column.
|
|
559
|
+
|
|
560
|
+
### Environment switches
|
|
561
|
+
|
|
562
|
+
- `TENANT_DATA_ENCRYPTION=yes|no` (default `yes`) — set to `no` to run the hooks as no-op (validation still applies).
|
|
563
|
+
- `TENANT_DATA_ENCRYPTION_DEBUG=yes` — log map evaluation, KMS calls, cache hits.
|
|
564
|
+
- `VAULT_ADDR` / `VAULT_TOKEN` / `VAULT_KV_PATH` — HashiCorp Vault KMS configuration.
|
|
565
|
+
- `TENANT_DATA_ENCRYPTION_FALLBACK_KEY` — local/dev fallback key when Vault is unavailable. In dev, `AUTH_SECRET` / `NEXTAUTH_SECRET` is used as a last resort; production falls back to noop KMS.
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## 9. Anti-Patterns
|
|
487
570
|
|
|
488
571
|
| Anti-Pattern | Problem | Correct Pattern |
|
|
489
572
|
|-------------|---------|-----------------|
|
|
@@ -498,6 +581,10 @@ export class TicketHistory {
|
|
|
498
581
|
| Storing arrays as comma-separated strings | Can't query, no integrity | Use `jsonb` arrays or junction tables |
|
|
499
582
|
| UUID FK without index | Slow joins | Always `@Index()` on FK columns |
|
|
500
583
|
| Nullable required fields | Data integrity issues | Use `!` assertion for required, `null` for optional |
|
|
584
|
+
| Hand-rolled AES / `crypto.subtle` / custom KMS for sensitive columns | Per-tenant key isolation, hash lookups, key rotation, and admin UI all break | Declare `<module>/encryption.ts` with `defaultEncryptionMaps`; let the framework manage DEKs and Vault |
|
|
585
|
+
| Reading encrypted columns with raw `em.find` / `em.findOne` | Returns ciphertext, breaks search, silent data corruption | Use `findWithDecryption` / `findOneWithDecryption` with `{ tenantId, organizationId }` |
|
|
586
|
+
| Storing PII as plaintext "for now" / TODO comments | GDPR violation, leaks at rest, expensive backfill later | Encrypt from day one; toggling later only protects new writes |
|
|
587
|
+
| Encrypting an `email` column without a `hashField` | Login / equality lookups stop working | Declare a sibling `hashField` (e.g. `email_hash`) in the encryption map and add the matching `varchar` column |
|
|
501
588
|
|
|
502
589
|
---
|
|
503
590
|
|
|
@@ -515,7 +602,8 @@ export class TicketHistory {
|
|
|
515
602
|
- **MUST** specify `length` on all `varchar` columns
|
|
516
603
|
- **MUST NOT** use ORM relationship decorators across module boundaries
|
|
517
604
|
- **MUST NOT** rename or drop columns in a single release
|
|
518
|
-
- **MUST
|
|
605
|
+
- **MUST** declare encrypted columns in `<module>/encryption.ts` exporting `defaultEncryptionMaps: ModuleEncryptionMap[]`, and read them via `findWithDecryption` / `findOneWithDecryption` from `@open-mercato/shared/lib/encryption/find` — see section 8
|
|
606
|
+
- **MUST NOT** hand-roll AES / KMS calls or store sensitive columns as plaintext "for now" — use the encryption-maps mechanism in section 8
|
|
519
607
|
- Use `jsonb` for flexible/nested data, proper columns for queryable/sortable data
|
|
520
608
|
- Use junction tables for many-to-many relationships
|
|
521
609
|
- Derive TypeScript types from Zod schemas, never duplicate type definitions
|
|
@@ -40,14 +40,18 @@ For every piece of code, enforce these code-review rules inline:
|
|
|
40
40
|
| Area | Rule |
|
|
41
41
|
|------|------|
|
|
42
42
|
| Types | No `any` — use zod + `z.infer` |
|
|
43
|
-
| API routes | Export `openApi` and `metadata` with
|
|
44
|
-
|
|
|
43
|
+
| API routes | Export `openApi` and per-method `metadata` with `requireAuth` / `requireFeatures` (no top-level `export const requireAuth`) |
|
|
44
|
+
| **CRUD APIs** | **Use `makeCrudRoute({ entity, entityId, operations, schema, indexer: { entityType } })` from `@open-mercato/shared/lib/crud/factory`. Custom write routes MUST call `validateCrudMutationGuard` before the mutation and `runCrudMutationGuardAfterSuccess` after success. See `AGENTS.md` → Mandatory Module Mechanisms.** |
|
|
45
|
+
| Entities | Standard columns, snake_case, UUID PKs, indexed `organization_id` + `tenant_id` |
|
|
45
46
|
| Security | `findWithDecryption`, tenant scoping, zod validation |
|
|
46
|
-
|
|
|
47
|
-
|
|
|
47
|
+
| **Encryption maps** | **For every PII / GDPR-relevant column the phase touches, declare in `<module>/encryption.ts` exporting `defaultEncryptionMaps` (type from `@open-mercato/shared/modules/encryption`). Reads via `findWithDecryption` / `findOneWithDecryption` (5-arg `(em, entity, where, options?, scope?)`). Equality-lookup columns declare a sibling `hashField`. NEVER hand-rolled AES/KMS, `crypto.subtle`, or "encrypt later" stubs. See `AGENTS.md` → CRITICAL Rule #11 (Encryption maps) + the "Encryption maps" row of the Mandatory Module Mechanisms table; `.ai/skills/data-model-design/SKILL.md` § Sensitive Data and Encryption Maps; `.ai/skills/module-scaffold/SKILL.md` § Encryption maps.** |
|
|
48
|
+
| UI | `<CrudForm>`/`<DataTable>` (with stable `entityId` + `extensionTableId`), `apiCall` (never raw `fetch`), `flash()`, `<LoadingMessage>`/`<ErrorMessage>` |
|
|
49
|
+
| **Design System** | **Semantic status tokens (no `text-red-*` / `bg-green-*`); Tailwind text scale (no `text-[13px]` / `text-[11px]`); shared primitives `StatusBadge` / `Alert` / `FormField` / `SectionHeader` / `CollapsibleSection` / `LoadingMessage` / `Spinner` / `DataLoader` / `EmptyState`; lucide-react icons in PAGE BODY (never inline `<svg>`); `aria-label` on every icon-only button; Boy Scout rule on touched lines. See `AGENTS.md` → CRITICAL Rule #10 (Strict Design System alignment) + `.ai/skills/backend-ui-design/SKILL.md`.** |
|
|
50
|
+
| **Cache** | **Resolve via DI (`container.resolve('cache')`); tag with `tenant:<id>` / `org:<id>`; declare invalidation per write path. NEVER `new Redis(...)` or raw SQLite.** |
|
|
51
|
+
| Events | `createModuleEvents()` with `as const`, subscribers export `metadata`; cross-module side effects via subscribers, never direct imports |
|
|
48
52
|
| i18n | `useT()` client, `resolveTranslations()` server, no hardcoded strings |
|
|
49
53
|
| Imports | Package-level `@open-mercato/<pkg>/...` for framework imports |
|
|
50
|
-
| Mutations | `useGuardedMutation` when not using CrudForm |
|
|
54
|
+
| Mutations | `useGuardedMutation` when not using CrudForm; pass `retryLastMutation` in injection context |
|
|
51
55
|
| Keyboard | `Cmd/Ctrl+Enter` submit, `Escape` cancel on dialogs |
|
|
52
56
|
| Naming | Modules plural snake_case, events `module.entity.past_tense`, features `module.action` |
|
|
53
57
|
|
|
@@ -41,8 +41,9 @@ Before writing any code, ask the developer:
|
|
|
41
41
|
- [ ] Background workers
|
|
42
42
|
- [ ] CLI commands
|
|
43
43
|
- [ ] Custom fields support
|
|
44
|
+
- [ ] **Sensitive / GDPR-relevant fields** (PII, contact info, addresses, free-text notes about people, integration credentials, secrets) — if yes, an `encryption.ts` declaring `defaultEncryptionMaps` is mandatory; see section 11 → Encryption maps
|
|
44
45
|
|
|
45
|
-
If the developer provides a brief description, infer reasonable defaults and confirm.
|
|
46
|
+
If the developer provides a brief description, infer reasonable defaults and confirm. When key fields include names, emails, phones, addresses, free-text comments, or external API keys, treat the encryption checkbox as `yes` by default and confirm with the user rather than skipping it silently.
|
|
46
47
|
|
|
47
48
|
---
|
|
48
49
|
|
|
@@ -57,6 +58,7 @@ src/modules/<module_id>/
|
|
|
57
58
|
├── setup.ts # Tenant init, role features
|
|
58
59
|
├── di.ts # Awilix DI registrations
|
|
59
60
|
├── events.ts # Typed event declarations (if needed)
|
|
61
|
+
├── encryption.ts # Tenant data encryption maps (only if entity has sensitive/GDPR fields)
|
|
60
62
|
├── data/
|
|
61
63
|
│ ├── entities.ts # MikroORM entity classes
|
|
62
64
|
│ └── validators.ts # Zod validation schemas
|
|
@@ -178,7 +180,7 @@ Use `makeCrudRoute` for standard CRUD. Each HTTP method lives in its own file.
|
|
|
178
180
|
**File**: `src/modules/<module_id>/api/get/<entities>.ts`
|
|
179
181
|
|
|
180
182
|
```typescript
|
|
181
|
-
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/
|
|
183
|
+
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
182
184
|
import { <Entity> } from '../../data/entities'
|
|
183
185
|
|
|
184
186
|
const handler = makeCrudRoute({
|
|
@@ -201,7 +203,7 @@ export const openApi = {
|
|
|
201
203
|
**File**: `src/modules/<module_id>/api/post/<entities>.ts`
|
|
202
204
|
|
|
203
205
|
```typescript
|
|
204
|
-
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/
|
|
206
|
+
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
205
207
|
import { <Entity> } from '../../data/entities'
|
|
206
208
|
import { create<Entity>Schema } from '../../data/validators'
|
|
207
209
|
|
|
@@ -225,7 +227,7 @@ export const openApi = {
|
|
|
225
227
|
**File**: `src/modules/<module_id>/api/put/<entities>.ts`
|
|
226
228
|
|
|
227
229
|
```typescript
|
|
228
|
-
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/
|
|
230
|
+
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
229
231
|
import { <Entity> } from '../../data/entities'
|
|
230
232
|
import { update<Entity>Schema } from '../../data/validators'
|
|
231
233
|
|
|
@@ -249,7 +251,7 @@ export const openApi = {
|
|
|
249
251
|
**File**: `src/modules/<module_id>/api/delete/<entities>.ts`
|
|
250
252
|
|
|
251
253
|
```typescript
|
|
252
|
-
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/
|
|
254
|
+
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
253
255
|
import { <Entity> } from '../../data/entities'
|
|
254
256
|
|
|
255
257
|
const handler = makeCrudRoute({
|
|
@@ -346,7 +348,7 @@ export const metadata = {
|
|
|
346
348
|
|
|
347
349
|
```tsx
|
|
348
350
|
'use client'
|
|
349
|
-
import { CrudForm } from '@open-mercato/ui/backend/
|
|
351
|
+
import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
|
|
350
352
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
351
353
|
|
|
352
354
|
export default function Create<Entity>Page() {
|
|
@@ -384,7 +386,7 @@ export const metadata = {
|
|
|
384
386
|
|
|
385
387
|
```tsx
|
|
386
388
|
'use client'
|
|
387
|
-
import { CrudForm } from '@open-mercato/ui/backend/
|
|
389
|
+
import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
|
|
388
390
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
389
391
|
|
|
390
392
|
export default function Edit<Entity>Page({ params }: { params: { id: string } }) {
|
|
@@ -573,6 +575,52 @@ export default function registerCli(program: any) {
|
|
|
573
575
|
}
|
|
574
576
|
```
|
|
575
577
|
|
|
578
|
+
### Encryption maps (sensitive / GDPR-relevant fields)
|
|
579
|
+
|
|
580
|
+
**Mandatory** when the entity stores PII, contact info, addresses, free-text notes about people, integration credentials, secrets, or anything subject to a data-processing agreement. Do NOT hand-roll AES, KMS calls, or "TODO encrypt later" stubs — the framework provides per-tenant DEKs and a declarative field-level map.
|
|
581
|
+
|
|
582
|
+
**File**: `src/modules/<module_id>/encryption.ts`
|
|
583
|
+
|
|
584
|
+
```typescript
|
|
585
|
+
import type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryption'
|
|
586
|
+
|
|
587
|
+
export const defaultEncryptionMaps: ModuleEncryptionMap[] = [
|
|
588
|
+
{
|
|
589
|
+
entityId: '<module_id>:<entity>', // matches data/entities.ts table id, colon-separated
|
|
590
|
+
fields: [
|
|
591
|
+
{ field: 'first_name' },
|
|
592
|
+
{ field: 'last_name' },
|
|
593
|
+
{ field: 'phone' },
|
|
594
|
+
// Add a hashField for deterministic equality lookups (e.g. login by email):
|
|
595
|
+
{ field: 'email', hashField: 'email_hash' },
|
|
596
|
+
],
|
|
597
|
+
},
|
|
598
|
+
]
|
|
599
|
+
|
|
600
|
+
export default defaultEncryptionMaps
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
**Read paths** — never `em.find` an encrypted column directly:
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
607
|
+
|
|
608
|
+
// Signature: (em, entityName, where, options?, scope?) — MikroORM FindOptions in slot 4
|
|
609
|
+
// (pass `undefined` when none), decryption scope in slot 5.
|
|
610
|
+
const records = await findWithDecryption(em, '<Entity>', filter, undefined, { tenantId, organizationId })
|
|
611
|
+
const single = await findOneWithDecryption(em, '<Entity>', { id }, undefined, { tenantId, organizationId })
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
**Apply to existing tenants** after declaring or updating maps:
|
|
615
|
+
|
|
616
|
+
```bash
|
|
617
|
+
yarn mercato entities seed-encryption --tenant <tenantId> [--organization <orgId>]
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
New tenants pick up `defaultEncryptionMaps` automatically during `auth:setup`. Toggling the **Encrypted** flag for a field only applies to data written **after** the change — historical plaintext rows stay as they were until backfilled via `yarn mercato entities rotate-encryption-key --tenant <tenantId> --org <organizationId>` (without `--old-key` the command only encrypts plaintext and skips already-encrypted fields). Use `yarn mercato entities decrypt-database` to roll back. For end-to-end usage and admin UI flows see <https://docs.open-mercato.dev/user-guide/encryption>.
|
|
621
|
+
|
|
622
|
+
> Tip: when `email` (or any other column) needs deterministic lookups while encrypted, declare a sibling `hashField` in the map and add a matching `varchar` column to the entity. The framework keeps the hash in sync on writes; queries can target the hash instead of the cleartext column.
|
|
623
|
+
|
|
576
624
|
---
|
|
577
625
|
|
|
578
626
|
## 12. Wire & Verify
|
|
@@ -658,3 +706,5 @@ yarn dev # Start dev server
|
|
|
658
706
|
- **MUST NOT** run `yarn db:migrate` without explicit user confirmation
|
|
659
707
|
- **MUST NOT** create ORM relationships (`@ManyToOne`, `@OneToMany`) to entities in other modules
|
|
660
708
|
- **MUST NOT** edit `.mercato/generated/*` files manually
|
|
709
|
+
- **MUST** declare `<module>/encryption.ts` exporting `defaultEncryptionMaps` whenever the entity stores sensitive / GDPR-relevant fields (PII, contact info, addresses, free-text notes about people, integration credentials, secrets) — and read those columns via `findWithDecryption` / `findOneWithDecryption`
|
|
710
|
+
- **MUST NOT** hand-roll AES/KMS calls or store "we'll encrypt this later" plaintext for sensitive columns — use the encryption-maps mechanism described in section 11 → Encryption maps
|
|
@@ -60,6 +60,9 @@ Use the [Specification Template](references/spec-template.md). Adapt if needed,
|
|
|
60
60
|
3. **Singularity Law**: Singular naming for entities, commands, events, feature IDs.
|
|
61
61
|
4. **Undo Contract**: Is the "Undo" logic as detailed as the "Execute"?
|
|
62
62
|
5. **Module Isolation**: Using Event Bus for side effects or cheating with direct imports?
|
|
63
|
+
6. **Canonical Mechanisms**: Does the spec reach for the framework primitives (`makeCrudRoute`, `<CrudForm>`, `<DataTable>`, `apiCall` / `useGuardedMutation`, DI-resolved cache, `createModuleEvents`) or invent its own substitute? See `AGENTS.md` → **Mandatory Module Mechanisms** for the full canon and links. No raw `fetch`, no raw `<form>`, no `new Redis(...)`, no manual cross-module ORM joins.
|
|
64
|
+
7. **Sensitive Data**: For every PII / GDPR / address / contact / free-text-about-people / integration-credential column the spec proposes, does it declare an `encryption.ts` `defaultEncryptionMaps` entry and route reads through `findWithDecryption`? See `AGENTS.md` → CRITICAL Rule #11 (Encryption maps) + the "Encryption maps for sensitive data" row of the Mandatory Module Mechanisms table and `.ai/skills/data-model-design/SKILL.md` § Sensitive Data and Encryption Maps. No hand-rolled AES, no `crypto.subtle`, no "TODO encrypt later".
|
|
65
|
+
8. **Design System**: Does every UI mock / className snippet in the spec match the DS canon — semantic status tokens (no `text-red-*` / `bg-green-*`), Tailwind text scale (no `text-[11px]` / `text-[13px]`), shared primitives (`StatusBadge`, `Alert`, `FormField`, `SectionHeader`, `CollapsibleSection`, `LoadingMessage` / `Spinner` / `DataLoader`, `EmptyState`), lucide-react icons in page body (never inline `<svg>`), dialog `Cmd/Ctrl+Enter` submit + `Escape` cancel, `aria-label` on every icon-only button? See `AGENTS.md` → CRITICAL Rule #10 (Strict Design System alignment for every UI change) and `.ai/skills/backend-ui-design/SKILL.md`. Specs that touch existing pages MUST honour the Boy Scout rule.
|
|
63
66
|
|
|
64
67
|
## Quick Rule Reference
|
|
65
68
|
|
|
@@ -68,9 +71,15 @@ Use the [Specification Template](references/spec-template.md). Adapt if needed,
|
|
|
68
71
|
- **`organization_id`** is mandatory for all tenant-scoped entities.
|
|
69
72
|
- **Undoability** is the default for state changes.
|
|
70
73
|
- **Zod validation** for all API inputs.
|
|
74
|
+
- **Encryption maps** for every sensitive / GDPR-relevant column (declare in `<module>/encryption.ts`, read via `findWithDecryption`) — see `AGENTS.md` → Data Encryption.
|
|
75
|
+
- **Canonical primitives** for CRUD APIs (`makeCrudRoute`), backend forms (`CrudForm`), tables (`DataTable`), HTTP (`apiCall` — never raw `fetch`), non-`CrudForm` writes (`useGuardedMutation`), cache (DI-resolved `@open-mercato/cache`), events (`createModuleEvents`) — see `AGENTS.md` → Mandatory Module Mechanisms.
|
|
76
|
+
- **Design System** tokens and shared UI primitives — no hardcoded status colors, no arbitrary text sizes, no inline `<svg>` in page-body UI. See `AGENTS.md` → Design System.
|
|
71
77
|
|
|
72
78
|
## Reference Materials
|
|
73
79
|
|
|
74
80
|
- [Spec Template](references/spec-template.md)
|
|
75
|
-
- [Spec Checklist](references/spec-checklist.md)
|
|
76
|
-
- [AGENTS.md](../../../AGENTS.md)
|
|
81
|
+
- [Spec Checklist](references/spec-checklist.md) — § 3 covers encryption maps; § 5 covers canonical mechanisms + DS
|
|
82
|
+
- [AGENTS.md](../../../AGENTS.md) — Mandatory Module Mechanisms table; CRITICAL Rule #10 (Design System); CRITICAL Rule #11 (Encryption maps)
|
|
83
|
+
- [`.ai/skills/data-model-design/SKILL.md`](../data-model-design/SKILL.md) → Sensitive Data and Encryption Maps
|
|
84
|
+
- [`.ai/skills/module-scaffold/SKILL.md`](../module-scaffold/SKILL.md) → Encryption maps
|
|
85
|
+
- [`.ai/skills/backend-ui-design/SKILL.md`](../backend-ui-design/SKILL.md) — DS-compliant pages
|
|
@@ -19,26 +19,42 @@ Every item must be answered in the spec or marked N/A with justification.
|
|
|
19
19
|
|
|
20
20
|
## 3. Data Integrity & Security
|
|
21
21
|
|
|
22
|
-
- [ ] Entities include `id`, `organization_id`, `created_at`, `updated_at`
|
|
22
|
+
- [ ] Entities include `id`, `organization_id`, `tenant_id`, `created_at`, `updated_at`, `deleted_at`, `is_active`
|
|
23
23
|
- [ ] Write operations define transaction boundaries
|
|
24
24
|
- [ ] Input validation uses zod schemas
|
|
25
25
|
- [ ] All user input validated before business logic/persistence
|
|
26
|
-
- [ ] Auth guards
|
|
27
|
-
- [ ] Tenant isolation: every scoped query filters by `organization_id`
|
|
26
|
+
- [ ] Auth guards declared per-method in `metadata` (`requireAuth`, `requireFeatures`) — never legacy top-level `export const requireAuth`
|
|
27
|
+
- [ ] Tenant isolation: every scoped query filters by `organization_id` (and `tenant_id` where applicable)
|
|
28
|
+
- [ ] **Encryption maps mechanism is used (no hand-rolled crypto).** For every PII / GDPR-relevant column the spec proposes — names, addresses, contacts, free-text notes about people, integration credentials, secrets, document numbers — the spec MUST declare them in a module-level `<module>/encryption.ts` exporting `defaultEncryptionMaps: ModuleEncryptionMap[]` (type from `@open-mercato/shared/modules/encryption`). Reads MUST go through `findWithDecryption` / `findOneWithDecryption` (5-arg `(em, entity, where, options?, scope?)`) from `@open-mercato/shared/lib/encryption/find`. Equality-lookup columns (e.g. login email) declare a sibling `hashField`. No `crypto.subtle`, no custom KMS calls, no "TODO encrypt later". See `AGENTS.md` → CRITICAL Rule #11 (Encryption maps) + the "Encryption maps for sensitive data" row of the Mandatory Module Mechanisms table; `.ai/skills/data-model-design/SKILL.md` § Sensitive Data and Encryption Maps.
|
|
28
29
|
|
|
29
30
|
## 4. Commands, Events & Naming
|
|
30
31
|
|
|
31
32
|
- [ ] Naming is singular and consistent
|
|
32
33
|
- [ ] All mutations are commands with undo logic
|
|
33
|
-
- [ ] Events declared in
|
|
34
|
+
- [ ] Events declared in `<module>/events.ts` via `createModuleEvents({ moduleId, events } as const)` before emitting; cross-module side effects use `subscribers/*.ts`, never direct cross-module imports
|
|
34
35
|
- [ ] Side-effect reversibility is documented
|
|
35
36
|
|
|
36
|
-
## 5. API & UI
|
|
37
|
+
## 5. API & UI — Canonical Mechanisms
|
|
37
38
|
|
|
38
39
|
- [ ] API contracts are complete (request/response/errors)
|
|
39
40
|
- [ ] Routes include `openApi` expectations
|
|
40
|
-
- [ ]
|
|
41
|
-
- [ ]
|
|
41
|
+
- [ ] **Canonical mechanisms — no DIY substitutes.** The spec MUST reach for the framework primitives, not invent its own. See `AGENTS.md` → **Mandatory Module Mechanisms**.
|
|
42
|
+
- [ ] **CRUD APIs** use `makeCrudRoute({ entity, entityId, operations, schema, indexer: { entityType } })` from `@open-mercato/shared/lib/crud/factory`. Custom write routes call `validateCrudMutationGuard` before mutation and `runCrudMutationGuardAfterSuccess` after.
|
|
43
|
+
- [ ] **API route files export `metadata`** with per-method `requireAuth` / `requireFeatures` (no top-level `export const requireAuth`).
|
|
44
|
+
- [ ] **Backend forms** use `<CrudForm>` from `@open-mercato/ui/backend/CrudForm` with helpers `createCrud` / `updateCrud` / `deleteCrud` from `@open-mercato/ui/backend/utils/crud`, throwing `createCrudFormError` from `@open-mercato/ui/backend/utils/serverErrors` for field-level errors. No raw `<form>`, no raw `fetch`.
|
|
45
|
+
- [ ] **Lists** use `<DataTable entityId apiPath columns />` from `@open-mercato/ui/backend/DataTable` with stable `entityId` / `extensionTableId` so widget injection (columns / row actions / bulk actions / filters / toolbar) keeps working.
|
|
46
|
+
- [ ] **HTTP clients** use `apiCall` / `apiCallOrThrow` / `readApiResultOrThrow` from `@open-mercato/ui/backend/utils/apiCall` — never raw `fetch`.
|
|
47
|
+
- [ ] **Non-`CrudForm` writes** are wrapped in `useGuardedMutation(...).runMutation(...)` and pass `retryLastMutation` in the injection context.
|
|
48
|
+
- [ ] **Cache** is resolved via DI (`container.resolve('cache')`) — never `new Redis(...)` or raw SQLite. Tags include `tenant:<id>` / `org:<id>`.
|
|
49
|
+
- [ ] **Design System compliance for every UI mock and className snippet in the spec.** See `AGENTS.md` → CRITICAL Rule #10 (Strict Design System alignment) and `.ai/skills/backend-ui-design/SKILL.md`.
|
|
50
|
+
- [ ] Use semantic status tokens (`text-status-error-text`, `bg-status-success-bg`, `border-status-warning-border`, `text-status-info-icon`, `text-destructive`, `bg-destructive`) — NEVER hardcoded shades like `text-red-500`, `bg-green-100`, `text-amber-*`, `text-emerald-*`, `bg-blue-*`. Status tokens already cover dark mode; no `dark:` overrides.
|
|
51
|
+
- [ ] Use the Tailwind text scale (`text-xs` 12, `text-sm` 14, `text-base` 16, `text-lg` 18, `text-xl` 20, `text-2xl` 24) or `text-overline` for 11px uppercase labels — NEVER arbitrary sizes (`text-[11px]`, `text-[13px]`, `text-[15px]`, `p-[13px]`, `rounded-[24px]`, `z-[9999]`).
|
|
52
|
+
- [ ] Use shared primitives instead of raw HTML: `<Alert variant=...>` for inline status, `flash('msg', 'success|error|warning|info')` for toasts, `useConfirmDialog()` for destructive confirmations, `<StatusBadge>` for entity status, `<FormField label error>` to wrap form inputs, `<SectionHeader title count action>` for section headers, `<CollapsibleSection>` for collapsible regions, `<LoadingMessage>` / `<Spinner>` / `<DataLoader>` for async states, `<EmptyState>` (or DataTable `emptyState` prop) for empty lists.
|
|
53
|
+
- [ ] Use lucide-react icons in PAGE BODY UI (`Page`, `DataTable`, `CrudForm`, cards, buttons) — never inline `<svg>`. Sizes from the `size-{3|4|5|6}` scale; `strokeWidth` is not overridden per-instance. `page.meta.ts` icons follow the `React.createElement('svg', …)` pattern.
|
|
54
|
+
- [ ] Every dialog supports `Cmd/Ctrl+Enter` to submit and `Escape` to cancel.
|
|
55
|
+
- [ ] Every icon-only button has an `aria-label`.
|
|
56
|
+
- [ ] Boy Scout rule: when the spec edits an existing page, any line touched gets migrated to semantic tokens / DS scale.
|
|
57
|
+
- [ ] i18n keys are planned for user-facing strings (`useT()` client-side, `resolveTranslations()` server-side; never hard-coded labels)
|
|
42
58
|
- [ ] Pagination limits defined (`pageSize <= 100`)
|
|
43
59
|
|
|
44
60
|
## 6. Risks & Anti-Patterns
|
|
@@ -48,3 +64,4 @@ Every item must be answered in the spec or marked N/A with justification.
|
|
|
48
64
|
- [ ] Does not introduce cross-module ORM links
|
|
49
65
|
- [ ] Does not skip undoability for state changes
|
|
50
66
|
- [ ] Does not mix MVP with speculative future phases
|
|
67
|
+
- [ ] Does not introduce hand-rolled AES, raw `fetch`, raw `<form>`, `new Redis(...)`, or arbitrary Tailwind sizes / status colors
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent Context Routing — {{PROJECT_NAME}}
|
|
2
2
|
|
|
3
|
-
**MANDATORY CONTEXT LOADING** — see Critical Rule
|
|
3
|
+
**MANDATORY CONTEXT LOADING** — see the "BEFORE writing ANY code" Critical Rule below.
|
|
4
4
|
Before writing code, find your task below and `Read` the listed files.
|
|
5
5
|
Do NOT load the entire src/ tree — Open Mercato apps can have many modules.
|
|
6
6
|
|
|
@@ -51,7 +51,8 @@ step, you WILL produce incorrect imports and miss required patterns.
|
|
|
51
51
|
| Add caching | `.ai/guides/cache.md` |
|
|
52
52
|
| Add background workers | `.ai/guides/queue.md` |
|
|
53
53
|
| Use i18n (translations) | `.ai/guides/shared.md` → i18n |
|
|
54
|
-
| Use encrypted queries | `.ai/guides/shared.md` → Encryption |
|
|
54
|
+
| Use encrypted queries (read sensitive columns that already have an encryption map; for authoring a NEW sensitive column see the row below first) | `.ai/guides/shared.md` → Encryption |
|
|
55
|
+
| **Encrypt sensitive / GDPR-relevant fields** ("we need this column encrypted", "store this securely", "this is PII", "GDPR", "encryption at rest", addresses, contact info, free-text notes about people, integration credentials, secrets) — declare them in the framework's encryption-maps mechanism, never hand-rolled AES/KMS | `.ai/skills/data-model-design/SKILL.md` → Sensitive Data and Encryption Maps, then `.ai/skills/module-scaffold/SKILL.md` → Encryption maps. Reference: <https://docs.open-mercato.dev/user-guide/encryption> |
|
|
55
56
|
| Use apiCall / UI components | `.ai/guides/ui.md` |
|
|
56
57
|
| Add permissions (RBAC) | `.ai/guides/core.md` → Access Control |
|
|
57
58
|
| Add notifications | `.ai/guides/core.md` → Notifications |
|
|
@@ -90,6 +91,8 @@ These guides ship automatically when the corresponding module is installed.
|
|
|
90
91
|
|---|---|
|
|
91
92
|
| Delegate an arbitrary task end-to-end as a PR | `.ai/skills/auto-create-pr/SKILL.md` |
|
|
92
93
|
| Resume an in-progress agent PR | `.ai/skills/auto-continue-pr/SKILL.md` |
|
|
94
|
+
| Run a long multi-step spec implementation with resumable checkpoints | `.ai/skills/auto-create-pr-loop/SKILL.md` |
|
|
95
|
+
| Resume a checkpointed PR started by `auto-create-pr-loop` | `.ai/skills/auto-continue-pr-loop/SKILL.md` |
|
|
93
96
|
| Automated code review on a PR (with optional autofix) | `.ai/skills/auto-review-pr/SKILL.md` |
|
|
94
97
|
| Fix a GitHub issue by number and open a PR | `.ai/skills/auto-fix-github/SKILL.md` |
|
|
95
98
|
| Propose disabling unused built-in modules after the user adds a new module (classic-mode slimdown) | `.ai/skills/trim-unused-modules/SKILL.md` |
|
|
@@ -128,11 +131,35 @@ src/modules/<id>/
|
|
|
128
131
|
├── ce.ts # Custom entities / custom field sets
|
|
129
132
|
├── translations.ts # Translatable fields per entity
|
|
130
133
|
├── notifications.ts # Notification type definitions
|
|
131
|
-
|
|
134
|
+
├── notifications.client.ts # Client-side notification renderers
|
|
135
|
+
└── encryption.ts # Tenant data encryption maps (defaultEncryptionMaps) for sensitive / GDPR fields
|
|
132
136
|
```
|
|
133
137
|
|
|
134
138
|
Register in `src/modules.ts`: `{ id: '<id>', from: '@app' }`
|
|
135
139
|
|
|
140
|
+
## Mandatory Module Mechanisms (every module MUST use these — no DIY substitutes)
|
|
141
|
+
|
|
142
|
+
When the user asks to **create a new application** or a **new module**, do not invent your own routing, auth, persistence, forms, or caching. The framework provides one canonical primitive for each concern. If a feature is not on this list and not in the Task → Context Map, ask before adding it — do not roll your own.
|
|
143
|
+
|
|
144
|
+
| Concern | Canonical mechanism | Where to learn it |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| Module structure & auto-discovery | `src/modules/<id>/{api,backend,frontend,data,subscribers,workers,widgets}` + `index.ts` + `src/modules.ts` (`from: '@app'`) — discovered by `yarn generate` | `.ai/skills/module-scaffold/SKILL.md`, `.ai/guides/core.md` → Module Files; <https://docs.open-mercato.dev/framework/modules/overview> |
|
|
147
|
+
| **Backend admin pages** | Auto-discovered files under `src/modules/<id>/backend/**`, paired `page.meta.ts` with `requireAuth` + `requireFeatures` + `pageGroup`/`pageGroupKey`/`pageOrder` | `.ai/skills/backend-ui-design/SKILL.md`, `.ai/skills/module-scaffold/references/navigation-patterns.md`; <https://docs.open-mercato.dev/framework/modules/routes-and-pages> |
|
|
148
|
+
| **Frontend public pages** | Auto-discovered files under `src/modules/<id>/frontend/**`. Customer portal pages live under `frontend/[orgSlug]/portal/<path>/page.tsx` with `requireCustomerAuth` / `requireCustomerFeatures` in `page.meta.ts` | `.ai/guides/ui.md` → Portal Extension; <https://docs.open-mercato.dev/framework/modules/routes-and-pages> |
|
|
149
|
+
| **API routes** | Files under `src/modules/<id>/api/**/route.ts` exporting handlers + `metadata` (per-method `requireAuth` / `requireFeatures`) + `openApi`. NEVER write a top-level `export const requireAuth` — the registry no longer recognises it | `.ai/guides/core.md` → API Routes; <https://docs.open-mercato.dev/framework/api/api-development-guide> |
|
|
150
|
+
| **CRUD APIs (factory)** | `makeCrudRoute({ entity, entityId, operations, schema, indexer: { entityType } })` from `@open-mercato/shared/lib/crud/factory`. Always set `indexer` so query-index coverage stays correct. Custom (non-`makeCrudRoute`) write routes MUST call `validateCrudMutationGuard` before the mutation and `runCrudMutationGuardAfterSuccess` after success | `.ai/skills/module-scaffold/SKILL.md` → Create API Routes; <https://docs.open-mercato.dev/framework/api/crud-factory> |
|
|
151
|
+
| **CRUD forms in admin** | `<CrudForm entityId apiPath mode fields />` from `@open-mercato/ui/backend/CrudForm`; helpers `createCrud` / `updateCrud` / `deleteCrud` from `@open-mercato/ui/backend/utils/crud`; `createCrudFormError` from `@open-mercato/ui/backend/utils/serverErrors`. Never raw `<form>`, never raw `fetch` | `.ai/skills/backend-ui-design/SKILL.md`; <https://docs.open-mercato.dev/framework/admin-ui/crud-form> |
|
|
152
|
+
| **DataTables in admin** | `<DataTable entityId apiPath title columns />` from `@open-mercato/ui/backend/DataTable`. Keep `entityId` and `extensionTableId` stable so widget injection (columns, row actions, bulk actions, filters, toolbar) keeps working | `.ai/skills/backend-ui-design/SKILL.md`; <https://docs.open-mercato.dev/framework/admin-ui/data-grids> |
|
|
153
|
+
| **Authorization (RBAC)** | Declare features in `<module>/acl.ts`, grant them in `<module>/setup.ts` `defaultRoleFeatures`, gate pages and routes with `requireFeatures` in `metadata` / `page.meta.ts`. NEVER use `requireRoles` (role names mutate). Run `yarn mercato auth sync-role-acls` after adding new features | `.ai/guides/core.md` → Access Control; <https://docs.open-mercato.dev/framework/rbac/overview> |
|
|
154
|
+
| **Multi-tenant scoping (default for every entity)** | Every tenant-scoped entity MUST include indexed `organization_id` and `tenant_id` columns and every read/write MUST filter by them. The CRUD factory injects scope automatically — do NOT bypass it. For ad-hoc queries use `withScopedPayload` from `@open-mercato/shared/lib/api/scoped` | `.ai/skills/data-model-design/SKILL.md`; <https://docs.open-mercato.dev/architecture/system-overview> |
|
|
155
|
+
| **Encryption maps for sensitive data** | Declare a module-level `<module>/encryption.ts` exporting `defaultEncryptionMaps: ModuleEncryptionMap[]` from `@open-mercato/shared/modules/encryption`. Read encrypted columns via `findWithDecryption` / `findOneWithDecryption` from `@open-mercato/shared/lib/encryption/find`. NEVER hand-roll AES/KMS, NEVER use `em.find` on encrypted columns | `.ai/skills/data-model-design/SKILL.md` → Sensitive Data and Encryption Maps; <https://docs.open-mercato.dev/user-guide/encryption> |
|
|
156
|
+
| **Cache** | Resolve the cache from DI (`container.resolve('cache')`) — never `new Redis(...)` or raw SQLite. Tag with `tenant:<id>` / `org:<id>` and the entity-type tag so invalidation stays tenant-scoped | `.ai/guides/cache.md`; <https://docs.open-mercato.dev/user-guide/cache-management> |
|
|
157
|
+
| **Background workers** | `src/modules/<id>/workers/*.ts` exporting `metadata: { queue, id?, concurrency? }` + default handler. Never spin up custom queues | `.ai/guides/queue.md`; <https://docs.open-mercato.dev/framework/events/queue-workers> |
|
|
158
|
+
| **Events between modules** | `<module>/events.ts` with `createModuleEvents({ moduleId, events } as const)`. Subscribers in `subscribers/*.ts`. Never call other modules' services directly across module boundaries | `.ai/guides/events.md`; <https://docs.open-mercato.dev/framework/events/overview> |
|
|
159
|
+
| **i18n (every user-facing string)** | `useT()` client-side from `@open-mercato/shared/lib/i18n/context`, `resolveTranslations()` server-side from `@open-mercato/shared/lib/i18n/server`; keys in `src/i18n/<locale>.json`. Never hard-code labels in components | `.ai/guides/shared.md` → i18n |
|
|
160
|
+
|
|
161
|
+
> Rule of thumb: if you find yourself reaching for raw `fetch`, raw `<form>`, ad-hoc `crypto`, ad-hoc `Redis`, or a manual `m2m` join across modules, stop and check the row above — there is a canonical helper.
|
|
162
|
+
|
|
136
163
|
## CRITICAL rules — always follow without exception
|
|
137
164
|
|
|
138
165
|
1. **Entity classes live in `src/modules/<module>/data/entities.ts` and MUST import decorators from `@mikro-orm/decorators/legacy`.** Start there for every schema change.
|
|
@@ -154,9 +181,16 @@ Register in `src/modules.ts`: `{ id: '<id>', from: '@app' }`
|
|
|
154
181
|
**Dashboards fallback rule.** When the user (or the `trim-unused-modules` skill) disables the `dashboards` module, you MUST update `src/app/(backend)/backend/page.tsx` so it no longer renders `<DashboardScreen />`. Replace the dashboard render with a `redirect(...)` to the first enabled backend page for the current user — preferring pages already registered in the main sidebar group and respecting the admin/superadmin role of the caller. Otherwise `/backend` will crash at build or request time because the removed module no longer ships `DashboardScreen`. Always fall back to `/backend/profile` only if no other backend page is available.
|
|
155
182
|
9. **New features MUST be visible to default roles immediately.** Every time you add a new feature ID (e.g. `my_module.view`, `my_module.manage`) to `src/modules/<module>/acl.ts`, you MUST also (a) add that feature to `defaultRoleFeatures` in the same module's `setup.ts` so the admin role and any other appropriate default roles get it on every tenant setup; and (b) run `yarn mercato auth sync-role-acls` so existing tenants pick up the new feature without a reinstall. Use `--tenant <tenantId>` only when the user asks to target one tenant. Do this automatically unless the user has explicitly said otherwise — the user should see the features you are building, not stare at a blank admin because their role is missing a grant. Feature IDs are FROZEN once shipped; if a rename is required, add the new ID alongside, grant it, and keep the old one as a deprecated alias.
|
|
156
183
|
10. **Strict Design System alignment for every UI change.** Any UI you add or edit MUST use the Open Mercato design system components and tokens. No hardcoded Tailwind status colors (`text-red-500`, `bg-green-100`, etc.) — use semantic tokens (`text-status-error-text`, `bg-status-success-bg`, …). No arbitrary text sizes (`text-[11px]`, `text-[13px]`) — use the Tailwind scale (`text-xs`, `text-sm`, `text-base`, `text-lg`, `text-xl`, `text-2xl`) or the `text-overline` token for 11px uppercase labels. In PAGE BODY UI, use `lucide-react` icons (never inline `<svg>`). Use `StatusBadge` for entity status, `Alert` for inline feedback, `FormField` for standalone form inputs, `SectionHeader` for detail-page section headings, `CollapsibleSection` for collapsible regions, `LoadingMessage`/`Spinner`/`DataLoader` for async states, and `EmptyState` (or DataTable's `emptyState` prop) for empty lists. For list pages, follow `.ai/skills/backend-ui-design/SKILL.md` and prefer the `DataTable` host pattern shown there (`entityId`, `apiPath`, stable `extensionTableId`, and explicit pagination props when you own the data source). Every dialog MUST support `Cmd/Ctrl+Enter` to submit and `Escape` to cancel. Every icon-only button MUST have an `aria-label`. These rules apply to `src/modules/<module>/backend/**` and `src/modules/<module>/frontend/**` alike.
|
|
157
|
-
11. **
|
|
184
|
+
11. **Sensitive / GDPR fields MUST go through the encryption-maps mechanism — never hand-rolled.** The framework provides per-tenant DEKs, KMS-backed key resolution, and a declarative field-level map. Whenever the user asks for "this field encrypted", "store this securely", "this is PII", "GDPR", "encryption at rest", or you are designing a column that will hold names, addresses, contacts, free-text notes about people, integration secrets/credentials, or any data subject to a data-processing agreement, you MUST:
|
|
185
|
+
- Declare the entity + field list in `src/modules/<module>/encryption.ts` exporting `defaultEncryptionMaps: ModuleEncryptionMap[]` (type imported from `@open-mercato/shared/modules/encryption`).
|
|
186
|
+
- Read those columns via `findWithDecryption` / `findOneWithDecryption` from `@open-mercato/shared/lib/encryption/find` (passing `tenantId` and `organizationId`). Never use raw `em.find` on encrypted columns.
|
|
187
|
+
- For deterministic-lookup fields (e.g., login email), declare a sibling `hashField` in the map so equality lookups still work.
|
|
188
|
+
- Run `yarn mercato entities seed-encryption --tenant <tenantId>` after adding maps so existing tenants pick them up; new tenants get them automatically during `auth:setup`.
|
|
189
|
+
- Treat hand-rolled AES, raw `crypto.subtle`, custom KMS calls, or storing plaintext "for now" as broken — rewrite via the maps. See `.ai/skills/data-model-design/SKILL.md` → Sensitive Data and Encryption Maps and <https://docs.open-mercato.dev/user-guide/encryption>.
|
|
190
|
+
12. **BEFORE writing ANY code**, you MUST:
|
|
158
191
|
- Match your task against the **Task → Context Map** above
|
|
159
192
|
- `Read` every file listed in the "Load" column for your task type
|
|
193
|
+
- Read the **Mandatory Module Mechanisms** section above to confirm which canonical primitives apply (CRUD factory, CrudForm, DataTable, RBAC, multi-tenant scoping, encryption maps, cache, events) — do not invent your own substitutes
|
|
160
194
|
- Only then proceed to implementation
|
|
161
195
|
- If your task matches multiple rows, load ALL listed files
|
|
162
196
|
- **Do NOT skip this step.** The guides contain canonical import paths, required patterns, and conventions that CANNOT be reliably inferred from existing code alone. Skipping leads to wrong imports, missing conventions, and rework.
|
|
@@ -195,7 +229,8 @@ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
|
195
229
|
import { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
196
230
|
|
|
197
231
|
// CRUD forms
|
|
198
|
-
import { CrudForm,
|
|
232
|
+
import { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
|
|
233
|
+
import { createCrud, updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
199
234
|
import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
200
235
|
|
|
201
236
|
// UI components (MUST use — never raw <button>)
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { defineConfig } from '@playwright/test'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
3
4
|
import { discoverIntegrationSpecFiles } from '@open-mercato/cli/lib/testing/integration-discovery'
|
|
4
5
|
|
|
5
6
|
const captureScreenshots = process.env.PW_CAPTURE_SCREENSHOTS === '1'
|
|
6
7
|
const isGitHubActions = process.env.GITHUB_ACTIONS === 'true'
|
|
8
|
+
// Standalone apps generated from this template declare `"type": "module"`,
|
|
9
|
+
// so the CommonJS `__dirname` is undefined when Playwright loads this config
|
|
10
|
+
// under the Node ESM loader. Reconstruct it from `import.meta.url`.
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
7
12
|
const projectRoot = path.resolve(__dirname, '..', '..', '..')
|
|
8
13
|
const qaTestResultsRoot = path.join(projectRoot, '.ai', 'qa', 'test-results')
|
|
9
14
|
const normalizePath = (value: string) => value.split(path.sep).join('/')
|