# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project intent `vibe_erp` is an **ERP framework** (not an ERP application) aimed at the **printing industry**. The goal is a pluggable platform on which any printing business — with its own workflows, forms, roles, and rules — can be assembled by **configuration and plug-in modules**, not by forking or rewriting the core. The reference business in `raw/业务流程设计文档/` is **one example customer**, not the spec. It must be treated as a *fixture* to validate that the framework can express such a workflow, never as a source of hard-coded entities or features in the core. Top-level domains in the reference doc (use only as inspiration / test cases): `销售管理` (sales) · `采购管理` (purchasing) · `材料仓库管理` (raw material warehouse) · `成品仓库管理` (finished goods warehouse) · `生产管理` (production) · `工艺管理` (process/craft) · `车间管理` (workshop) · `设备管理` (equipment) · `品质管理` (quality) · `财务管理` (finance) · `项目管理` (project) ## Architectural guardrails These rules exist because the whole point of the project is reusability across customers. Violating them defeats the project. 1. **Core stays domain-agnostic.** No printing-specific entity, field, status, or workflow step belongs in the framework core. "Plate", "ink", "press", "color proof", etc. live in plug-in modules or configuration — never in core tables, core types, or core code paths. 2. **Workflows are data, not code.** State machines, approval chains, document routing, and form definitions must be declarative (config / DB / DSL) so a new customer can be onboarded by editing definitions, not by editing source. 3. **Extensibility seams come first.** Before adding any feature, identify the extension point (hook, plug-in interface, event, custom field, scripting hook) it should hang off of. If no seam exists yet, design the seam first, then implement the reference customer's behavior on top of it. 4. **The reference customer is a test, not a requirement.** When implementing something inspired by `raw/业务流程设计文档/`, ask: "Could a different printing shop with different rules also be expressed here without code changes?" If no, redesign. 5. **Single-tenant per instance, isolated database.** vibe_erp is deliberately NOT multi-tenant. One running instance serves exactly one company against an isolated Postgres database. Hosting many customers means provisioning many independent instances (each with its own DB), not multiplexing them in one process. There are no `tenant_id` columns, no row-level tenant filtering, no Row-Level Security, no `TenantContext`, and no per-request tenant resolution anywhere in the framework. Customer isolation is a deployment concern, not a code concern. Data models and APIs may freely assume "one company, one process, one DB" — but they must still allow per-customer customization through configuration, plug-ins, and metadata so the SAME image runs for any customer. 6. **Global / i18n from day one.** This codebase is the foundation of an ERP/EBC product intended to be sold worldwide. No hard-coded user-facing strings, no hard-coded currency, date format, time zone, number format, address shape, tax model, or language. All such concerns must go through localization, formatting, and configuration layers — even in the very first prototype. The reference docs being in Chinese does **not** mean Chinese is the primary or default locale; it is just one supported locale among many. 7. **Clean Core.** Borrowed from SAP S/4HANA's vocabulary: extensions **never modify the core**. The core is stable, generic, and upgrade-safe; everything customer-specific lives in plug-ins or in metadata rows. Extensions are graded A/B/C/D by upgrade safety: - **A** = Tier-1 metadata only (custom fields, forms, workflows, rules) — always upgrade-safe - **B** = Tier-2 plug-in using only the public `api.v1.*` surface — safe within a major version - **C** = Tier-2 plug-in using deprecated `api.v1` symbols — safe until next major; loader emits warnings - **D** = Tier-2 plug-in reaching into `platform.*` or `pbc.*.internal.*` via reflection — UNSUPPORTED, rejected by the plug-in linter at install time 8. **Two-tier extensibility, both first-class.** The framework offers two extension paths and both must be designed for, not bolted on: - **Tier 1 — key user / no-code:** business analysts add fields, forms, list views, workflows, automation rules, custom entities, reports, and translations through the web UI. Everything is stored as rows in the `metadata__*` tables (Doctype-style), tagged `source = 'user'`, scoped to their tenant. No build, no restart. The OpenAPI spec, REST API, and AI-agent function catalog auto-update from the metadata. - **Tier 2 — developer / pro-code:** PF4J JAR plug-ins. Plug-ins see only `org.vibeerp.api.v1.*`. Importing anything from `platform.*` or any PBC's internal package fails the plug-in linter. 9. **PBC boundaries are sacred.** Each core capability (`pbc-identity`, `pbc-catalog`, `pbc-inventory`, `pbc-orders-sales`, `pbc-production`, …) is its own **Packaged Business Capability**: own Gradle subproject, own table-name prefix, own bounded context, own public API surface. **PBCs never import each other.** Cross-PBC interaction goes through (a) the event bus or (b) service interfaces declared in `api.v1.ext.`. The Gradle build enforces this — `pbc-orders-sales` cannot declare `pbc-inventory` as a dependency. This is what makes "modular monolith now, splittable later" real instead of aspirational. 10. **`api.v1` is the only stable contract.** The package `org.vibeerp.api.v1.*` is the single semver-governed surface that plug-ins consume. It is published as `api-v1.jar` to Maven Central. Everything else in the codebase (`platform.*`, `pbc.*.internal.*`, every concrete Spring bean) is internal and may change in any release. Adding to `api.v1` is allowed within a major version; renaming, removing, or behavior-changing anything in `api.v1` is a major version bump. When in doubt, keep things OUT of `api.v1`; it is easier to grow the API deliberately than to maintain a regretted symbol forever. 11. **AI agents are a first-class client.** The framework is built so that the same business operations exposed via REST/OpenAPI are also callable by LLM-driven agents through an MCP server (or equivalent function-calling surface). The MCP endpoint itself is a v1.1 deliverable, but v1.0 must not architect it out — operations must be discoverable, typed, permission-checked, and locale-aware in a way that lets an agent call them safely. AI agents are listed alongside humans, web clients, mobile clients, and integrations as supported callers. ## Working with the reference docs - `raw/业务流程设计文档/` is in **Chinese**. Module and process names are domain terms (e.g. `工艺` = process/craft, `车间` = workshop floor, `品质` = QC). Preserve original terminology when quoting; do not silently rename concepts when discussing them. - Treat these documents as **read-only reference material**. Do not move, restructure, or "clean up" `raw/`. - When the user asks for a feature "from the doc", confirm whether they want it built as a **reference plug-in / fixture** on top of the framework, or as a **core capability** — the answer is almost always the former. ## Documentation discipline Because this is a framework that other developers (and future-you) will build on, **documentation is part of "done"**, not an afterthought. As the repo grows: - Maintain a top-level `docs/` tree alongside the code. At minimum it should cover: architecture overview, extension points / plug-in API, workflow & form definition format, configuration reference, i18n/localization guide, and a getting-started guide for onboarding a new customer. - Every new extension point, plug-in interface, config key, or workflow primitive added to the core **must** ship with documentation in the same change. A PR that adds a public seam without docs is incomplete. - Keep `raw/业务流程设计文档/` separate from `docs/`. `raw/` is one customer's business reference (read-only fixture). `docs/` is the framework's own documentation, written in English by default with localization where appropriate. - When a doc and the code disagree, treat it as a bug — fix whichever is wrong, do not silently let docs rot. ## Stack and shape (decided, not yet built) The architecture has been brainstormed, validated against 2026 ERP/EBC SOTA, and approved. The full design lives at `docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md` and is the source of truth for everything below. - **Backend:** Kotlin on the JVM, Spring Boot, single fat-JAR / single Docker image - **Workflow engine:** Embedded Flowable (BPMN 2.0) - **Persistence:** PostgreSQL (the only mandatory external dependency) - **Multi-tenancy:** intentionally none. Single-tenant per instance, one isolated Postgres database per customer (see guardrail #5) - **Custom fields:** JSONB `ext` column on every business table, described by `metadata__custom_field` rows; GIN-indexed - **Plug-in framework:** PF4J + Spring Boot child contexts (classloader isolation per plug-in) - **i18n:** ICU MessageFormat (ICU4J) + Spring `MessageSource` - **Reporting:** JasperReports - **Auth:** Built-in JWT + OIDC (Keycloak-compatible) - **Web client (v1):** React + TypeScript SPA; **mobile (React Native) is v2** - **API:** REST + OpenAPI; MCP server is v1.1 (seam exists in v1.0) **Topology:** one Spring Boot process per instance, one Docker image per release, one mounted volume (`/opt/vibe-erp/`) for `config/`, `plugins/`, `i18n-overrides/`, `files/`, `logs/`. Customer extensions live **outside** the image. Upgrading vibe_erp = swapping the image; extensions and config stay put. **Module layout (Gradle multi-project):** ``` vibe-erp/ ├── api/api-v1/ ← THE CONTRACT (semver, published to Maven Central) ├── platform/* ← Framework runtime (internal, not exposed to plug-ins) ├── pbc/* ← Core PBCs: identity, catalog, partners, inventory, │ warehousing, orders-sales, orders-purchase, │ production, quality, finance ├── reference-customer/ │ └── plugin-printing-shop/ ← Reference plug-in expressing raw/业务流程设计文档/. │ Built and CI-tested; NOT loaded by default. ├── web/ ← React + TypeScript SPA └── docs/ ← Framework documentation ``` **Dependency rule (enforced by the Gradle build):** ``` api/api-v1 depends on: nothing (Kotlin stdlib + jakarta.validation only) platform/* depends on: api/api-v1 + Spring + libs pbc/* depends on: api/api-v1 + platform/* (NEVER another pbc) plugins (incl. ref) depend on: api/api-v1 only ``` ## Repository state **Foundation complete; first business surface in place.** As of the latest commit: - **16 Gradle subprojects** built and tested. The dependency rule (PBCs never import each other; plug-ins only see `api.v1`) is enforced at configuration time by the root `build.gradle.kts`. - **192 unit tests across 16 modules**, all green. `./gradlew build` is the canonical full build. - **All 9 cross-cutting platform services live and smoke-tested end-to-end** against real Postgres: auth (P4.1), authorization with `@RequirePermission` + JWT roles claim + AOP enforcement (P4.3), plug-in HTTP endpoints (P1.3), plug-in linter (P1.2), plug-in-owned Liquibase + typed SQL (P1.4), event bus + transactional outbox (P1.7), metadata loader + custom-field validation (P1.5 + P3.4), ICU4J translator with per-plug-in locale chain (P1.6), plus the audit/principal context bridge. - **6 of 10 core PBCs implemented end-to-end** (`pbc-identity`, `pbc-catalog`, `pbc-partners`, `pbc-inventory`, `pbc-orders-sales`, `pbc-orders-purchase`). **The full buy-and-sell loop works**: a purchase order receives stock via `PURCHASE_RECEIPT` ledger rows, a sales order ships stock via `SALES_SHIPMENT` ledger rows. Both PBCs feed the same `inventory__stock_movement` ledger via the same `InventoryApi.recordMovement` facade. - **Event-driven cross-PBC integration is live.** Six typed events under `org.vibeerp.api.v1.event.orders.*` (`SalesOrderConfirmed/Shipped/Cancelled`, `PurchaseOrderConfirmed/Received/Cancelled`) are published from `SalesOrderService` and `PurchaseOrderService` inside the same `@Transactional` method as their state changes. The wildcard `EventAuditLogSubscriber` logs every one and `platform__event_outbox` rows are persisted + dispatched by the `OutboxPoller`. This is the first end-to-end use of the event bus from real PBC business logic. - **Reference printing-shop plug-in** owns its own DB schema, CRUDs plates and ink recipes via REST through `context.jdbc`, ships its own metadata YAML, and registers 7 HTTP endpoints. It is the executable acceptance test for the framework. - **Package root** is `org.vibeerp`. - **Build commands:** `./gradlew build` (full), `./gradlew test` (unit tests), `./gradlew :distribution:bootRun` (run dev profile, stages plug-in JAR automatically), `docker compose up -d db` (Postgres). The bootstrap admin password is printed to the application logs on first boot. - **Documentation:** site under `docs/`. Architecture spec and implementation plan under `docs/superpowers/specs/`. Live progress tracker in [`PROGRESS.md`](PROGRESS.md). - **Pushed to** https://github.com/reporkey/vibe-erp with GitHub Actions CI on every commit. For the per-unit status of every implementation plan item and the list of deferred work, see [`PROGRESS.md`](PROGRESS.md). ## What to do when asked to "just add" a printing-specific feature Stop and reframe. The correct shape is almost always: - a **generic mechanism** in the core (e.g. "documents with configurable line items and a configurable approval workflow"), plus - a **plug-in / config bundle** that uses that mechanism to express the specific printing-shop behavior. If you cannot see how to split a request that way, ask the user before writing code. ## The reference plug-in is the executable acceptance test `reference-customer/plugin-printing-shop/` is the framework's primary correctness test. It expresses the workflows in `raw/业务流程设计文档/` using **only `api.v1`**. Two consequences: - If a feature in `pbc/*` exists only to make this plug-in work, the design has failed guardrail #1 — that feature must move into the plug-in. - If the plug-in needs to reach into a `platform.*` or `pbc.*` internal class, the seam is wrong and `api.v1` needs to grow (deliberately, with a version bump consideration). CI must build and load this plug-in in an integration test environment against a real Postgres on every PR. A green core build with a red printing-shop integration test is a release-blocker.