# AGENTS.md — Canonical AI Coding & Repository Instructions > **Platform:** KeluargaMart Monorepo > **Audience:** Human Engineers & Autonomous AI Coding Agents > **Standard:** Production Hardened, Zero-Drift, Resource-Conscious Development --- ## 1. Product Context & Architectural Boundaries KeluargaMart is an enterprise retail, distribution, and wholesale omnichannel platform comprising five main applications: 1. `apps/backend`: Go 1.23+ Chi REST API (Modular Monolith, Single Source of Truth for business rules, database schema, and authorization). 2. `apps/erp`: Vue 3 + Vite + Tailwind CSS back-office operations management portal. 3. `apps/pos`: Vue 3 + Vite cashier register SPA optimized for fast transactions, barcode scanning, and offline-first shift tracking. 4. `apps/b2b`: Nuxt 4 + Vue 3 wholesale e-commerce storefront with server-side rendering (SSR/Nitro). 5. `apps/nextreport`: Python/Node reporting engine for asynchronous batch generation of PDF/Excel accounting and operational reports. ### Single Source of Truth Rule The **Go backend (`apps/backend`) is the authoritative source** for: - Database schema and migrations (`apps/backend/migrations` / `internal/dbmigrate`). - RBAC permissions catalog (`apps/backend/internal/auth/catalog.go`). - Financial calculations, discounts, price tiers, stock movements, and debt accounting. - Business validation and idempotent request processing. Frontends (`apps/erp`, `apps/pos`, `apps/b2b`) MUST NOT invent business rules or bypass backend authorization. --- ## 2. Monorepo Structure & Toolchain The monorepo is managed with **pnpm workspaces (v12)** and orchestrated with **Turborepo (v2)**. ```text keluargamart-monorepo/ ├── apps/ # Deployable applications (backend, erp, pos, b2b, nextreport) ├── packages/ # Shared internal workspace packages (@keluargamart/*) ├── infra/ # Container stacks, reverse proxy configs, Cloudflare tunnels ├── docs/ # Architecture, deployment runbooks, ADRs, performance specs ├── scripts/ # Drift guard and code generators ├── package.json # Root workspace manifests & scripts ├── pnpm-workspace.yaml # Workspace packages definition └── turbo.json # Task pipeline orchestration & cache configuration ``` ### Essential Universal Commands | Action | Universal Command | Sub-Target Filter Example | |---|---|---| | **Install Dependencies** | `pnpm install` | — | | **Run Dev Servers** | `pnpm dev` | `pnpm --filter @keluargamart/erp dev` | | **Run Backend Dev** | `pnpm dev:backend` | — | | **Run All Tests** | `pnpm test` | `pnpm --filter @keluargamart/pos test` | | **Run Backend Tests** | `pnpm test:backend` | — | | **Build All Apps** | `pnpm build` | `pnpm --filter @keluargamart/b2b build` | | **Build Backend Binary** | `pnpm build:backend` | — | | **Lint Entire Workspace** | `pnpm lint` | `pnpm --filter @keluargamart/contracts lint` | | **API Drift Guard Check** | `pnpm drift:check` | — | | **Playwright Full Sweep** | `pnpm sweep` / `pnpm test:e2e` | — | | **Exhaustive UI Deep Sweep** | `pnpm test:ui` / `pnpm sweep:deep` | — | | **E2E Real Data Flow Test** | `pnpm test:flow` | — | --- ## 3. Strict Dependency & Package Management Rules 1. **pnpm Workspace Resolution**: All internal packages must be referenced using `workspace:*` (e.g. `"@keluargamart/contracts": "workspace:*"`). 2. **Zero Phantom Dependencies**: Do NOT import packages that are not explicitly declared in the application's `package.json`. pnpm enforces strict node-linker isolation. 3. **No Cross-App Imports**: Apps in `apps/*` are forbidden from importing files directly from another app in `apps/*`. Common code MUST be placed in `packages/*`. 4. **Pure Shared Packages**: Packages in `packages/*` must remain leaf libraries and NEVER import from `apps/*`. --- ## 4. API Contract & Permission Governance - **Routes**: Canonical routes are defined in `apps/backend/docs/openapi.yaml` and mirrored into `packages/contracts`. - **Permissions**: Canonical RBAC permissions are defined in `apps/backend/internal/auth/catalog.go` and mirrored into `packages/permissions`. - **Drift Guard**: Run `pnpm drift:check` whenever an endpoint or permission is altered. A PR will fail CI if any drift exists. - **Never Copy-Paste Contract Files**: Frontend applications MUST import routes and permissions exclusively from `@keluargamart/contracts/routes` and `@keluargamart/permissions`. --- ## 5. Security & Multi-Tenancy Invariants 1. **Fail-Closed Authorization**: All routes must default to requiring authentication unless explicitly public. 2. **Tenant & Outlet Isolation**: All queries touching store inventories, shifts, cash drawers, and orders must filter by the authenticated user's store/warehouse context. Never remove tenant filters to "fix" query tests. 3. **Financial Immutability**: Journal entries, completed order payments, and finalized POS transactions must NEVER be mutated or deleted. Corrections must be handled via reversing adjustments. 4. **Secret Protection**: NEVER commit `.env`, private keys, JWT secrets, database credentials, or Cloudflare tunnel tokens. --- ## 6. Vibe Coding Without Engineering Drift (Anti-Hallucination Protocol) When implementing features or bug fixes as an AI agent: ### A. Before Implementation - Inspect actual source files and existing tests. - Propose the minimal surgical change required. - Identify downstream consumers in other applications. - Verify whether an API contract change is required. ### B. During Implementation - **DO NOT Fabricate**: Never create fake/mock implementations or hardcoded return values in production code to make tests pass. - **DO NOT Break Semantics**: Preserve existing API response shapes (`{ data: ..., meta: ... }` / `{ error: ... }`). - **DO NOT Over-Refactor**: Confine edits to the assigned task. Do not reformat entire files or rewrite working modules. - **Maintain Comments**: Preserve existing explanatory comments and historical bug notes. ### C. Before Completion - Execute actual test suites (`pnpm test:backend`, `pnpm drift:check`, `pnpm turbo run test`). - Execute live Playwright browser sweep against running containers (`pnpm sweep`). - Execute exhaustive interactive UI test (`pnpm test:ui`) and data flow test (`pnpm test:flow`). - Verify production buildability (`pnpm build`). - Verify git status is clean of unintended artifacts or temporary scratch files. --- ## 7. Strict Omnichannel End-to-End Validation Protocol To eliminate discrepancies between reported test results and real-world operational user experience: 1. **Anti-Assumption & Full Persona Testing**: - An omnichannel feature (e.g. B2B Store Pickup, COD Settlement, POS Sync) is NOT complete until all touching personas are verified in the real UI: - *Buyer Persona* (`apps/b2b`): Checkout form, status badges, payment suppression for COD/delivered, tracking suppression for terminal states. - *Warehouse / Store Staff Persona* (`apps/erp`): Packing order list, packing slip print, validation gates preventing packing of unpaid transfer orders. - *Retail Cashier Persona* (`apps/pos`): Direct visibility of pickup orders, till cash collection, shift drawer logging. - *Backend Source of Truth* (`apps/backend`): DB transactions, status history, ledger balances, fail-closed enforcement. 2. **Mandatory Negative & Boundary Testing**: - Every status transition or payment rule must have an accompanying automated test verifying that invalid operations are strictly rejected (e.g. unpaid transfer orders CANNOT advance to `picking` or `shipped`). 3. **No Phantom Claims in Reports**: - Release reports and documentation must document the actual software behavior currently running on the server. If a cross-application link is planned or requires manual back-office action, it must be stated explicitly as an operational prerequisite, never reported as an automated seamless handoff until code and UI exist. --- ## 7-A. Fulfillment-Mode Segregation Check (pickup vs delivery) Store pickup ("Ambil Sendiri di Toko") and courier delivery are two different operations that share one order record. Every defect in this family came from one surface treating them as the same thing, so EVERY change touching orders MUST answer this checklist per surface: | Surface | Pickup (ambil di toko) | Delivery (kirim ke alamat) | |---|---|---| | POS "Ambil Toko" list (`for-pickup`) | MUST appear | MUST NOT appear | | ERP "Packing Order" list | MUST NOT appear | MUST appear | | ERP "Delivery Order" auto-DO on confirm | MUST NOT be created | MUST be created | | POS "Serahkan Barang" (till handover) | MUST work from `pending_approval` | — (warehouse chain only) | | B2B "Lacak Pengiriman" button | MUST NOT appear | MUST appear | | B2B "Status Pesanan" stepper / "Dokumen" card | MUST NOT render courier steps | MUST render | | Fulfillment chain (confirmed → picking → shipped → delivered) | SKIPPED (till handover) | ENFORCED | Concretely, before calling any of these surfaces done: 1. **Backend filters are mode-aware.** List endpoints that serve one mode (`for-pickup`, the packing list via `exclude_pickup`) filter on `isPickupOrder` / the `shipping_address` marker — NOT on `order_type` alone. An e-commerce order is BOTH modes; order_type never separates them. 2. **Auto-created artifacts respect the mode.** Anything auto-created on confirm (invoice, DO) either skips pickup orders or is correct for both modes. `onOrderConfirmed` in `cmd/server/main.go` is the reference. 3. **Status transition gates name the mode.** A jump like `pending_approval → delivered` is legal ONLY for cashier handover of a cash pickup (`isCashierHandover && isPickupOrder && isCashOrder`). The same jump for a transfer or delivery order must stay refused. 4. **UI hides the other mode's affordances.** A pickup order must not show a tracking button, courier stepper, DO paperwork, or a packing action; a delivery order must not appear on the till's pickup list. 5. **One marker, three consumers, kept in lockstep.** `internal/sales.isPickupOrder` (backend), `isPickupOrder` in `apps/b2b/app/utils/paymentMethods.ts`, and the `exclude_pickup` SQL pattern must agree. Test each against the literal checkout marker `[Ambil Sendiri di Toko]` — see `TestIsPickupOrder_MatchesCheckoutMarker`. 6. **Verify in the running UI, with a real order of EACH mode.** A green unit suite does not prove the list a cashier sees. Create one pickup and one delivery order, then check all three lists (POS till, ERP packing, ERP delivery orders) and both order pages (B2B detail, ERP packing detail). Historical reference (Nov 2026): the till handover button was dead because the transition gate lacked `pending_approval`; both lists leaked the other mode because they filtered on order_type only; pickup orders appeared as "Sedang Diantar" because auto-DO ignored the mode. All three were the same root cause surfacing on three surfaces. --- ## 7-B. UI-Orphan Check (frontend feature with no backend) A frontend element that reads a field or calls an endpoint the backend does not implement renders forever-empty (or silently fails) and passes every unit test, because unit tests stub the API. The POD ("Foto POD ✓", "Belum Ada Foto POD" panel) shipped exactly this way: fully built in ERP, backed by a field that never existed. Before shipping any UI that displays data: 1. **Trace the field to its source.** Grep the response field (e.g. `proof_photo`) across `internal/**` and `db/query/**`. If nothing produces it, the UI is an orphan — either implement the backend or delete the UI. Do not ship the pairing "hopeful". 2. **One round-trip beats any mock.** Against the local stack, drive the real flow (upload → store → read back) and confirm the value the UI renders came from the database, not from a stub. 3. **New UI displaying stored data needs a column + a route.** If the answer to "which migration and which endpoint serve this?" is silence, the work is not done. Conversely, a new backend column for user-facing data should ship together with the UI that shows it. 4. **A page without a route is not shipped.** The POS "Ambil Toko" screen was fully written — list, handover dialog, receipt — yet had no router entry, no sidebar link, and called two service methods that did not exist. It was unreachable in every build and no test could have caught it, because the component was never mounted. Before calling UI done: the route resolves, the menu entry appears for the permission that needs it, and Playwright reaches the page by clicking, not by typing a URL. 5. **Every screen that writes must have a reachable negative path.** A page that offers an action the backend refuses (handover of an unpaid transfer pickup) must disable it and say why; otherwise the first real user discovers the rule as an error toast. --- ## 7-C. Terminal Status & Side-Effect Hook Guard A status transition is not finished when the `UPDATE` runs — it is finished when every hook that follows it has run and the row still holds the status the caller asked for. The 1 Nov defect: `AdvanceFulfillment` wrote `delivered`, then fired `onConfirm` for *every* transition; `onConfirm` auto-creates an invoice whose `SetSOStatus('invoiced')` was unconditional, so every till handover silently ended `invoiced` with a phantom unpaid invoice. The unit test passed because the fake store had no hooks. 1. **Hooks belong to the transition they are named after.** `onConfirm` fires only when the order reaches `confirmed`. Delivery, picking and shipping are not confirmations. 2. **Never write a status backwards.** Any side-effect stamp (`invoiced`, `paid`, `completed`) must be conditional on the order still sitting in a pre-fulfilment status; a late/duplicate hook must be a no-op, not a rollback of work already done. 3. **Terminal is terminal.** `delivered`/`cancelled`/`completed` must not be overwritten by an invoice, payment, or retry. Corrections use a reversal or a return, never a status rewrite. 4. **Test the final state, after hooks.** A transition test asserts the status returned by the endpoint *and* that no extra document was created (e.g. a cash pickup handover leaves zero open invoices). A fake store that omits the hook cannot prove this — wire the hook in the test or verify live. 5. **Verify each entry point separately.** The same order can be advanced from ERP (warehouse chain), POS (till handover) and B2B (buyer confirmation). Run all three against the local stack and read the status back from the database, not from the toast. --- ## 7-D. Money Arithmetic Must Reconcile On Screen A line's numbers must add up where the user can see them: ``` unit_price × quantity − line discount − membership/promo discount = subtotal ``` 1. **Show every component you subtract.** ERP's sales-order detail showed `Harga` 15.000 and `Subtotal` 14.700 with `Diskon` 0, because the membership discount lives in its own column (`membership_discount_amount`) and was never rendered. Buyers read that as a pricing bug — and they were right to: the arithmetic on screen did not close. 2. **A separate discount column is still a discount.** If a benefit is stored apart from `discount_amount`, either fold it into the displayed discount or label it explicitly next to the line and in the summary. 3. **Never mutate finalized money to make a screen look right.** Fix the display (and the writer for future rows), not the historic ledger. 4. **Reconcile by hand after any pricing change.** Open a real order for each price source — list price, membership tier, voucher, manual line discount, multi-unit (dus/pcs) — and check Harga × Qty − Diskon = Subtotal and Σ lines + tax + shipping − order discount = Total. --- ## 8. Metric Truth & Summary-to-Drilldown Parity Invariants Dashboard cards and analytical widgets must always reflect verifiable, living transactional data: ### A. Summary vs Drilldown Parity Rule - A summary stat card (e.g., `Net Cashflow`, `Piutang`, `Order Aktif`) and its drilldown modal/detail view MUST query the **exact same underlying data source** and agree in total. - NEVER compute a summary metric using a synthetic proxy formula (e.g., deriving Net Cashflow via `MonthSales - MonthPurchase`) while pointing the drilldown modal to an unrelated or unpopulated table. ### B. Zero Fabrication & Zero Estimation Rule - Operational daily figures must never divide monthly totals by 30 (`MonthSales.Div(30)`) to fake daily velocity. If a metric represents "Hari Ini", it must filter by `order_date = CURRENT_DATE`. - Active entity counts (such as `activePOs`, `pendingOrders`) must execute a `COUNT(*)` against active database statuses, never hardcoding placeholder constants (e.g. `ActivePOs: 2`) or borrowing unrelated slice lengths (`len(RecentActivity)`). ### C. Live Table Verification (Ghost Table Guard) - Before querying a table for dashboard analytics, trace where the actual operational events are written. If customer and supplier money moves through `payments` and `journal_entries`, queries must aggregate those tables rather than querying empty legacy tables (e.g., unpopulated `cashflows`). - Detail payloads must never be stubbed with empty slices (`pendingInvoices: []any{}`). If a card shows a non-zero count or sum, the corresponding drilldown array must return the matching active records. ### D. Drilldown Roundtrip Verification Protocol Whenever modifying or introducing a dashboard widget: 1. Verify the summary query against database reality. 2. Verify that clicking the widget opens a modal containing the identical records that sum to the summary value. 3. Add or update automated end-to-end regression tests to assert both the summary endpoint (`/dashboard/kpis`) and drilldown endpoint (`/dashboard/charts`) return consistent data. ### E. One Predicate, One Window - The card's aggregate and the modal's row list MUST share the same predicate, status set, date window and exclusions. Define the predicate once (e.g. `const activeOrderStatuses` in `internal/reports/repository.go`) and reuse it in both the `COUNT(*)` and the `SELECT … WHERE`. Two hand-written copies drift on the first status added. - If a list must be capped, the modal shows the server total (from the card) and states that the list is truncated. A modal that computes its own total from a capped list silently under-reports. - A clickable card with an empty drilldown array is a defect, not a placeholder. Test the payload with a non-empty dataset: the 1 Nov report ("dashboard shows numbers, detail is 0") was exactly this — the handler never assigned `pendingInvoices`/`activePOs` and no test asserted a non-empty array. ### F. Verify The Deployed Artifact - After a rebuild/redeploy, verify the behaviour with a real request and read the database back — not the source tree, not a comment string in a binary. Comments are stripped at compile time; searching them in `strings` proves nothing. Verify with a query or an HTTP round-trip whose result could not exist in the old build. - When a live result contradicts the source, suspect the artifact before re-reading the code: stale image layer, `--force-recreate` not applied, or a container still serving the previous tag. `podman images --format` and the container's start time are the first checks. --- ## 9. Resource-Conscious Production Standards (Hostinger KVM 2) The production server operates on **2 vCPU and 8 GB RAM**: 1. **Zero-Build on Production**: Production servers pull prebuilt container images from GHCR (`compose.prod.yml`). Never run compilers or build tools directly on the VPS. 2. **Memory Bounds**: - Backend Go: Set `GOMEMLIMIT=512MiB`, `GOGC=100`. - B2B Nuxt SSR: Set `NODE_OPTIONS="--max-old-space-size=256 --enable-source-maps=false"`. - ERP & POS: Served statically via lightweight Nginx/Caddy with long-lived asset caching. - NextReport: Bounded Node/Chromium worker with memory limits. 3. **Database Pooling**: Backend database pool defaults to `MaxConns=25`, `MinConns=2`, avoiding connection starvation. 4. **Log Rotation**: All container configurations must enforce `max-size: 10m` and `max-file: 3`. --- ## 10. Escalation & Prohibited Actions The following actions require explicit human operator approval and MUST NOT be executed autonomously: - Running destructive database operations (`DROP TABLE`, unreviewed `DOWN` migrations). - Deleting persistent volumes or production database backups. - Modifying production DNS records or live Cloudflare tunnel credentials. - Force-pushing to shared branches (`git push -f origin main`). - Disabling failing CI checks or security scanners.