Commit 32942f6e4f14232a6d5c1b87e784d95d84948103

Authored by vibe_erp
1 parent 6789d26b

docs: README, CONTRIBUTING, LICENSE, full docs site, architecture spec, implemen…

…tation plan, updated CLAUDE.md
CONTRIBUTING.md 0 → 100644
  1 +# Contributing to vibe_erp
  2 +
  3 +vibe_erp is a framework that other developers will build on. The rules below exist so the framework stays reusable across customers and upgrade-safe across releases. They are not negotiable.
  4 +
  5 +For the full architectural reasoning, see [`docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md) and `CLAUDE.md`.
  6 +
  7 +## The dependency rule
  8 +
  9 +The Gradle build enforces this. CI fails on violations.
  10 +
  11 +```
  12 +api/api-v1 depends on: nothing (Kotlin stdlib + jakarta.validation only)
  13 +platform/* depends on: api/api-v1 + Spring + libs
  14 +pbc/* depends on: api/api-v1 + platform/* (NEVER another pbc)
  15 +plugins (incl. ref) depend on: api/api-v1 only
  16 +```
  17 +
  18 +Consequences:
  19 +
  20 +- **PBCs never import each other.** `pbc-orders-sales` cannot declare `pbc-inventory` as a dependency. Cross-PBC interaction goes through (a) the event bus or (b) service interfaces declared in `api.v1.ext.<pbc>`.
  21 +- **Plug-ins only see `api.v1`.** Importing anything from `platform.*` or any PBC's internal package fails the plug-in linter at install time.
  22 +- **Reaching into `internal` packages via reflection is Grade D** and rejected by the loader.
  23 +
  24 +## Adding to `api.v1` is a one-way door — don't
  25 +
  26 +`api.v1` is the only semver-governed surface in the codebase. Once a class, method, or behavior is in `api.v1`, removing or renaming it requires a major version bump and a deprecation window of at least one major release.
  27 +
  28 +When in doubt, **keep things out of `api.v1`**. It is much easier to grow the API deliberately later than to maintain a regretted symbol forever.
  29 +
  30 +Adding to `api.v1` is OK when:
  31 +
  32 +- The reference printing-shop plug-in (or a plausible second customer plug-in) cannot express its requirement without it.
  33 +- The addition is a new type or a default-implemented method on an existing interface (binary-compatible).
  34 +- The addition has a written KDoc contract and at least one consuming test.
  35 +
  36 +Adding to `api.v1` is **not** OK when:
  37 +
  38 +- It exists only to make one PBC easier to write — that belongs in `platform.*`.
  39 +- It leaks an implementation detail (Hibernate type, Spring annotation, JPA entity).
  40 +- It is "we might need it later" — wait until you do.
  41 +
  42 +## Commit message convention
  43 +
  44 +Conventional Commits, scoped by module:
  45 +
  46 +```
  47 +feat(pbc-identity): add SCIM provisioning endpoint
  48 +fix(api-v1): tolerate missing locale on Translator.format
  49 +chore(platform-plugins): bump PF4J to 3.x
  50 +docs(architecture): clarify outbox seam
  51 +test(pbc-catalog): cover JSONB ext field round-trip
  52 +refactor(platform-persistence): extract tenant filter
  53 +```
  54 +
  55 +Allowed types: `feat`, `fix`, `chore`, `docs`, `test`, `refactor`, `build`, `ci`, `perf`, `revert`. Scope is the module name. Breaking changes use `!` after the scope (e.g. `feat(api-v1)!: …`) and require a `BREAKING CHANGE:` footer.
  56 +
  57 +## Pull request checklist
  58 +
  59 +Every PR must satisfy all of these:
  60 +
  61 +- [ ] Tests cover the new behavior. New PBC code has unit tests; new endpoints have integration tests against a real Postgres.
  62 +- [ ] Every public `api.v1` type has KDoc on the type and on every method, including parameter, return, and exception contracts.
  63 +- [ ] Liquibase changesets ship with **rollback blocks**. CI rejects PRs without them.
  64 +- [ ] Any user-facing string is referenced via an i18n key, never concatenated. Default `en-US` translation is added in the same PR.
  65 +- [ ] No printing-specific terminology in `pbc/*` or `platform/*`. Printing concepts live in `reference-customer/plugin-printing-shop/`.
  66 +- [ ] No new cross-PBC dependency. If you reached for one, design an `api.v1.ext.<pbc>` interface or a `DomainEvent` instead.
  67 +- [ ] Commit messages follow Conventional Commits.
  68 +- [ ] Documentation under `docs/` is updated in the same PR if you added or changed a public seam.
  69 +
  70 +## Build, test, and plug-in load commands
  71 +
  72 +```bash
  73 +# Build everything
  74 +./gradlew build
  75 +
  76 +# Run the full test suite
  77 +./gradlew test
  78 +
  79 +# Run a single PBC's tests
  80 +./gradlew :pbc:pbc-identity:test
  81 +
  82 +# Build the api-v1 jar (the contract that plug-ins consume)
  83 +./gradlew :api:api-v1:jar
  84 +
  85 +# Build the reference plug-in
  86 +./gradlew :reference-customer:plugin-printing-shop:jar
  87 +
  88 +# Boot the distribution and load the reference plug-in
  89 +cp reference-customer/plugin-printing-shop/build/libs/*.jar /tmp/vibeerp/plugins/
  90 +./gradlew :distribution:bootRun
  91 +```
  92 +
  93 +The plug-in loader logs every plug-in it scans, accepts, and rejects, with the reason. If a plug-in fails to load, the cause is in the boot log under the `platform-plugins` logger.
  94 +
  95 +## Style notes
  96 +
  97 +- Kotlin: idiomatic Kotlin, no `!!`, no `lateinit` outside test fixtures, prefer data classes for value objects.
  98 +- Public API: KDoc is required, not optional. Internal helpers may skip KDoc but should still be self-explanatory.
  99 +- No printing terminology in core, ever. "Item", "document", "operation", "work order" are generic. "Plate", "ink", "press", "color proof" are not.
... ...
LICENSE 0 → 100644
  1 + Apache License
  2 + Version 2.0, January 2004
  3 + http://www.apache.org/licenses/
  4 +
  5 + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
  6 +
  7 + 1. Definitions.
  8 +
  9 + "License" shall mean the terms and conditions for use, reproduction,
  10 + and distribution as defined by Sections 1 through 9 of this document.
  11 +
  12 + "Licensor" shall mean the copyright owner or entity authorized by
  13 + the copyright owner that is granting the License.
  14 +
  15 + "Legal Entity" shall mean the union of the acting entity and all
  16 + other entities that control, are controlled by, or are under common
  17 + control with that entity. For the purposes of this definition,
  18 + "control" means (i) the power, direct or indirect, to cause the
  19 + direction or management of such entity, whether by contract or
  20 + otherwise, or (ii) ownership of fifty percent (50%) or more of the
  21 + outstanding shares, or (iii) beneficial ownership of such entity.
  22 +
  23 + "You" (or "Your") shall mean an individual or Legal Entity
  24 + exercising permissions granted by this License.
  25 +
  26 + "Source" form shall mean the preferred form for making modifications,
  27 + including but not limited to software source code, documentation
  28 + source, and configuration files.
  29 +
  30 + "Object" form shall mean any form resulting from mechanical
  31 + transformation or translation of a Source form, including but
  32 + not limited to compiled object code, generated documentation,
  33 + and conversions to other media types.
  34 +
  35 + "Work" shall mean the work of authorship, whether in Source or
  36 + Object form, made available under the License, as indicated by a
  37 + copyright notice that is included in or attached to the work
  38 + (an example is provided in the Appendix below).
  39 +
  40 + "Derivative Works" shall mean any work, whether in Source or Object
  41 + form, that is based on (or derived from) the Work and for which the
  42 + editorial revisions, annotations, elaborations, or other modifications
  43 + represent, as a whole, an original work of authorship. For the purposes
  44 + of this License, Derivative Works shall not include works that remain
  45 + separable from, or merely link (or bind by name) to the interfaces of,
  46 + the Work and Derivative Works thereof.
  47 +
  48 + "Contribution" shall mean any work of authorship, including
  49 + the original version of the Work and any modifications or additions
  50 + to that Work or Derivative Works thereof, that is intentionally
  51 + submitted to Licensor for inclusion in the Work by the copyright owner
  52 + or by an individual or Legal Entity authorized to submit on behalf of
  53 + the copyright owner. For the purposes of this definition, "submitted"
  54 + means any form of electronic, verbal, or written communication sent
  55 + to the Licensor or its representatives, including but not limited to
  56 + communication on electronic mailing lists, source code control systems,
  57 + and issue tracking systems that are managed by, or on behalf of, the
  58 + Licensor for the purpose of discussing and improving the Work, but
  59 + excluding communication that is conspicuously marked or otherwise
  60 + designated in writing by the copyright owner as "Not a Contribution."
  61 +
  62 + "Contributor" shall mean Licensor and any individual or Legal Entity
  63 + on behalf of whom a Contribution has been received by Licensor and
  64 + subsequently incorporated within the Work.
  65 +
  66 + 2. Grant of Copyright License. Subject to the terms and conditions of
  67 + this License, each Contributor hereby grants to You a perpetual,
  68 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable
  69 + copyright license to reproduce, prepare Derivative Works of,
  70 + publicly display, publicly perform, sublicense, and distribute the
  71 + Work and such Derivative Works in Source or Object form.
  72 +
  73 + 3. Grant of Patent License. Subject to the terms and conditions of
  74 + this License, each Contributor hereby grants to You a perpetual,
  75 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable
  76 + (except as stated in this section) patent license to make, have made,
  77 + use, offer to sell, sell, import, and otherwise transfer the Work,
  78 + where such license applies only to those patent claims licensable
  79 + by such Contributor that are necessarily infringed by their
  80 + Contribution(s) alone or by combination of their Contribution(s)
  81 + with the Work to which such Contribution(s) was submitted. If You
  82 + institute patent litigation against any entity (including a
  83 + cross-claim or counterclaim in a lawsuit) alleging that the Work
  84 + or a Contribution incorporated within the Work constitutes direct
  85 + or contributory patent infringement, then any patent licenses
  86 + granted to You under this License for that Work shall terminate
  87 + as of the date such litigation is filed.
  88 +
  89 + 4. Redistribution. You may reproduce and distribute copies of the
  90 + Work or Derivative Works thereof in any medium, with or without
  91 + modifications, and in Source or Object form, provided that You
  92 + meet the following conditions:
  93 +
  94 + (a) You must give any other recipients of the Work or
  95 + Derivative Works a copy of this License; and
  96 +
  97 + (b) You must cause any modified files to carry prominent notices
  98 + stating that You changed the files; and
  99 +
  100 + (c) You must retain, in the Source form of any Derivative Works
  101 + that You distribute, all copyright, patent, trademark, and
  102 + attribution notices from the Source form of the Work,
  103 + excluding those notices that do not pertain to any part of
  104 + the Derivative Works; and
  105 +
  106 + (d) If the Work includes a "NOTICE" text file as part of its
  107 + distribution, then any Derivative Works that You distribute must
  108 + include a readable copy of the attribution notices contained
  109 + within such NOTICE file, excluding those notices that do not
  110 + pertain to any part of the Derivative Works, in at least one
  111 + of the following places: within a NOTICE text file distributed
  112 + as part of the Derivative Works; within the Source form or
  113 + documentation, if provided along with the Derivative Works; or,
  114 + within a display generated by the Derivative Works, if and
  115 + wherever such third-party notices normally appear. The contents
  116 + of the NOTICE file are for informational purposes only and
  117 + do not modify the License. You may add Your own attribution
  118 + notices within Derivative Works that You distribute, alongside
  119 + or as an addendum to the NOTICE text from the Work, provided
  120 + that such additional attribution notices cannot be construed
  121 + as modifying the License.
  122 +
  123 + You may add Your own copyright statement to Your modifications and
  124 + may provide additional or different license terms and conditions
  125 + for use, reproduction, or distribution of Your modifications, or
  126 + for any such Derivative Works as a whole, provided Your use,
  127 + reproduction, and distribution of the Work otherwise complies with
  128 + the conditions stated in this License.
  129 +
  130 + 5. Submission of Contributions. Unless You explicitly state otherwise,
  131 + any Contribution intentionally submitted for inclusion in the Work
  132 + by You to the Licensor shall be under the terms and conditions of
  133 + this License, without any additional terms or conditions.
  134 + Notwithstanding the above, nothing herein shall supersede or modify
  135 + the terms of any separate license agreement you may have executed
  136 + with Licensor regarding such Contributions.
  137 +
  138 + 6. Trademarks. This License does not grant permission to use the trade
  139 + names, trademarks, service marks, or product names of the Licensor,
  140 + except as required for describing the origin of the Work and
  141 + reproducing the content of the NOTICE file.
  142 +
  143 + 7. Disclaimer of Warranty. Unless required by applicable law or
  144 + agreed to in writing, Licensor provides the Work (and each
  145 + Contributor provides its Contributions) on an "AS IS" BASIS,
  146 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
  147 + implied, including, without limitation, any warranties or conditions
  148 + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
  149 + PARTICULAR PURPOSE. You are solely responsible for determining the
  150 + appropriateness of using or redistributing the Work and assume any
  151 + risks associated with Your exercise of permissions under this License.
  152 +
  153 + 8. Limitation of Liability. In no event and under no legal theory,
  154 + whether in tort (including negligence), contract, or otherwise,
  155 + unless required by applicable law (such as deliberate and grossly
  156 + negligent acts) or agreed to in writing, shall any Contributor be
  157 + liable to You for damages, including any direct, indirect, special,
  158 + incidental, or consequential damages of any character arising as a
  159 + result of this License or out of the use or inability to use the
  160 + Work (including but not limited to damages for loss of goodwill,
  161 + work stoppage, computer failure or malfunction, or any and all
  162 + other commercial damages or losses), even if such Contributor
  163 + has been advised of the possibility of such damages.
  164 +
  165 + 9. Accepting Warranty or Additional Liability. While redistributing
  166 + the Work or Derivative Works thereof, You may choose to offer,
  167 + and charge a fee for, acceptance of support, warranty, indemnity,
  168 + or other liability obligations and/or rights consistent with this
  169 + License. However, in accepting such obligations, You may act only
  170 + on Your own behalf and on Your sole responsibility, not on behalf
  171 + of any other Contributor, and only if You agree to indemnify,
  172 + defend, and hold each Contributor harmless for any liability
  173 + incurred by, or claims asserted against, such Contributor by reason
  174 + of your accepting any such warranty or additional liability.
  175 +
  176 + END OF TERMS AND CONDITIONS
  177 +
  178 + APPENDIX: How to apply the Apache License to your work.
  179 +
  180 + To apply the Apache License to your work, attach the following
  181 + boilerplate notice, with the fields enclosed by brackets "[]"
  182 + replaced with your own identifying information. (Don't include
  183 + the brackets!) The text should be enclosed in the appropriate
  184 + comment syntax for the file format. We also recommend that a
  185 + file or class name and description of purpose be included on the
  186 + same "printed page" as the copyright notice for easier
  187 + identification within third-party archives.
  188 +
  189 + Copyright [yyyy] [name of copyright owner]
  190 +
  191 + Licensed under the Apache License, Version 2.0 (the "License");
  192 + you may not use this file except in compliance with the License.
  193 + You may obtain a copy of the License at
  194 +
  195 + http://www.apache.org/licenses/LICENSE-2.0
  196 +
  197 + Unless required by applicable law or agreed to in writing, software
  198 + distributed under the License is distributed on an "AS IS" BASIS,
  199 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
  200 + implied. See the License for the specific language governing
  201 + permissions and limitations under the License.
... ...
README.md 0 → 100644
  1 +# vibe_erp
  2 +
  3 +vibe_erp is an ERP/EBC framework for the printing industry, sold worldwide and deployed self-hosted-first. It is not an ERP application: it is the substrate on which any printing shop's workflows, forms, roles, and rules can be assembled by configuration and plug-ins instead of by forking the core. The reference business under `raw/` is one example customer, used as an executable acceptance test, never as the spec.
  4 +
  5 +## Why a framework, not an app
  6 +
  7 +Printing shops differ in process, terminology, paperwork, and order of operations. An app that hard-codes one shop's workflow becomes a fork farm the moment a second customer signs up. vibe_erp follows the **Clean Core** philosophy borrowed from SAP S/4HANA: the core stays generic and upgrade-safe, and every customer-specific concept lives in metadata rows or plug-ins. The core never knows what a "plate" or a "press" is — those concepts are introduced by a plug-in. Upgrading the core does not touch the customer's extensions.
  8 +
  9 +## Architecture in one picture
  10 +
  11 +```
  12 +┌──────────────────────────────────────────────────────────────────────┐
  13 +│ Customer's network │
  14 +│ │
  15 +│ Browser (React SPA) ─┐ │
  16 +│ AI agent (MCP, v1.1)─┼─► Reverse proxy ──► vibe_erp backend (1 image)│
  17 +│ 3rd-party system ─┘ │ │
  18 +│ │ │
  19 +│ Inside the image (one Spring Boot process): │ │
  20 +│ ┌─────────────────────────────────────┐ │ │
  21 +│ │ HTTP layer (REST + OpenAPI + MCP) │ │ │
  22 +│ ├─────────────────────────────────────┤ │ │
  23 +│ │ Public Plug-in API (api.v1.*) │◄──┤ loaded from │
  24 +│ │ — the only stable contract │ │ ./plugins/*.jar │
  25 +│ ├─────────────────────────────────────┤ │ via PF4J │
  26 +│ │ Core PBCs (modular monolith): │ │ │
  27 +│ │ identity · catalog · partners · │ │ │
  28 +│ │ inventory · warehousing · │ │ │
  29 +│ │ orders-sales · orders-purchase · │ │ │
  30 +│ │ production · quality · finance │ │ │
  31 +│ ├─────────────────────────────────────┤ │ │
  32 +│ │ Cross-cutting: │ │ │
  33 +│ │ • Flowable (workflows-as-data) │ │ │
  34 +│ │ • Metadata store (Doctype-style) │ │ │
  35 +│ │ • i18n (ICU MessageFormat) │ │ │
  36 +│ │ • Reporting (JasperReports) │ │ │
  37 +│ │ • Job scheduler (Quartz) │ │ │
  38 +│ │ • Audit, security, events │ │ │
  39 +│ └─────────────────────────────────────┘ │ │
  40 +│ ▼ │
  41 +│ PostgreSQL (mandatory) │
  42 +│ File store (local or S3) │
  43 +└──────────────────────────────────────────────────────────────────────┘
  44 +
  45 +Optional sidecars for larger deployments (off by default):
  46 + • Keycloak (OIDC) • Redis (cache + queue)
  47 + • OpenSearch (search) • SMTP relay
  48 +```
  49 +
  50 +The full architecture lives at [`docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md).
  51 +
  52 +## Stack
  53 +
  54 +- Kotlin on the JVM, Spring Boot, single fat-JAR / single Docker image
  55 +- PostgreSQL (the only mandatory external dependency)
  56 +- Embedded Flowable (BPMN 2.0) for workflows-as-data
  57 +- PF4J + Spring Boot child contexts for plug-in classloader isolation
  58 +- ICU4J + Spring `MessageSource` for i18n
  59 +- JasperReports for reporting
  60 +- React + TypeScript SPA for the web client
  61 +
  62 +## Repository layout
  63 +
  64 +```
  65 +vibe-erp/
  66 +├── api/
  67 +│ └── api-v1/ ← THE CONTRACT (semver, published to Maven Central)
  68 +├── platform/ ← Framework runtime (internal)
  69 +├── pbc/ ← Core PBCs (one Gradle subproject each)
  70 +├── reference-customer/
  71 +│ └── plugin-printing-shop/ ← Reference plug-in, built and CI-tested, not loaded by default
  72 +├── web/ ← React + TypeScript SPA
  73 +├── docs/ ← Framework documentation
  74 +└── distribution/ ← Bootable assembly: fat JAR + Docker image
  75 +```
  76 +
  77 +## Building
  78 +
  79 +This is a v0.1 skeleton. Not all modules ship yet — expect commands below to work for the modules that exist and to be no-ops or stubs for the rest.
  80 +
  81 +```bash
  82 +# Build everything that currently exists
  83 +./gradlew build
  84 +
  85 +# Run the bootable distribution against a local Postgres
  86 +./gradlew :distribution:bootRun
  87 +
  88 +# Or bring everything (Postgres + vibe_erp) up with Docker
  89 +docker compose up
  90 +```
  91 +
  92 +## Status
  93 +
  94 +**v0.1** — buildable skeleton, one PBC implemented end-to-end (`pbc-identity`), plug-in loading proven with a hello-world reference plug-in. Not production. See [`docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md) for the full v1.0 plan.
  95 +
  96 +## Documentation
  97 +
  98 +- [Documentation index](docs/index.md)
  99 +- [Architecture overview](docs/architecture/overview.md)
  100 +- [Plug-in API overview](docs/plugin-api/overview.md)
  101 +- [Plug-in author getting started](docs/plugin-author/getting-started.md)
  102 +- [Workflow authoring guide](docs/workflow-authoring/guide.md)
  103 +- [Form authoring guide](docs/form-authoring/guide.md)
  104 +- [i18n guide](docs/i18n/guide.md)
  105 +- [Customer onboarding guide](docs/customer-onboarding/guide.md)
  106 +
  107 +## License
  108 +
  109 +Apache License 2.0. See [`LICENSE`](LICENSE).
... ...
docs/architecture/overview.md 0 → 100644
  1 +# Architecture overview
  2 +
  3 +This document distills the vibe_erp architecture for someone who has not read the full spec. The complete design lives at [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md). Read that when you need the full reasoning, the foundational decisions table, the v1.0 cut line, or the risk register.
  4 +
  5 +## What vibe_erp is
  6 +
  7 +vibe_erp is an **ERP/EBC framework** — not an ERP application — targeting the **printing industry**, sold worldwide and deployed self-hosted-first. The whole point of the project is **reusability across customers**: any printing shop, with its own workflows, forms, roles, and rules, should be onboardable by configuration and plug-ins, without forking the core.
  8 +
  9 +The reference printing-shop business documented under `raw/业务流程设计文档/` is **one example customer**, treated as a fixture and an executable acceptance test, never as a specification. No part of its workflow is hard-coded into the core.
  10 +
  11 +## Clean Core philosophy
  12 +
  13 +vibe_erp adopts SAP S/4HANA's **Clean Core** vocabulary: **extensions never modify the core**. The core is stable, generic, upgrade-safe, and domain-agnostic. Everything customer-specific lives in metadata rows or in plug-ins. This is enforced, not aspirational:
  14 +
  15 +1. **Core stays domain-agnostic.** No printing-specific entity, field, status, or workflow step belongs in the framework core. "Plate", "ink", "press", "color proof" live in plug-ins or in configuration.
  16 +2. **Workflows are data, not code.** State machines, approval chains, and form definitions are declarative, so a new customer is onboarded by editing definitions, not source.
  17 +3. **Extensibility seams come first.** Before any feature is added, the extension point it hangs off of must exist. If no seam exists, the seam is designed first.
  18 +4. **The reference customer is a test, not a requirement.** Anything implemented for the reference plug-in must be expressible by a different printing shop with different rules, without code changes.
  19 +5. **Multi-tenant in spirit from day one.** Even single-tenant deployments use the same multi-tenant code path.
  20 +6. **Global / i18n from day one.** No hard-coded user-facing strings, currencies, date formats, time zones, address shapes, or tax models.
  21 +
  22 +## Two-tier extensibility
  23 +
  24 +vibe_erp offers **two extension paths**, both first-class. The same outcome can often be reached through either path; Tier 1 is preferred when expressive enough.
  25 +
  26 +### Tier 1 — Key user, no-code
  27 +
  28 +Business analysts customize the system through the web UI. Everything they create is stored as **rows in the metadata tables**, scoped to their tenant, tagged `source = 'user'`, and preserved across plug-in install/uninstall and core upgrades.
  29 +
  30 +| Capability | Stored in |
  31 +|---|---|
  32 +| Custom field on an existing entity | `metadata__custom_field` → JSONB `ext` column at runtime |
  33 +| Custom form layout | `metadata__form` (JSON Schema + UI Schema) |
  34 +| Custom list view, filter, column set | `metadata__list_view` |
  35 +| Custom workflow | `metadata__workflow` → deployed to Flowable as BPMN |
  36 +| Simple "if X then Y" automation | `metadata__rule` |
  37 +| Custom entity (Doctype-style) | `metadata__entity` → auto-generated table at apply time |
  38 +| Custom report | `metadata__report` |
  39 +| Translations override | `metadata__translation` |
  40 +
  41 +No build, no restart, no deploy. The OpenAPI spec, the AI-agent function catalog, and the REST API auto-update from the metadata.
  42 +
  43 +### Tier 2 — Developer, pro-code
  44 +
  45 +Software developers ship a **PF4J plug-in JAR**. The plug-in:
  46 +
  47 +- Sees only `org.vibeerp.api.v1.*` — the public, semver-governed contract.
  48 +- Cannot import `org.vibeerp.platform.*` or any PBC's internal classes.
  49 +- Lives in its own classloader, its own Spring child context, its own DB schema namespace (`plugin_<id>__*`), its own metadata-source tag.
  50 +- Can register: new entities, REST endpoints, workflow tasks, form widgets, report templates, event listeners, permissions, menu entries, and React micro-frontends.
  51 +
  52 +### Extension grading
  53 +
  54 +Borrowed from SAP. The plug-in loader knows these grades and acts on them.
  55 +
  56 +| Grade | Definition | Upgrade safety |
  57 +|---|---|---|
  58 +| **A** | Tier 1 only (metadata) | Always safe across any core version |
  59 +| **B** | Tier 2, uses only `api.v1` stable surface | Safe within a major version |
  60 +| **C** | Tier 2, uses deprecated-but-supported `api.v1` symbols | Safe until next major; loader emits warnings |
  61 +| **D** | Tier 2, reaches into internal classes via reflection | UNSUPPORTED; loader rejects unless explicitly overridden; will break |
  62 +
  63 +A guiding principle: **anything a Tier 2 plug-in does should also become possible as Tier 1 over time.** Tier 2 is the escape hatch where Tier 1 is not yet expressive enough.
  64 +
  65 +## Modular monolith of PBCs
  66 +
  67 +vibe_erp is a **modular monolith**: one Spring Boot process per instance, one Docker image per release, but internally divided into strict bounded contexts called **Packaged Business Capabilities (PBCs)**. Each PBC is its own Gradle subproject, its own table-name prefix, its own bounded context, its own public API surface.
  68 +
  69 +The v1.0 core PBCs:
  70 +
  71 +```
  72 +pbc-identity pbc-catalog pbc-partners
  73 +pbc-inventory pbc-warehousing
  74 +pbc-orders-sales pbc-orders-purchase
  75 +pbc-production pbc-quality pbc-finance
  76 +```
  77 +
  78 +These names are **illustrative core capabilities**. None is printing-specific. The reference printing-shop plug-in lives entirely outside `pbc/` under `reference-customer/plugin-printing-shop/`, built and CI-tested but **not loaded by default**.
  79 +
  80 +### Per-PBC layout
  81 +
  82 +Every PBC follows the same shape:
  83 +
  84 +```
  85 +pbc-orders-sales/
  86 +├── api/ ← service contracts re-exported by api.v1
  87 +├── domain/ ← entities, value objects, domain services
  88 +├── application/ ← use cases / application services
  89 +├── infrastructure/ ← Hibernate mappings, repositories
  90 +├── http/ ← REST controllers
  91 +├── workflow/ ← BPMN files, task handlers
  92 +├── metadata/ ← seed metadata (default forms, rules)
  93 +├── i18n/ ← message bundles
  94 +└── migrations/ ← Liquibase changesets (own table prefix)
  95 +```
  96 +
  97 +## The dependency rule
  98 +
  99 +The single rule that makes "modular monolith now, splittable later" real instead of aspirational. Enforced by the Gradle build; CI fails on violations.
  100 +
  101 +```
  102 +api/api-v1 depends on: nothing (Kotlin stdlib + jakarta.validation only)
  103 +platform/* depends on: api/api-v1 + Spring + libs
  104 +pbc/* depends on: api/api-v1 + platform/* (NEVER another pbc)
  105 +plugins (incl. ref) depend on: api/api-v1 only
  106 +```
  107 +
  108 +**PBCs never import each other.** Cross-PBC interaction goes through one of two channels:
  109 +
  110 +1. **The event bus.** A PBC publishes a typed `DomainEvent`. Any other PBC (or any plug-in) subscribes via `EventListener`. The default bus is in-process; an outbox table in Postgres is the seam where Kafka or NATS can plug in later without changing PBC code.
  111 +2. **Service interfaces declared in `api.v1.ext.<pbc>`.** When a synchronous call is genuinely needed, it goes through a typed interface in `api.v1.ext`, not through a direct class reference.
  112 +
  113 +If you find yourself wanting to add a `pbc-foo → pbc-bar` Gradle dependency, the seam is wrong. Design an event or an `api.v1.ext.bar` interface instead.
  114 +
  115 +## The `api.v1` contract
  116 +
  117 +`api.v1` is the **only stable contract** in the entire codebase. Everything in `org.vibeerp.api.v1.*` is binary-stable within the `1.x` line. Everything not in `api.v1` is internal and may change in any release.
  118 +
  119 +Package layout:
  120 +
  121 +```
  122 +org.vibeerp.api.v1
  123 +├── core/ Tenant, Locale, Money, Quantity, Id<T>, Result<T,E>
  124 +├── entity/ Entity, Field, FieldType, EntityRegistry
  125 +├── persistence/ Repository<T>, Query, Page, Transaction
  126 +├── workflow/ WorkflowTask, WorkflowEvent, TaskHandler
  127 +├── form/ FormSchema, UiSchema
  128 +├── http/ @PluginEndpoint, RequestContext, ResponseBuilder
  129 +├── event/ DomainEvent, EventListener, EventBus
  130 +├── security/ Principal, Permission, PermissionCheck
  131 +├── i18n/ MessageKey, Translator, LocaleProvider
  132 +├── reporting/ ReportTemplate, ReportContext
  133 +├── plugin/ Plugin, PluginManifest, ExtensionPoint
  134 +└── ext/ Typed extension interfaces a plug-in implements
  135 +```
  136 +
  137 +`api.v1` is published as `api-v1.jar` to **Maven Central**, so plug-in authors can build against it without pulling the entire vibe_erp source tree.
  138 +
  139 +### Upgrade contract
  140 +
  141 +| Change | Allowed within 1.x? |
  142 +|---|---|
  143 +| Add a class to `api.v1` | yes |
  144 +| Add a method to an `api.v1` interface (with default impl) | yes |
  145 +| Remove or rename anything in `api.v1` | no — major bump |
  146 +| Change behavior of an `api.v1` symbol in a way plug-ins can observe | no — major bump |
  147 +| Anything in `platform.*` or `pbc.*.internal.*` | yes — that is why it is internal |
  148 +
  149 +When in doubt, **keep things out of `api.v1`**.
  150 +
  151 +## Topology
  152 +
  153 +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.
  154 +
  155 +```
  156 +┌──────────────────────────────────────────────────────────────────────┐
  157 +│ Customer's network │
  158 +│ │
  159 +│ Browser (React SPA) ─┐ │
  160 +│ AI agent (MCP, v1.1)─┼─► Reverse proxy ──► vibe_erp backend (1 image)│
  161 +│ 3rd-party system ─┘ │ │
  162 +│ │ │
  163 +│ Inside the image (one Spring Boot process): │ │
  164 +│ ┌─────────────────────────────────────┐ │ │
  165 +│ │ HTTP layer (REST + OpenAPI + MCP) │ │ │
  166 +│ ├─────────────────────────────────────┤ │ │
  167 +│ │ Public Plug-in API (api.v1.*) │◄──┤ loaded from │
  168 +│ │ — the only stable contract │ │ ./plugins/*.jar │
  169 +│ ├─────────────────────────────────────┤ │ via PF4J │
  170 +│ │ Core PBCs (modular monolith) │ │ │
  171 +│ ├─────────────────────────────────────┤ │ │
  172 +│ │ Cross-cutting: │ │ │
  173 +│ │ • Flowable (workflows-as-data) │ │ │
  174 +│ │ • Metadata store (Doctype-style) │ │ │
  175 +│ │ • i18n (ICU MessageFormat) │ │ │
  176 +│ │ • Reporting (JasperReports) │ │ │
  177 +│ │ • Job scheduler (Quartz) │ │ │
  178 +│ │ • Audit, security, events │ │ │
  179 +│ └─────────────────────────────────────┘ │ │
  180 +│ ▼ │
  181 +│ PostgreSQL (mandatory) │
  182 +│ File store (local or S3) │
  183 +└──────────────────────────────────────────────────────────────────────┘
  184 +```
  185 +
  186 +The only mandatory external dependency is **PostgreSQL**. Optional sidecars for larger deployments — Keycloak, Redis, OpenSearch, SMTP relay — are off by default.
  187 +
  188 +## Multi-tenancy
  189 +
  190 +vibe_erp is **multi-tenant in spirit from day one**, even when deployed for a single customer. The same code path serves self-hosted single-tenant and hosted multi-tenant deployments.
  191 +
  192 +### Schema namespacing
  193 +
  194 +PBCs and plug-ins use **table name prefixes**, not Postgres schemas:
  195 +
  196 +```
  197 +identity__user, identity__role
  198 +catalog__item, catalog__item_attribute
  199 +inventory__stock_item, inventory__movement
  200 +orders_sales__order, orders_sales__order_line
  201 +production__work_order, production__operation
  202 +plugin_printingshop__plate_spec (reference plug-in)
  203 +metadata__custom_field, metadata__form, metadata__workflow
  204 +flowable_* (Flowable's own tables, untouched)
  205 +```
  206 +
  207 +This keeps Hibernate, RLS policies, and migrations all in one logical schema (`public`), avoids `search_path` traps, and gives clean uninstall semantics.
  208 +
  209 +### Tenant isolation — two independent walls
  210 +
  211 +- Every business table has `tenant_id`, NOT NULL.
  212 +- Hibernate `@TenantId` filters every query at the application layer.
  213 +- Postgres Row-Level Security policies filter every query at the database layer.
  214 +
  215 +Two independent walls. A bug in one is not a data leak. Self-hosted single-customer deployments use one tenant row called `default`. Hosted multi-tenant deployments use many tenant rows. **Same code path.**
  216 +
  217 +### Data sovereignty
  218 +
  219 +- Self-hosted is automatically compliant — the customer chose where Postgres lives.
  220 +- Hosted supports per-region tenant routing: each tenant row carries a region; connections are routed to the right regional Postgres cluster.
  221 +- PII tagging on field metadata drives auto-generated DSAR exports and erasure jobs (GDPR Articles 15/17).
  222 +- Append-only audit log records access to PII fields when audit-strict mode is on.
  223 +
  224 +## Custom fields via JSONB
  225 +
  226 +Every business table has:
  227 +
  228 +```sql
  229 +ext jsonb not null default '{}',
  230 +ext_meta text generated
  231 +```
  232 +
  233 +Custom fields are JSON keys inside `ext`. A GIN index on `ext` makes them queryable. The `metadata__custom_field` table describes the JSON shape per entity per tenant. The form designer, list views, OpenAPI generator, and AI-agent function catalog all read from this table.
  234 +
  235 +Why JSONB and not EAV: one row, one read, indexable, no migrations needed for additions, no joins. EAV is the wrong tool.
  236 +
  237 +For the rare hot-path custom field, an operator can promote a JSON key to a real generated column via an auto-generated Liquibase changeset. This is an optimization, not the default.
  238 +
  239 +## The metadata store
  240 +
  241 +```
  242 +metadata__entity metadata__form metadata__permission
  243 +metadata__custom_field metadata__list_view metadata__role_permission
  244 +metadata__workflow metadata__rule metadata__menu
  245 +metadata__report metadata__translation metadata__plugin_config
  246 +```
  247 +
  248 +Every row carries `tenant_id`, `source` (`core` / `plugin:<id>` / `user`), `version`, `is_active`. The `source` column makes uninstall and upgrade safe: removing a plug-in cleans up its metadata, and user-created metadata is sacred and never touched by an upgrade.
  249 +
  250 +Every consumer reads from this store: the form renderer, list views, OpenAPI generator, AI-agent function catalog, role editor. There are no parallel sources of truth.
  251 +
  252 +## The workflow engine
  253 +
  254 +vibe_erp embeds **Flowable** (BPMN 2.0). Workflows are data: `.bpmn` files at design time, `flowable_*` tables at runtime. Two authoring paths exist, both first-class:
  255 +
  256 +- **Tier 1 (visual designer in the web UI, v1.0):** business analysts draw workflows in a BPMN designer. The result is stored as a row in `metadata__workflow`, deployed to Flowable, and tagged `source = 'user'`.
  257 +- **Tier 2 (`.bpmn` files in a plug-in JAR):** plug-in authors ship BPMN files in their JAR. The plug-in lifecycle deploys them on plug-in install.
  258 +
  259 +Workflows interact with the system through:
  260 +
  261 +- **Service tasks** that call typed `TaskHandler` implementations. A plug-in registers a `TaskHandler` via `@Extension(point = TaskHandler::class)`; the workflow references it by id. There is no scripting language to invent — the `TaskHandler` is just Kotlin code behind a typed interface.
  262 +- **User tasks** that render forms from `metadata__form` definitions. The form is referenced by id; rendering goes through the same code path used by Tier 1 forms.
  263 +
  264 +The temptation to invent a vibe_erp-only workflow language must be rejected. BPMN 2.0 via Flowable is the standard, and the standard is the contract.
  265 +
  266 +## Cross-cutting concerns
  267 +
  268 +| Concern | Approach |
  269 +|---|---|
  270 +| Security | `PermissionCheck` declared in `api.v1.security`; plug-ins register their own permissions, auto-listed in the role editor |
  271 +| Transactions | Spring `@Transactional` at the application-service layer; plug-ins use `api.v1.persistence.Transaction`, never Spring directly |
  272 +| Audit | `created_at`, `created_by`, `updated_at`, `updated_by`, `tenant_id` on every entity, applied by a JPA listener |
  273 +| Events | Typed `DomainEvent`s on every state change; in-process bus by default; outbox table in Postgres for cross-crash reliability and as the seam where Kafka/NATS plugs in later |
  274 +| AI-agent surface | Same business operations exposed through REST are exposable through an MCP server; v1.1 ships the MCP endpoint, v1.0 architects the seam |
  275 +| Reporting | JasperReports; templates shipped by core or by plug-ins, customer-skinnable |
  276 +| i18n | ICU MessageFormat, locale-aware formatting for dates, numbers, and currencies; no string concatenation in user-facing code |
  277 +
  278 +## Where to read more
  279 +
  280 +- The full architecture spec: [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md)
  281 +- The architectural guardrails (the rules that exist because reusability across customers is the entire point of the project): `CLAUDE.md` at the repo root.
  282 +- Plug-in API surface: [`../plugin-api/overview.md`](../plugin-api/overview.md)
  283 +- Building your first plug-in: [`../plugin-author/getting-started.md`](../plugin-author/getting-started.md)
... ...
docs/customer-onboarding/guide.md 0 → 100644
  1 +# Customer onboarding guide
  2 +
  3 +This guide is for an **integrator** standing up vibe_erp for a new customer end-to-end. It is written from a Tier 1 perspective: no plug-in code is required to follow it. Where Tier 2 plug-ins are useful, the guide says so.
  4 +
  5 +For the architectural background, see [`../architecture/overview.md`](../architecture/overview.md). For the full v1.0 cut line, see section 11 of [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md).
  6 +
  7 +## Honest scope note
  8 +
  9 +vibe_erp is a framework, not a turnkey product. **The Tier 1 customization UIs (custom field designer, form designer, BPMN designer, list view editor, role editor) are v1.0 deliverables.** v0.1 ships only the underlying API surface — the metadata tables, the REST endpoints, the seed data — without the polished UIs on top of them. Until v1.0 ships, the steps below that say "use the Customize UI" mean "POST to the metadata REST endpoints" or "edit a YAML seed file in your plug-in JAR".
  10 +
  11 +This guide is written against the v1.0 experience. Where v0.1 deviates, the step is annotated **(v0.1: API only)**.
  12 +
  13 +## 1. Install the host
  14 +
  15 +vibe_erp ships as a single Docker image with PostgreSQL as the only mandatory external dependency.
  16 +
  17 +```bash
  18 +docker run -d --name vibe-erp \
  19 + -p 8080:8080 \
  20 + -v /srv/vibeerp:/opt/vibe-erp \
  21 + -e DB_URL=jdbc:postgresql://db.internal:5432/vibeerp \
  22 + -e DB_USER=vibeerp \
  23 + -e DB_PASSWORD=... \
  24 + ghcr.io/vibeerp/vibe-erp:1.0.0
  25 +```
  26 +
  27 +What happens on first boot:
  28 +
  29 +1. The host connects to Postgres.
  30 +2. Liquibase runs every core PBC's migrations and creates the `flowable_*` tables.
  31 +3. A `default` tenant row is created in `identity__tenant`.
  32 +4. A bootstrap admin user is created and its one-time password is printed to the boot log.
  33 +5. The host is ready in under 30 seconds.
  34 +
  35 +The mounted volume `/srv/vibeerp` (mapped to `/opt/vibe-erp` inside the container) holds:
  36 +
  37 +```
  38 +/opt/vibe-erp/
  39 +├── config/vibe-erp.yaml single config file (closed key set)
  40 +├── plugins/ drop *.jar to install
  41 +├── i18n-overrides/ tenant-level translation overrides
  42 +├── files/ file store (if not using S3)
  43 +└── logs/
  44 +```
  45 +
  46 +Customer extensions live entirely outside the image. Upgrading the host is `docker rm` plus `docker run` with the new image tag — extensions and config stay put.
  47 +
  48 +## 2. Log in as the bootstrap admin
  49 +
  50 +Open `http://<host>:8080/`, log in with `admin` and the one-time password from the boot log, and change the password immediately. Configure OIDC (Keycloak-compatible) at this point if the customer has an existing identity provider — built-in JWT auth and OIDC SSO are both shipped from day one.
  51 +
  52 +## 3. Create a tenant (hosted only)
  53 +
  54 +For a self-hosted single-customer deployment, the `default` tenant is everything you need. Skip this step.
  55 +
  56 +For a hosted multi-tenant deployment, create one tenant row per customer. Each tenant carries:
  57 +
  58 +- A unique tenant id.
  59 +- A region (used by the per-region routing layer in hosted mode).
  60 +- A default locale, currency, and time zone.
  61 +
  62 +Tenant onboarding is an `INSERT` plus seed metadata, not a migration — sub-second per tenant.
  63 +
  64 +## 4. Use the Customize UI to model the customer's reality
  65 +
  66 +This is where most of the integrator's time is spent. Everything here is **Tier 1**: rows in `metadata__*` tables, tagged `source = 'user'`, scoped to the tenant. No build, no restart, no deploy. **(v0.1: API only — POST to the metadata endpoints; the UIs ship in v1.0.)**
  67 +
  68 +### Custom fields
  69 +
  70 +Add custom fields to existing entities (item, partner, order, work order, …). Each custom field is a row in `metadata__custom_field` and a JSON key in the entity's `ext` JSONB column at runtime. A GIN index on `ext` keeps custom fields queryable. The form designer, list views, OpenAPI spec, and AI-agent function catalog all auto-update from the metadata.
  71 +
  72 +For the rare hot-path field, an operator can promote a JSON key to a real generated column via an auto-generated Liquibase changeset. This is an optimization, not the default.
  73 +
  74 +### Custom forms
  75 +
  76 +Define how each entity is displayed and edited. Forms are JSON Schema (data shape) plus UI Schema (layout and widgets), stored in `metadata__form`. Forms can reference custom fields by their key. See the [form authoring guide](../form-authoring/guide.md).
  77 +
  78 +### Custom workflows
  79 +
  80 +Draw the customer's process flow in the BPMN designer. Workflows are stored in `metadata__workflow` and deployed to the embedded Flowable engine. User tasks render forms; service tasks call typed `TaskHandler` implementations registered by plug-ins. See the [workflow authoring guide](../workflow-authoring/guide.md).
  81 +
  82 +### Custom list views, rules, menus, reports
  83 +
  84 +- **List views** (`metadata__list_view`): which columns, which filters, which default sort.
  85 +- **Rules** (`metadata__rule`): simple "if X then Y" automations for the cases that do not warrant a full BPMN workflow.
  86 +- **Menus** (`metadata__menu`): the navigation tree the user sees.
  87 +- **Reports** (`metadata__report`): JasperReports templates the customer can run on demand or on a schedule.
  88 +
  89 +## 5. Import master data
  90 +
  91 +vibe_erp accepts master data through two paths:
  92 +
  93 +- **CSV import** through the Customize UI for one-off bulk loads (catalog items, partners, opening stock, chart of accounts). **(v0.1: API only.)**
  94 +- **REST API** for ongoing integration with the customer's existing systems. Every entity, including custom fields, is exposed through OpenAPI-documented REST endpoints. The OpenAPI spec auto-updates as you add custom fields and custom entities, so the integration code is never out of date.
  95 +
  96 +## 6. Configure roles and permissions
  97 +
  98 +Define the customer's roles (e.g. *sales clerk*, *production planner*, *warehouse operator*, *finance reviewer*) and assign permissions. Permissions are auto-discovered from:
  99 +
  100 +- Core PBCs.
  101 +- Plug-ins (each plug-in registers its own permissions through `api.v1.security.PermissionCheck`).
  102 +- Custom entities (each generates a standard CRUD permission set).
  103 +
  104 +Bind users to roles, bind roles to tenants. Hosted deployments can also bind roles via OIDC group claims.
  105 +
  106 +## 7. Add Tier 2 plug-ins as needed
  107 +
  108 +When Tier 1 metadata is not expressive enough — for example, a printing-specific entity like *plate spec* with its own validation logic, or a workflow service task that calls an external MIS — install a Tier 2 plug-in.
  109 +
  110 +To install a plug-in:
  111 +
  112 +1. Drop the JAR into `/opt/vibe-erp/plugins/`.
  113 +2. Restart the host (`docker restart vibe-erp`). Hot reload is on the v1.2+ roadmap.
  114 +3. The plug-in loader scans the JAR, validates the manifest, runs the plug-in linter, runs the plug-in's Liquibase migrations in `plugin_<id>__*`, seeds the plug-in's metadata, and starts the plug-in.
  115 +4. Verify the plug-in shows up under `/actuator/health` and in the boot log.
  116 +
  117 +The reference printing-shop plug-in (`reference-customer/plugin-printing-shop/`) is a worked example of a non-trivial Tier 2 plug-in. It expresses the workflows in `raw/业务流程设计文档/` using **only `api.v1`** and is built and CI-tested on every PR. It is **not** loaded by default — drop its JAR into `./plugins/` to use it.
  118 +
  119 +A guiding principle: **anything a Tier 2 plug-in does should also become possible as Tier 1 over time.** Tier 2 is the escape hatch where Tier 1 is not yet expressive enough. When you find yourself reaching for a plug-in to do something a metadata row could express, file an issue against the framework.
  120 +
  121 +## 8. Go-live checklist
  122 +
  123 +- [ ] Backups configured against the Postgres instance. Customer data lives there exclusively (plus the file store).
  124 +- [ ] Audit log enabled for PII fields if the customer is subject to GDPR or equivalent.
  125 +- [ ] DSAR export and erasure jobs tested against a non-production tenant.
  126 +- [ ] Locale, currency, time zone defaults set for the tenant.
  127 +- [ ] OIDC integration tested with the customer's identity provider.
  128 +- [ ] Health check (`/actuator/health`) wired into the customer's monitoring.
  129 +- [ ] Documented upgrade procedure for the operator: `docker rm` plus `docker run` with the new image tag, plug-ins and config stay put.
  130 +
  131 +## What this guide deliberately does not cover
  132 +
  133 +- **Plug-in development.** That is the [plug-in author getting-started guide](../plugin-author/getting-started.md).
  134 +- **Hosted multi-tenant operations** (per-region routing, billing, tenant provisioning UI). Hosted mode is a v2 deliverable; the data model and routing layer are architected for it from day one but the operations UI is not in v1.0.
  135 +- **MCP / AI-agent endpoint.** v1.1 deliverable. The seam exists in v1.0; the endpoint does not.
... ...
docs/form-authoring/guide.md 0 → 100644
  1 +# Form authoring guide
  2 +
  3 +Forms in vibe_erp describe how an entity is displayed and edited. They are **data, not code** — stored as `metadata__form` rows, consumed by one renderer, and used identically inside and outside workflows.
  4 +
  5 +For where forms sit in the architecture, see [`../architecture/overview.md`](../architecture/overview.md). For the workflow side, see the [workflow authoring guide](../workflow-authoring/guide.md).
  6 +
  7 +## JSON Schema + UI Schema
  8 +
  9 +A vibe_erp form definition has two halves:
  10 +
  11 +- **JSON Schema** describes the **data shape**: which fields exist, their types, which are required, what their validation constraints are.
  12 +- **UI Schema** describes the **layout and widgets**: which field renders as a dropdown vs. a radio group, which fields are grouped under which tab, which fields are read-only, which are conditionally visible.
  13 +
  14 +The split matters. The JSON Schema is the contract — server-side validation, OpenAPI generation, and the AI-agent function catalog all read it. The UI Schema is presentation — only the renderer reads it. Two forms can share a JSON Schema but render differently.
  15 +
  16 +A form definition looks roughly like this (illustrative):
  17 +
  18 +```json
  19 +{
  20 + "id": "orders_sales.order.create",
  21 + "schema": {
  22 + "type": "object",
  23 + "required": ["customerId", "lines"],
  24 + "properties": {
  25 + "customerId": { "type": "string", "format": "uuid" },
  26 + "deliveryDate": { "type": "string", "format": "date" },
  27 + "lines": {
  28 + "type": "array",
  29 + "minItems": 1,
  30 + "items": {
  31 + "type": "object",
  32 + "required": ["itemId", "quantity"],
  33 + "properties": {
  34 + "itemId": { "type": "string", "format": "uuid" },
  35 + "quantity": { "type": "number", "exclusiveMinimum": 0 }
  36 + }
  37 + }
  38 + }
  39 + }
  40 + },
  41 + "uiSchema": {
  42 + "type": "VerticalLayout",
  43 + "elements": [
  44 + { "type": "Control", "scope": "#/properties/customerId", "options": { "widget": "partner-picker" } },
  45 + { "type": "Control", "scope": "#/properties/deliveryDate" },
  46 + { "type": "Control", "scope": "#/properties/lines", "options": { "widget": "line-items-grid" } }
  47 + ]
  48 + }
  49 +}
  50 +```
  51 +
  52 +## Two authoring paths
  53 +
  54 +Both paths are first-class. The renderer does not know which one produced a given form.
  55 +
  56 +### Tier 1 — Form designer in the web UI (v1.0)
  57 +
  58 +Business analysts build forms in the form designer that ships in the web SPA. The result is stored as a row in `metadata__form`, scoped to the tenant, tagged `source = 'user'` so it survives plug-in install/uninstall and core upgrades. No build, no restart.
  59 +
  60 +This path is the right one for:
  61 +
  62 +- Tweaking the layout of an existing entity for a specific tenant.
  63 +- Adding a tenant-specific custom field to an existing form.
  64 +- Building a form for a Tier 1 custom entity.
  65 +
  66 +The form designer is a **v1.0 deliverable**.
  67 +
  68 +### Tier 2 — JSON files in a plug-in JAR (v0.1)
  69 +
  70 +Plug-in authors ship form definitions inside their JAR under `src/main/resources/metadata/forms/`. The plug-in lifecycle upserts them into `metadata__form` on plug-in install, tagged `source = 'plugin:<id>'`.
  71 +
  72 +```
  73 +src/main/resources/
  74 +├── plugin.yml
  75 +└── metadata/
  76 + └── forms/
  77 + ├── plate_spec.json
  78 + └── job_card_review.json
  79 +```
  80 +
  81 +```yaml
  82 +# plugin.yml
  83 +metadata:
  84 + forms:
  85 + - metadata/forms/plate_spec.json
  86 + - metadata/forms/job_card_review.json
  87 +```
  88 +
  89 +This path is the right one for forms that ship as part of a vertical plug-in (the printing-shop plug-in is the canonical example).
  90 +
  91 +## Forms can reference custom fields
  92 +
  93 +A form can reference any custom field defined for the same entity by its key. Because custom fields live as JSON keys in the entity's `ext` JSONB column and are described by `metadata__custom_field` rows, the form designer and the form renderer both already know about them — no additional wiring required.
  94 +
  95 +The key is the same one the customer chose when creating the custom field. If a printing-shop integrator added a `coating_finish` custom field to the `production.work_order` entity, a form referencing `ext.coating_finish` will render and validate it correctly, whether the form was authored through the Tier 1 designer or shipped inside a plug-in JAR.
  96 +
  97 +## Validation runs on both sides
  98 +
  99 +Validation happens in two places, and both come from the same JSON Schema:
  100 +
  101 +- **Client-side** in the renderer, driven by the UI Schema's widgets and the JSON Schema constraints. This is the responsive, immediate-feedback layer.
  102 +- **Server-side** in the host, driven by the JSON Schema. This is the **authoritative** layer. The renderer's checks are a convenience; the server's checks are the contract.
  103 +
  104 +A form submission that bypasses the UI (a script, an integration, an AI agent calling through MCP) is validated against exactly the same JSON Schema as a form filled in through the SPA. There is no path that skips validation.
  105 +
  106 +## Where to go next
  107 +
  108 +- Workflows that render forms in user tasks: [`../workflow-authoring/guide.md`](../workflow-authoring/guide.md)
  109 +- Plug-in author walkthrough: [`../plugin-author/getting-started.md`](../plugin-author/getting-started.md)
  110 +- Plug-in API surface, including `api.v1.form`: [`../plugin-api/overview.md`](../plugin-api/overview.md)
... ...
docs/i18n/guide.md 0 → 100644
  1 +# i18n guide
  2 +
  3 +vibe_erp is built to be sold worldwide. There are no hard-coded user-facing strings, currencies, date formats, time zones, number formats, address shapes, or tax models in the framework — everything user-facing flows through the i18n and formatting layers from day one. This is guardrail #6 in `CLAUDE.md`, and it applies to the core, every PBC, and every plug-in.
  4 +
  5 +For the architectural placement of i18n, see [`../architecture/overview.md`](../architecture/overview.md) and the full spec at [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md).
  6 +
  7 +## ICU MessageFormat is the backbone
  8 +
  9 +vibe_erp uses **ICU MessageFormat** (via ICU4J) on top of Spring's `MessageSource`. ICU MessageFormat handles the things naive `printf`-style formatting cannot:
  10 +
  11 +- Plurals (`{count, plural, one {# item} other {# items}}`)
  12 +- Gender selection
  13 +- Locale-aware number, date, and currency formatting
  14 +- Nested arguments and select clauses
  15 +
  16 +A message in a bundle file looks like this:
  17 +
  18 +```properties
  19 +orders_sales.cart.summary = {count, plural, =0 {Your cart is empty.} one {# item, total {total, number, currency}} other {# items, total {total, number, currency}}}
  20 +```
  21 +
  22 +The same key produces correct output in English, German, Japanese, Chinese, and Spanish without changing a line of calling code.
  23 +
  24 +## Plug-ins ship message bundles
  25 +
  26 +Each plug-in (and each PBC) ships its message bundles inside its JAR under `i18n/<locale>.properties`. The locale tag follows BCP 47:
  27 +
  28 +```
  29 +src/main/resources/
  30 +└── i18n/
  31 + ├── en-US.properties
  32 + ├── zh-CN.properties
  33 + ├── de-DE.properties
  34 + ├── ja-JP.properties
  35 + └── es-ES.properties
  36 +```
  37 +
  38 +The `plugin.yml` manifest lists the bundles so the loader knows to pick them up:
  39 +
  40 +```yaml
  41 +metadata:
  42 + i18n:
  43 + - i18n/en-US.properties
  44 + - i18n/de-DE.properties
  45 +```
  46 +
  47 +## How the host merges bundles
  48 +
  49 +On boot, and on every plug-in install, the host builds a merged `MessageSource` per locale. The merge order, from lowest precedence to highest:
  50 +
  51 +1. **Core bundles** shipped inside the vibe_erp image.
  52 +2. **Plug-in bundles** picked up from `./plugins/*.jar` in plug-in load order. A plug-in may override a core key with the same key in its own bundle, but is not encouraged to.
  53 +3. **Tenant overrides** stored in `metadata__translation` rows tagged with the tenant id. These are entered through the Tier 1 customization UI and let an integrator rewrite any string for any tenant without rebuilding anything.
  54 +
  55 +```
  56 +tenant overrides ← highest precedence
  57 + plug-in bundles
  58 + core bundles ← lowest precedence
  59 +```
  60 +
  61 +The merged `MessageSource` is per-locale and per-tenant. Switching the user's locale or switching tenants picks the right merged bundle without restarting anything.
  62 +
  63 +## Shipping locales for v1.0
  64 +
  65 +vibe_erp v1.0 ships with five locales:
  66 +
  67 +| Locale tag | Language |
  68 +|---|---|
  69 +| `en-US` | English (United States) |
  70 +| `zh-CN` | Chinese (Simplified) |
  71 +| `de-DE` | German |
  72 +| `ja-JP` | Japanese |
  73 +| `es-ES` | Spanish (Spain) |
  74 +
  75 +Adding a sixth locale is a Tier 1 operation: drop a `<locale>.properties` file into `i18n-overrides/` on the mounted volume, or add `metadata__translation` rows through the customization UI. No rebuild.
  76 +
  77 +The reference business documentation under `raw/业务流程设计文档/` is in Chinese. **That does not make Chinese the default.** Chinese is one supported locale among the five.
  78 +
  79 +## Translation key naming
  80 +
  81 +Translation keys follow `<plugin-or-pbc>.<area>.<message>`:
  82 +
  83 +```
  84 +identity.login.title
  85 +identity.login.error.invalid_credentials
  86 +catalog.item.field.sku.label
  87 +orders_sales.order.status.confirmed
  88 +plugin_printingshop.plate.field.thickness.label
  89 +```
  90 +
  91 +Rules:
  92 +
  93 +- The first segment is the PBC table prefix (`identity`, `catalog`, `orders_sales`, …) or, for a plug-in, `plugin_<id>`. This is the same prefix used in the database, the metadata `source` column, and the OpenAPI tag.
  94 +- Segments are `snake_case`.
  95 +- The last segment names the message itself, not the widget that renders it.
  96 +- Two plug-ins can never collide: their first segment differs by definition.
  97 +
  98 +## The `Translator` is the only sanctioned way
  99 +
  100 +The host injects a `Translator` (from `org.vibeerp.api.v1.i18n`) into every plug-in endpoint, every workflow task handler, every event listener. **There is no string concatenation in user-facing code, anywhere, ever.**
  101 +
  102 +```kotlin
  103 +val message = translator.format(
  104 + MessageKey("orders_sales.order.created.notification"),
  105 + mapOf(
  106 + "orderNumber" to order.number,
  107 + "customerName" to order.customer.name,
  108 + "total" to order.total,
  109 + ),
  110 +)
  111 +```
  112 +
  113 +What this means in practice:
  114 +
  115 +- A reviewer who sees `"Order " + number + " was created"` in a PR rejects the PR.
  116 +- A reviewer who sees a `String.format` against a hard-coded format string rejects the PR.
  117 +- A reviewer who sees `if (locale == "en") "..." else "..."` rejects the PR with prejudice.
  118 +
  119 +The `Translator` is locale-aware: it picks up the request's resolved locale from the `LocaleProvider`, which in turn resolves from (in order) the user's profile preference, the request's `Accept-Language`, the tenant default, and the system default.
  120 +
  121 +## PII, dates, numbers, currency
  122 +
  123 +Locale handling does not stop at strings:
  124 +
  125 +- **Dates and times** are formatted through the locale, not through hard-coded patterns. The host exposes locale-aware formatters; plug-ins use them.
  126 +- **Numbers** use the locale's grouping and decimal separators.
  127 +- **Currency** values are `Money` (from `api.v1.core`) — an amount plus an ISO 4217 code — and the formatter renders them in the user's locale (e.g. `$1,234.56` vs. `1.234,56 €` vs. `¥1,235`).
  128 +- **Time zones** are per-tenant and per-user, not per-server.
  129 +- **PII fields** are tagged in the field metadata. Tagged fields drive auto-generated DSAR exports and erasure jobs (GDPR Articles 15/17), and the audit log records access to them when audit-strict mode is on.
  130 +- **Address shapes** are not assumed. There is no "state/province" or "ZIP code" hard-coded in the core; address fields are configurable via metadata so a Japanese address can have prefecture/city/ward and a French address can have arrondissement.
  131 +
  132 +## Where to go next
  133 +
  134 +- Plug-in author walkthrough that uses `Translator`: [`../plugin-author/getting-started.md`](../plugin-author/getting-started.md)
  135 +- Plug-in API surface for i18n: [`../plugin-api/overview.md`](../plugin-api/overview.md)
... ...
docs/index.md 0 → 100644
  1 +# vibe_erp documentation
  2 +
  3 +vibe_erp is an ERP/EBC framework for the printing industry, designed so customer-specific workflows are assembled by configuration and plug-ins instead of by forking the core. This index is the entry point to the framework's own documentation. The full architecture spec lives at [`superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](superpowers/specs/2026-04-07-vibe-erp-architecture-design.md) and is the source of truth for everything in this tree.
  4 +
  5 +## Guides
  6 +
  7 +| Guide | What it covers |
  8 +|---|---|
  9 +| [Architecture overview](architecture/overview.md) | Clean Core philosophy, two-tier extensibility, PBCs, the dependency rule, multi-tenancy, custom fields, the workflow engine. |
  10 +| [Plug-in API overview](plugin-api/overview.md) | What `api.v1` is, the package layout, the stability contract, the A/B/C/D extension grading. |
  11 +| [Plug-in author getting started](plugin-author/getting-started.md) | A concrete walkthrough for building, packaging, and loading your first vibe_erp plug-in. |
  12 +| [Workflow authoring guide](workflow-authoring/guide.md) | BPMN 2.0 workflows on embedded Flowable: visual designer (Tier 1) and `.bpmn` files in plug-ins (Tier 2). |
  13 +| [Form authoring guide](form-authoring/guide.md) | JSON Schema + UI Schema forms: visual designer (Tier 1) and JSON files in plug-ins (Tier 2). |
  14 +| [i18n guide](i18n/guide.md) | ICU MessageFormat, message bundles, locale resolution, tenant overrides, shipping locales. |
  15 +| [Customer onboarding guide](customer-onboarding/guide.md) | Integrator's checklist for standing up a new vibe_erp customer end-to-end. |
... ...
docs/plugin-api/overview.md 0 → 100644
  1 +# Plug-in API overview
  2 +
  3 +`org.vibeerp.api.v1` (`api.v1` for short) is the **only stable contract** in vibe_erp. It is the surface that PF4J plug-ins compile against, and the only surface they are permitted to touch. Everything else in the codebase — `platform.*`, every PBC's internal package, every concrete Spring bean — is internal and may change in any release.
  4 +
  5 +For the architectural reasoning behind `api.v1`, see [`../architecture/overview.md`](../architecture/overview.md) and the full spec at [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md).
  6 +
  7 +## What `api.v1` is
  8 +
  9 +- A Kotlin module published to **Maven Central** as `org.vibeerp:api-v1`.
  10 +- Depends on **only** Kotlin stdlib and `jakarta.validation`. No Spring, no Hibernate, no PF4J types leak through it.
  11 +- Semver-governed on the `1.x` line. Across a major version, `api.v1` and `api.v2` ship side by side for at least one major release window so plug-ins have time to migrate.
  12 +- The single import surface for plug-ins. The plug-in linter rejects any import outside `org.vibeerp.api.v1.*` at install time.
  13 +
  14 +## Package layout
  15 +
  16 +```
  17 +org.vibeerp.api.v1
  18 +├── core/ Tenant, Locale, Money, Quantity, Id<T>, Result<T,E>
  19 +├── entity/ Entity, Field, FieldType, EntityRegistry
  20 +├── persistence/ Repository<T>, Query, Page, Transaction
  21 +├── event/ DomainEvent, EventListener, EventBus
  22 +├── security/ Principal, Permission, PermissionCheck
  23 +├── i18n/ MessageKey, Translator, LocaleProvider
  24 +├── http/ @PluginEndpoint, RequestContext, ResponseBuilder
  25 +├── plugin/ Plugin, PluginManifest, ExtensionPoint
  26 +├── ext/ Typed extension interfaces a plug-in implements
  27 +├── workflow/ WorkflowTask, WorkflowEvent, TaskHandler
  28 +└── form/ FormSchema, UiSchema
  29 +```
  30 +
  31 +A short orientation:
  32 +
  33 +| Package | What it gives you |
  34 +|---|---|
  35 +| `core/` | The primitive value types every plug-in needs: `Tenant`, `Locale`, `Money`, `Quantity`, typed `Id<T>`, `Result<T,E>`. No printing concepts. |
  36 +| `entity/` | Declarative entity model. Plug-ins describe entities, fields, and field types through `EntityRegistry`; the platform handles persistence and OpenAPI. |
  37 +| `persistence/` | `Repository<T>`, `Query`, `Page`, `Transaction`. Plug-ins never see Hibernate or Spring `@Transactional` directly. |
  38 +| `event/` | `DomainEvent`, `EventListener`, `EventBus`. The primary cross-PBC and cross-plug-in communication channel. |
  39 +| `security/` | `Principal`, `Permission`, `PermissionCheck`. Plug-ins register their own permissions; the role editor auto-discovers them. |
  40 +| `i18n/` | `MessageKey`, `Translator`, `LocaleProvider`. The only sanctioned way for a plug-in to produce user-facing text. |
  41 +| `http/` | `@PluginEndpoint` and the request/response abstractions for adding REST endpoints from a plug-in. |
  42 +| `plugin/` | `Plugin`, `PluginManifest`, `ExtensionPoint`, and the `@Extension` annotation. The plug-in lifecycle entry points. |
  43 +| `ext/` | Typed extension interfaces that PBCs declare and plug-ins implement (e.g. `api.v1.ext.inventory.StockReservationStrategy`). The cross-PBC interaction surface. |
  44 +| `workflow/` | `WorkflowTask`, `WorkflowEvent`, `TaskHandler`. The hooks BPMN service tasks call into. |
  45 +| `form/` | `FormSchema`, `UiSchema`. JSON Schema and UI Schema as Kotlin types, for plug-ins shipping form definitions. |
  46 +
  47 +## The stability contract
  48 +
  49 +| Change | Allowed within 1.x? |
  50 +|---|---|
  51 +| Add a class to `api.v1` | yes |
  52 +| Add a method to an `api.v1` interface (with default impl) | yes |
  53 +| Remove or rename anything in `api.v1` | no — major bump |
  54 +| Change behavior of an `api.v1` symbol in a way plug-ins can observe | no — major bump |
  55 +| Anything in `platform.*` or `pbc.*.internal.*` | yes — that is why it is internal |
  56 +
  57 +Practical consequences for plug-in authors:
  58 +
  59 +- A plug-in built against `api.v1` version `1.4.0` will load in any vibe_erp `1.x` release.
  60 +- A symbol that gets deprecated in `1.x` keeps working until `2.0`. The plug-in loader emits a warning when a plug-in uses a deprecated symbol — that is the **Grade C** signal in the extension grading.
  61 +- A plug-in that reaches into `platform.*` or `pbc.*.internal.*` via reflection is **Grade D**, unsupported, and rejected by the plug-in linter at install time.
  62 +
  63 +When in doubt about whether something belongs in `api.v1`: **keep it out**. Growing the API deliberately later is much cheaper than maintaining a regretted symbol forever.
  64 +
  65 +## The A/B/C/D extension grading
  66 +
  67 +From CLAUDE.md guardrail #7. Every extension to vibe_erp falls into one of four grades, ordered from safest to least safe.
  68 +
  69 +| Grade | What it is | Upgrade safety |
  70 +|---|---|---|
  71 +| **A** | Tier 1 metadata only — custom fields, forms, workflows, rules, list views, translations entered through the web UI. Stored as rows in `metadata__*` tables, tagged `source = 'user'`. | Always upgrade-safe across **any** core version. |
  72 +| **B** | Tier 2 plug-in using only the public `api.v1` surface. | Safe within a major version. Loads cleanly across every `1.x` release. |
  73 +| **C** | Tier 2 plug-in using deprecated-but-supported `api.v1` symbols. | Safe until the next major. The plug-in loader emits warnings; the plug-in author should migrate before the next major release. |
  74 +| **D** | Tier 2 plug-in reaching into `platform.*` or `pbc.*.internal.*` via reflection. | UNSUPPORTED. The plug-in linter rejects this at install time. Will break on the next core upgrade. |
  75 +
  76 +Two principles follow from the grading:
  77 +
  78 +1. Anything a Tier 2 plug-in does should also become possible as a Tier 1 customization over time. Tier 2 is the escape hatch, not the default.
  79 +2. If you find yourself wanting to do Grade D, the seam is wrong and `api.v1` needs to grow — deliberately, with a version bump consideration.
  80 +
  81 +## Reference
  82 +
  83 +The full reference for every type, method, and contract in `api.v1` is generated from KDoc by **Dokka** and published alongside each release. This document is the conceptual overview; the generated reference is the authoritative per-symbol documentation. (The Dokka site is wired up as part of the v1.0 documentation deliverable; until then, the source under `api/api-v1/src/main/kotlin/org/vibeerp/api/v1/` is the source of truth, and every public type carries a KDoc block — that is part of the PR checklist in [`../../CONTRIBUTING.md`](../../CONTRIBUTING.md).)
  84 +
  85 +## Where to go next
  86 +
  87 +- Build your first plug-in: [`../plugin-author/getting-started.md`](../plugin-author/getting-started.md)
  88 +- Author a workflow: [`../workflow-authoring/guide.md`](../workflow-authoring/guide.md)
  89 +- Author a form: [`../form-authoring/guide.md`](../form-authoring/guide.md)
  90 +- Localize a plug-in: [`../i18n/guide.md`](../i18n/guide.md)
... ...
docs/plugin-author/getting-started.md 0 → 100644
  1 +# Plug-in author: getting started
  2 +
  3 +This walkthrough is for a developer building their first vibe_erp plug-in. By the end you will have a JAR that drops into `./plugins/`, registers an extension, exposes a REST endpoint, and shows up in the plug-in loader log on the next restart.
  4 +
  5 +For the conceptual overview of the plug-in API, read [`../plugin-api/overview.md`](../plugin-api/overview.md) first. For the architectural reasoning behind the constraints, see [`../architecture/overview.md`](../architecture/overview.md) and the full spec at [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md).
  6 +
  7 +## 1. The only dependency you need
  8 +
  9 +Add `org.vibeerp:api-v1` from Maven Central. **This is the only vibe_erp dependency a plug-in is allowed to declare.** Importing anything from `org.vibeerp.platform.*` or any PBC's internal package will fail the plug-in linter at install time.
  10 +
  11 +In Gradle (`build.gradle.kts`):
  12 +
  13 +```kotlin
  14 +plugins {
  15 + kotlin("jvm") version "2.0.0"
  16 +}
  17 +
  18 +repositories {
  19 + mavenCentral()
  20 +}
  21 +
  22 +dependencies {
  23 + compileOnly("org.vibeerp:api-v1:1.0.0")
  24 + // Test against the same artifact at runtime
  25 + testImplementation("org.vibeerp:api-v1:1.0.0")
  26 +}
  27 +```
  28 +
  29 +`compileOnly` is correct: at runtime the plug-in classloader is given `api.v1` from the host. Bundling it inside your plug-in JAR causes classloader confusion.
  30 +
  31 +## 2. Implement `org.vibeerp.api.v1.plugin.Plugin`
  32 +
  33 +The plug-in's entry class is the single point where the host hands you a context and asks you to wire yourself up.
  34 +
  35 +```kotlin
  36 +package com.example.helloprint
  37 +
  38 +import org.vibeerp.api.v1.plugin.Plugin
  39 +import org.vibeerp.api.v1.plugin.PluginContext
  40 +
  41 +class HelloPrintPlugin : Plugin {
  42 +
  43 + override fun start(context: PluginContext) {
  44 + context.log.info("hello-print plug-in starting")
  45 + // Register listeners, seed metadata, etc.
  46 + }
  47 +
  48 + override fun stop(context: PluginContext) {
  49 + context.log.info("hello-print plug-in stopping")
  50 + }
  51 +}
  52 +```
  53 +
  54 +The host calls `start` after the plug-in's classloader, Spring child context, and Liquibase migrations are ready. Anything the plug-in needs from the host (event bus, translator, repositories, configuration) comes through `PluginContext`.
  55 +
  56 +## 3. Write a `plugin.yml` manifest
  57 +
  58 +The manifest sits at the root of the JAR (`src/main/resources/plugin.yml`). It tells the host who you are, what API version you target, and what permissions you need.
  59 +
  60 +```yaml
  61 +id: com.example.helloprint
  62 +version: 0.1.0
  63 +requiresApi: "1.x"
  64 +name: Hello Print
  65 +entryClass: com.example.helloprint.HelloPrintPlugin
  66 +
  67 +description: >
  68 + A minimal worked example for the vibe_erp plug-in author guide.
  69 + Registers one extension and exposes one REST endpoint.
  70 +
  71 +vendor:
  72 + name: Example Print Co.
  73 + url: https://example.com
  74 +
  75 +permissions:
  76 + - hello_print.greet.read
  77 +
  78 +metadata:
  79 + forms:
  80 + - metadata/forms/greeting.json
  81 + i18n:
  82 + - i18n/en-US.properties
  83 + - i18n/de-DE.properties
  84 +```
  85 +
  86 +Field notes:
  87 +
  88 +- `id` is globally unique. Use a reverse-DNS style. The host uses it as the plug-in's schema namespace (`plugin_helloprint__*`) and as the `source` tag (`plugin:com.example.helloprint`) on every metadata row the plug-in seeds.
  89 +- `version` is your plug-in's version, not the host's.
  90 +- `requiresApi: "1.x"` means "any 1.x release of `api.v1`". A mismatch fails at install time, not at runtime. Across a major version, the host loads `api.v1` and `api.v2` side by side for at least one major release window.
  91 +- `permissions` are auto-registered with the role editor. Plug-ins should not invent permissions outside their own namespace.
  92 +- `metadata` lists files inside the JAR that the host should pick up on plug-in start: form definitions, BPMN files, message bundles, seed rules.
  93 +
  94 +## 4. Register an extension via `@Extension`
  95 +
  96 +Extensions are how a plug-in plugs into a typed seam declared by the core or another PBC. Every seam lives in `org.vibeerp.api.v1.ext.<area>`.
  97 +
  98 +```kotlin
  99 +package com.example.helloprint
  100 +
  101 +import org.vibeerp.api.v1.plugin.Extension
  102 +import org.vibeerp.api.v1.workflow.TaskHandler
  103 +import org.vibeerp.api.v1.workflow.WorkflowTask
  104 +
  105 +@Extension(point = TaskHandler::class)
  106 +class GreetCustomerHandler : TaskHandler {
  107 +
  108 + override val id: String = "hello_print.greet_customer"
  109 +
  110 + override fun handle(task: WorkflowTask) {
  111 + val name = task.variable<String>("customerName") ?: "world"
  112 + task.setVariable("greeting", "hello, $name")
  113 + task.complete()
  114 + }
  115 +}
  116 +```
  117 +
  118 +The host scans `@Extension`-annotated classes inside the plug-in JAR and registers them against the declared extension point. A BPMN service task referencing `hello_print.greet_customer` will now be routed to this handler.
  119 +
  120 +## 5. Add a `@PluginEndpoint` controller
  121 +
  122 +Plug-ins add REST endpoints through `@PluginEndpoint`. The endpoint runs inside the plug-in's Spring child context and is auto-listed in the OpenAPI document under the plug-in's namespace.
  123 +
  124 +```kotlin
  125 +package com.example.helloprint
  126 +
  127 +import org.vibeerp.api.v1.http.PluginEndpoint
  128 +import org.vibeerp.api.v1.http.RequestContext
  129 +import org.vibeerp.api.v1.http.ResponseBuilder
  130 +import org.vibeerp.api.v1.i18n.MessageKey
  131 +import org.vibeerp.api.v1.security.Permission
  132 +
  133 +@PluginEndpoint(
  134 + path = "/api/plugin/hello-print/greet",
  135 + method = "GET",
  136 + permission = "hello_print.greet.read",
  137 +)
  138 +class GreetEndpoint {
  139 +
  140 + fun handle(request: RequestContext): ResponseBuilder {
  141 + val name = request.query("name") ?: "world"
  142 + val greeting = request.translator.format(
  143 + MessageKey("hello_print.greet.message"),
  144 + mapOf("name" to name),
  145 + )
  146 + return ResponseBuilder.ok(mapOf("greeting" to greeting))
  147 + }
  148 +}
  149 +```
  150 +
  151 +A few things this snippet is doing on purpose:
  152 +
  153 +- The endpoint declares a `permission`. The plug-in loader registers `hello_print.greet.read` with the role editor; an admin grants it before the endpoint becomes callable.
  154 +- The greeting goes through the `Translator`. There is **no** string concatenation in user-facing code. See the [i18n guide](../i18n/guide.md).
  155 +- The handler returns through `ResponseBuilder`, not through Spring's `ResponseEntity`. Plug-ins do not see Spring directly.
  156 +
  157 +## 6. Loading the plug-in
  158 +
  159 +Build the JAR and drop it into the host's plug-in directory:
  160 +
  161 +```bash
  162 +./gradlew :jar
  163 +cp build/libs/hello-print-0.1.0.jar /opt/vibe-erp/plugins/
  164 +# or, for a Docker deployment
  165 +docker cp build/libs/hello-print-0.1.0.jar vibe-erp:/opt/vibe-erp/plugins/
  166 +```
  167 +
  168 +Restart the host:
  169 +
  170 +```bash
  171 +docker restart vibe-erp
  172 +```
  173 +
  174 +Hot reload of plug-ins without a restart is not in v1.0 — it is on the v1.2+ roadmap. For v1.0 and v1.1, install and uninstall require a restart.
  175 +
  176 +On boot, the host scans `./plugins/`, validates each manifest, runs the plug-in linter, creates a classloader and Spring child context per plug-in, runs the plug-in's Liquibase changesets in `plugin_<id>__*`, seeds metadata, and finally calls `Plugin.start`. The lifecycle in full is described in section 7 of [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md).
  177 +
  178 +## 7. Where to find logs and errors
  179 +
  180 +- **Boot log:** `/opt/vibe-erp/logs/vibe-erp.log`. Filter on the `platform-plugins` logger to see exactly which plug-ins were scanned, accepted, rejected, and why.
  181 +- **Linter rejections:** if your JAR imports a class outside `org.vibeerp.api.v1.*`, the linter rejects it at install time and logs the offending import. Fix the import; never work around the linter.
  182 +- **Migration failures:** Liquibase output is logged under the `platform-persistence` logger, scoped to your plug-in id.
  183 +- **Runtime errors inside your plug-in:** logged under the plug-in's id (`com.example.helloprint`). Use the `PluginContext.log` handed to you in `start`; do not pull in your own logging framework.
  184 +- **Health endpoint:** `/actuator/health` reports per-plug-in status. A plug-in stuck in a half-loaded state shows up here before it shows up in user-visible failures.
  185 +
  186 +## 8. The reference printing-shop plug-in is your worked example
  187 +
  188 +`reference-customer/plugin-printing-shop/` is a real, production-shaped plug-in built and CI-tested on every PR. It expresses the workflows in `raw/业务流程设计文档/` using **only `api.v1`**. When this guide leaves a question unanswered, the answer is almost always "look at how `plugin-printing-shop` does it":
  189 +
  190 +- The shape of `plugin.yml` for a non-trivial plug-in.
  191 +- How a plug-in declares a brand-new entity (printing plates, presses, color proofs) without touching any PBC.
  192 +- How a multi-step BPMN workflow is shipped inside a JAR and wired up to typed `TaskHandler` implementations.
  193 +- How form definitions reference custom fields.
  194 +- How i18n bundles are organized for a plug-in that ships in five locales.
  195 +
  196 +The plug-in is built by the main Gradle build but **not loaded by default**. Drop its JAR into `./plugins/` to load it locally.
  197 +
  198 +## What this guide does not yet cover
  199 +
  200 +- Publishing a plug-in to a marketplace — the marketplace and signed plug-ins are a v2 deliverable.
  201 +- Hot reload without restart — v1.2+.
  202 +- Calling the MCP endpoint as an AI agent — v1.1.
  203 +
  204 +For everything else, the source of truth is [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md).
... ...
docs/superpowers/specs/2026-04-07-vibe-erp-implementation-plan.md 0 → 100644
  1 +# vibe_erp — Implementation Plan (post-v0.1)
  2 +
  3 +**Status:** Approved roadmap. v0.1 skeleton is built. This document is the source of truth for the work that turns v0.1 into v1.0.
  4 +**Date:** 2026-04-07
  5 +**Companion document:** [Architecture spec](2026-04-07-vibe-erp-architecture-design.md)
  6 +
  7 +---
  8 +
  9 +## What v0.1 (this commit) actually delivers
  10 +
  11 +A buildable, runnable, tested skeleton of the vibe_erp framework. Concretely:
  12 +
  13 +| Item | Status |
  14 +|---|---|
  15 +| Gradle multi-project with 7 subprojects | ✅ Builds |
  16 +| `api/api-v1` — public plug-in contract (~30 files, semver-governed) | ✅ Compiles, unit-tested |
  17 +| `platform/platform-bootstrap` — Spring Boot main, config, tenant filter | ✅ Compiles |
  18 +| `platform/platform-persistence` — multi-tenant JPA scaffolding, audit base | ✅ Compiles |
  19 +| `platform/platform-plugins` — PF4J host, lifecycle | ✅ Compiles |
  20 +| `pbc/pbc-identity` — User entity end-to-end (entity → repo → service → REST) | ✅ Compiles, unit-tested |
  21 +| `reference-customer/plugin-printing-shop` — hello-world PF4J plug-in | ✅ Compiles, packs into a real plug-in JAR with PF4J manifest |
  22 +| `distribution` — Spring Boot fat jar (`vibe-erp.jar`, ~59 MB) | ✅ `bootJar` succeeds |
  23 +| Liquibase changelogs (platform init + identity init) | ✅ Schema-valid, runs against Postgres on first boot |
  24 +| Dockerfile (multi-stage), docker-compose.yaml (app + Postgres), Makefile | ✅ Present, image build untested in this session |
  25 +| GitHub Actions workflows (build + architecture-rule check) | ✅ Present, untested in CI |
  26 +| Architecture-rule enforcement in `build.gradle.kts` | ✅ Active — fails the build if a PBC depends on another PBC, or if a plug-in depends on `platform/*` |
  27 +| README, CONTRIBUTING, LICENSE, 8 docs guides | ✅ Present |
  28 +| Architecture spec (this folder) | ✅ Present |
  29 +| Implementation plan (this file) | ✅ Present |
  30 +
  31 +**What v0.1 explicitly does NOT deliver:**
  32 +
  33 +- The 9 other core PBCs (catalog, partners, inventory, warehousing, orders-sales, orders-purchase, production, quality, finance)
  34 +- The metadata store machinery (custom-field application, form designer, list views, rules engine)
  35 +- Embedded Flowable workflow engine
  36 +- ICU4J `Translator` implementation
  37 +- Postgres Row-Level Security `SET LOCAL` hook (the policies exist; the hook that sets `vibeerp.current_tenant` per transaction is deferred)
  38 +- JasperReports
  39 +- OIDC integration
  40 +- React web SPA
  41 +- Plug-in linter (rejecting reflection into platform internals at install time)
  42 +- Plug-in Liquibase application
  43 +- Per-plug-in Spring child context
  44 +- MCP server
  45 +- Mobile app (v2 anyway)
  46 +
  47 +The architecture **accommodates** all of these — the seams are in `api.v1` already — they just aren't implemented.
  48 +
  49 +---
  50 +
  51 +## How to read this plan
  52 +
  53 +Each work unit is sized to be a single coherent implementation pass: roughly 1–5 days of work, one developer, one PR. Units are grouped by theme, ordered roughly by dependency, but parallelizable wherever the prerequisites are satisfied.
  54 +
  55 +Each unit names:
  56 +- the **Gradle modules** it touches (or creates)
  57 +- the **api.v1 surface** it consumes (and never modifies, unless explicitly noted)
  58 +- the **acceptance test** that proves it landed correctly
  59 +- any **upstream dependencies** that must complete first
  60 +
  61 +The 11 architecture guardrails in `CLAUDE.md` and the 14-section design in `2026-04-07-vibe-erp-architecture-design.md` apply to every unit. This document does not restate them.
  62 +
  63 +---
  64 +
  65 +## Phase 1 — Platform completion (the foundation everything else needs)
  66 +
  67 +These units finish the platform layer so PBCs can be implemented without inventing scaffolding.
  68 +
  69 +### P1.1 — Postgres RLS transaction hook
  70 +**Module:** `platform-persistence`
  71 +**Why:** v0.1 enables RLS policies but never sets `current_setting('vibeerp.current_tenant')`. Without the hook, RLS-enabled tables return zero rows. **Block on this for any read/write integration test.**
  72 +**What:** A Hibernate `StatementInspector` (or a Spring `TransactionSynchronization`) that runs `SET LOCAL vibeerp.current_tenant = '<tenant_id>'` at the start of every transaction, reading from `TenantContext`.
  73 +**Acceptance:** integration test with Testcontainers Postgres, two tenants, one user per tenant, asserts that tenant A's queries return zero rows from tenant B's data even when the Hibernate filter is bypassed.
  74 +
  75 +### P1.2 — Plug-in linter
  76 +**Module:** `platform-plugins`
  77 +**Depends on:** —
  78 +**What:** At plug-in load time, scan the JAR's class files for any reference to `org.vibeerp.platform.*` or `org.vibeerp.pbc.*.internal.*` packages. If any are found, refuse to load and report a "grade D extension" error to the operator. Reuse ASM (already a transitive dep of Spring) for bytecode inspection.
  79 +**Acceptance:** unit test with a hand-crafted bad JAR that imports a fictitious `org.vibeerp.platform.foo.Bar` — the loader rejects it with a clear message.
  80 +
  81 +### P1.3 — Per-plug-in Spring child context
  82 +**Module:** `platform-plugins`
  83 +**Depends on:** P1.2 (linter must run first)
  84 +**What:** When a plug-in starts, create a Spring `AnnotationConfigApplicationContext` with the host context as parent. Register the plug-in's `@Component` classes (discovered via classpath scanning of its classloader). The plug-in's `Plugin.start(context: PluginContext)` is invoked with a `PluginContext` whose dependencies are wired from the parent context.
  85 +**Acceptance:** the reference printing-shop plug-in declares an `@Extension` and a `@PluginEndpoint` controller; the integration test confirms the endpoint is mounted under `/api/v1/plugins/printing-shop/...` and the extension is invocable.
  86 +
  87 +### P1.4 — Plug-in Liquibase application
  88 +**Module:** `platform-plugins` + `platform-persistence`
  89 +**Depends on:** P1.3
  90 +**What:** When a plug-in starts, look for `db/changelog/master.xml` in its JAR; if present, run it through Liquibase against the host's datasource with the plug-in's id as the changelog `contexts` filter. Tables created live under the `plugin_<id>__*` prefix (enforced by lint of the changelog at load time, not runtime).
  91 +**Acceptance:** a test plug-in ships a changelog that creates `plugin_test__widget`; after load, the table exists. Uninstall removes the table after operator confirmation.
  92 +
  93 +### P1.5 — Metadata store seeding
  94 +**Module:** new `platform-metadata` module (or expand `platform-persistence`)
  95 +**Depends on:** —
  96 +**What:** A `MetadataLoader` Spring component that, on boot and on plug-in start, upserts rows into `metadata__entity`, `metadata__form`, `metadata__workflow`, etc. from YAML files in the classpath / plug-in JAR. Each row carries `source = 'core' | 'plugin:<id>' | 'user'`. Idempotent; safe to run on every boot.
  97 +**Acceptance:** integration test confirms that booting twice doesn't duplicate rows; uninstalling a plug-in removes its `source = 'plugin:<id>'` rows; user-edited rows (`source = 'user'`) are never touched.
  98 +
  99 +### P1.6 — `Translator` implementation backed by ICU4J
  100 +**Module:** new `platform-i18n` module
  101 +**Depends on:** P1.5 (so plug-in message bundles can be loaded as part of metadata)
  102 +**What:** Implement `org.vibeerp.api.v1.i18n.Translator` using ICU4J `MessageFormat` with locale fallback. Resolve message bundles in this order: `metadata__translation` (tenant overrides) → plug-in `i18n/messages_<locale>.properties` → core `i18n/messages_<locale>.properties` → fallback locale.
  103 +**Acceptance:** unit tests covering plurals, gender, number formatting in en-US, zh-CN, de-DE, ja-JP, es-ES; a test that a tenant override beats a plug-in default beats a core default.
  104 +
  105 +### P1.7 — Event bus + outbox
  106 +**Module:** new `platform-events` module
  107 +**Depends on:** —
  108 +**What:** Implement `org.vibeerp.api.v1.event.EventBus` with two parts: (a) an in-process Spring `ApplicationEventPublisher` for synchronous delivery to listeners in the same process; (b) an `event_outbox` table written in the same DB transaction as the originating change, scanned by a background poller, marked dispatched after delivery. The outbox is the seam where Kafka/NATS plugs in later.
  109 +**Acceptance:** integration test that an OrderCreated event survives a process crash between the original transaction commit and a downstream listener — the listener fires when the process restarts.
  110 +
  111 +### P1.8 — JasperReports integration
  112 +**Module:** new `platform-reporting` module
  113 +**Depends on:** P1.5
  114 +**What:** Wrap JasperReports compilation/rendering behind a `ReportRenderer` Spring component. Reports live in `metadata__report` (path or content). API exposes `/api/v1/_meta/reports/{id}.pdf?param=...` for synchronous rendering, with a job-based async path for big reports.
  115 +**Acceptance:** ship one core report (a User list) and confirm it renders to PDF with the correct locale-aware date and number formatting.
  116 +
  117 +### P1.9 — File store (local + S3)
  118 +**Module:** new `platform-files` module
  119 +**Depends on:** —
  120 +**What:** A `FileStore` interface with `LocalFileStore` and `S3FileStore` implementations, configured via `vibeerp.files.backend`. Each stored file has a `tenant_id` namespace baked into its key, so a misconfigured S3 bucket can't accidentally serve cross-tenant files.
  121 +**Acceptance:** unit tests for both backends; integration test with MinIO Testcontainer for S3.
  122 +
  123 +### P1.10 — Job scheduler
  124 +**Module:** new `platform-jobs` module
  125 +**Depends on:** —
  126 +**What:** Quartz integration with the `JDBCJobStore` against the same Postgres. PBCs and plug-ins register jobs through a typed `JobScheduler` API in api.v1 (already has the seam, may need a tiny addition). Each job runs inside a `TenantContext.runAs(tenantId) { ... }` block — never inheriting a stale tenant from the trigger thread.
  127 +**Acceptance:** a recurring core job that prunes the audit log; integration test confirms the job runs in the right tenant context.
  128 +
  129 +---
  130 +
  131 +## Phase 2 — Embedded workflow engine
  132 +
  133 +### P2.1 — Embedded Flowable
  134 +**Module:** new `platform-workflow` module
  135 +**Depends on:** P1.5 (metadata), P1.6 (i18n for task names), P1.7 (events)
  136 +**What:** Pull in `flowable-spring-boot-starter`. Configure Flowable to use the same Postgres datasource (so its tables live alongside ours, prefixed `flowable_*`). Wire `org.vibeerp.api.v1.workflow.TaskHandler` implementations registered by plug-ins as Flowable `JavaDelegate`s. Workflows are deployed from `metadata__workflow` rows (BPMN XML payload) on boot and on plug-in load.
  137 +**Acceptance:** the reference printing-shop plug-in registers a "quote-to-job-card" `TaskHandler`; a BPMN process that calls it can be started via REST and returns the expected output.
  138 +
  139 +### P2.2 — BPMN designer integration (web)
  140 +**Module:** the future React SPA
  141 +**Depends on:** P2.1, R1 (web bootstrap)
  142 +**What:** Embed `bpmn-js` (Camunda's BPMN modeler) in a "Workflow Designer" page. Save as a `metadata__workflow` row; deploy through the Tier 1 path.
  143 +**Acceptance:** end-to-end test where a Tier 1 user creates a workflow in the browser and starts an instance of it.
  144 +
  145 +### P2.3 — User task / form rendering
  146 +**Module:** `platform-workflow` + future React SPA
  147 +**Depends on:** P2.1, F1 (form designer), R1
  148 +**What:** When a Flowable user task fires, look up the form id from the task definition, render it via the form renderer (P3.x), capture the user's input, and complete the task.
  149 +**Acceptance:** end-to-end test of an approval workflow where a human clicks a button.
  150 +
  151 +---
  152 +
  153 +## Phase 3 — Metadata store: forms and rules (the Tier 1 user journeys)
  154 +
  155 +### P3.1 — JSON Schema form renderer (server)
  156 +**Module:** new `platform-forms` module
  157 +**Depends on:** P1.5
  158 +**What:** A `FormRenderer` that, given an entity instance and a `FormSchema`, returns a server-side JSON describing what to render. The actual rendering happens in the SPA (P3.2). The server-side piece is needed for non-browser clients (CLI, integration tests, AI agents).
  159 +**Acceptance:** snapshot tests of the rendered JSON for a User form with an added custom field.
  160 +
  161 +### P3.2 — Form renderer (web)
  162 +**Module:** future React SPA
  163 +**Depends on:** P3.1, R1
  164 +**What:** A React component that consumes JSON Schema + UI Schema and renders an interactive form. Use `react-jsonschema-form` as the base, customize the widget set for vibe_erp's design language.
  165 +**Acceptance:** Storybook for every widget type; e2e test that filling and submitting a form persists the data.
  166 +
  167 +### P3.3 — Form designer (web)
  168 +**Module:** future React SPA
  169 +**Depends on:** P3.2
  170 +**What:** A drag-and-drop designer that produces JSON Schema + UI Schema and saves into `metadata__form`. This is the Tier 1 entry point for "add a field, change a layout".
  171 +**Acceptance:** e2e test where a Tier 1 user adds a "Customer PO Reference" field to the Sales Order form without writing code.
  172 +
  173 +### P3.4 — Custom field application
  174 +**Module:** `platform-metadata`
  175 +**Depends on:** P1.5
  176 +**What:** A Hibernate interceptor (or JPA listener) that reads the entity's `metadata__custom_field` rows on save and validates the JSON in the `ext` column against them — required, type, max length, etc. Validation errors come back as 422 with a per-field map.
  177 +**Acceptance:** integration test that a User with a missing required custom field cannot be saved; one with a valid one round-trips.
  178 +
  179 +### P3.5 — Rules engine (simple "if X then Y")
  180 +**Module:** new `platform-rules` module
  181 +**Depends on:** P1.7 (events)
  182 +**What:** Listen to `DomainEvent`s, evaluate `metadata__rule` rows scoped to that event type, run the configured action (send email, set field, call workflow, publish another event). Use a simple expression language (something like `JEXL` or Spring SpEL — bias toward Spring SpEL since we already have it).
  183 +**Acceptance:** integration test where a rule "on UserCreated, set ext.greeting = 'Welcome'" actually fires.
  184 +
  185 +### P3.6 — List view designer (web)
  186 +**Module:** future React SPA
  187 +**Depends on:** R1
  188 +**What:** A grid component backed by `metadata__list_view` rows. Tier 1 users pick columns, filters, sort, save as a named list view, share with their tenant.
  189 +**Acceptance:** e2e test where a user creates a "Active customers in Germany" list view and re-opens it later.
  190 +
  191 +---
  192 +
  193 +## Phase 4 — Authentication and authorization (real)
  194 +
  195 +### P4.1 — Built-in JWT auth
  196 +**Module:** new `pbc-auth` (under `pbc/`) + `platform-security` (might already exist)
  197 +**Depends on:** —
  198 +**What:** Username/password login backed by `identity__user_credential` (separate table from `identity__user` so the User entity stays small). Argon2id for hashing. Issue a signed JWT with `sub`, `tenant`, `roles`, `iat`, `exp`. Validate on every request via a Spring Security filter.
  199 +**Acceptance:** integration test of full login/logout/refresh flow; assertion that the JWT carries `tenant` and that downstream PBC code sees it via `Principal`.
  200 +
  201 +### P4.2 — OIDC integration
  202 +**Module:** `pbc-auth`
  203 +**Depends on:** P4.1
  204 +**What:** Plug Spring Security's OIDC client into the same `Principal` resolution. Configurable per tenant: which provider, which client id, which claim maps to `tenant`. Keycloak is the smoke-test target.
  205 +**Acceptance:** Keycloak Testcontainer + integration test of a full OIDC code flow; the same `UserController` endpoints work without changes.
  206 +
  207 +### P4.3 — Permission checking (real)
  208 +**Module:** `platform-security`
  209 +**Depends on:** P4.1
  210 +**What:** Implement `org.vibeerp.api.v1.security.PermissionCheck`. Resolve permissions from `metadata__role_permission` rows for the current `Principal`'s roles. Plug-ins register their own permissions via `@Extension(point = PermissionRegistrar::class)` (api.v1 may need a tiny addition here — deliberate, with a minor version bump).
  211 +**Acceptance:** integration test that a user without `identity.user.create` gets 403 from `POST /api/v1/identity/users`; with the role, 201.
  212 +
  213 +---
  214 +
  215 +## Phase 5 — Core PBCs (the meat of the framework)
  216 +
  217 +Each PBC follows the same 7-step recipe:
  218 +
  219 +1. Create the Gradle subproject under `pbc/`
  220 +2. Domain entities (extending `AuditedJpaEntity`, with `ext jsonb`)
  221 +3. Spring Data JPA repositories
  222 +4. Application services
  223 +5. REST controllers under `/api/v1/<pbc>/<resource>`
  224 +6. Liquibase changelog (`db/changelog/<pbc>/`) with rollback blocks AND RLS policies
  225 +7. Cross-PBC facade in `api.v1.ext.<pbc>` + adapter in the PBC
  226 +
  227 +### P5.1 — `pbc-catalog` — items, units of measure, attributes
  228 +Foundation for everything that's bought, sold, made, or stored. Hold off on price lists and configurable products until P5.10.
  229 +
  230 +### P5.2 — `pbc-partners` — customers, suppliers, contacts
  231 +Companies, addresses, contacts, contact channels. PII-tagged from day one (CLAUDE.md guardrail #6 / DSAR).
  232 +
  233 +### P5.3 — `pbc-inventory` — stock items, lots, locations, movements
  234 +Movements as immutable events; on-hand quantity is a projection. Locations are hierarchical.
  235 +
  236 +### P5.4 — `pbc-warehousing` — receipts, picks, transfers, counts
  237 +Built on top of `inventory` via `api.v1.ext.inventory`. The first PBC that proves the cross-PBC dependency rule under load.
  238 +
  239 +### P5.5 — `pbc-orders-sales` — quotes, sales orders, deliveries
  240 +Workflow-heavy. The reference printing-shop plug-in's quote-to-job-card flow exercises this.
  241 +
  242 +### P5.6 — `pbc-orders-purchase` — RFQs, POs, receipts
  243 +Mirror image of sales. Shares the document/approval pattern.
  244 +
  245 +### P5.7 — `pbc-production` (basic) — work orders, routings, operations
  246 +Generic discrete-manufacturing primitives. NOT printing-specific.
  247 +
  248 +### P5.8 — `pbc-quality` (basic) — inspection plans, results, holds
  249 +Generic; the printing-specific QC steps live in the plug-in.
  250 +
  251 +### P5.9 — `pbc-finance` (basic) — GL, journal entries, AR/AP minimal
  252 +Basic double-entry. Tax engines, multi-currency revaluation, consolidation are v1.2+.
  253 +
  254 +### P5.10 — `pbc-catalog` — pricing, configurable products
  255 +Defer until P5.5 (orders-sales) is real, since pricing only matters when orders exist.
  256 +
  257 +---
  258 +
  259 +## Phase 6 — Web SPA
  260 +
  261 +### R1 — React + TypeScript bootstrap
  262 +**Module:** new `web/` directory (Vite + React + TS)
  263 +**Depends on:** P4.1 (need a way to log in)
  264 +**What:** Vite scaffold, design system (likely Mantine or Radix), routing (React Router), data fetching (TanStack Query), generated TypeScript client from the backend's OpenAPI spec.
  265 +**Acceptance:** can log in, see a list of users, log out.
  266 +
  267 +### R2 — Identity screens
  268 +**Depends on:** R1
  269 +**What:** User list, user detail, role list, role detail, permission editor.
  270 +
  271 +### R3 — Customize / metadata UIs
  272 +**Depends on:** R1, P3.3, P3.6
  273 +**What:** The Tier 1 entry point. Form designer, list view designer, custom field designer, workflow designer.
  274 +
  275 +### R4 — One screen per core PBC
  276 +**Depends on:** R1, the matching PBC from Phase 5
  277 +**What:** Per-PBC list / detail / create / edit screens. Generated from metadata wherever possible; hand-built where the metadata-driven approach falls short (and those gaps become future enhancements to the metadata store).
  278 +
  279 +---
  280 +
  281 +## Phase 7 — Reference plug-in (the executable acceptance test)
  282 +
  283 +### REF.1 — Real workflow handler
  284 +**Module:** `reference-customer/plugin-printing-shop`
  285 +**Depends on:** P2.1 (Flowable)
  286 +**What:** Replace the v0.1 hello-world with a real `TaskHandler` that converts a Quote into a JobCard, registered as `acme.printingshop.quoteToJobCard`. A BPMN file in the plug-in JAR drives a workflow that calls this handler.
  287 +**Acceptance:** integration test starts the workflow with a real Quote and asserts the JobCard appears.
  288 +
  289 +### REF.2 — Plate / ink / press domain
  290 +**Module:** same plug-in
  291 +**Depends on:** P1.4 (plug-in Liquibase application)
  292 +**What:** The plug-in defines its own entities (`plugin_printingshop__plate`, `plugin_printingshop__ink_recipe`, `plugin_printingshop__press`) via its own Liquibase changelog. It registers REST endpoints for them via `@PluginEndpoint`. It registers permissions like `printingshop.plate.approve`.
  293 +**Acceptance:** integration test that a printing-shop user can CRUD plates against `/api/v1/plugins/printing-shop/plates`, scoped to their tenant.
  294 +
  295 +### REF.3 — Real reference forms and metadata
  296 +**Module:** same plug-in
  297 +**Depends on:** P3.3, P3.4
  298 +**What:** The plug-in ships YAML metadata for: a custom field on Sales Order ("printing job type"), a custom form for plate approval, an automation rule "on plate approved, dispatch to press". All loaded at plug-in start, all tagged `source = 'plugin:printing-shop'`.
  299 +**Acceptance:** end-to-end smoke test that loads the plug-in into a fresh tenant and exercises the full quote-to-job-card-to-press flow without any code changes outside the plug-in.
  300 +
  301 +---
  302 +
  303 +## Phase 8 — Hosted, AI agents, mobile (post-v1.0)
  304 +
  305 +### H1 — Per-region tenant routing (hosted)
  306 +Make `platform-persistence` look up the tenant's region from `identity__tenant` and route the connection accordingly. v1.0 doesn't ship hosted, but the routing seam should land in v1.0 so v2.0 can flip a switch.
  307 +
  308 +### H2 — Tenant provisioning UI
  309 +A per-instance "operator console" that creates tenants, manages quotas, watches health. Separate React app, separate URL.
  310 +
  311 +### A1 — MCP server
  312 +**Module:** new `platform-mcp`
  313 +**Depends on:** every PBC, every endpoint having a typed OpenAPI definition
  314 +**What:** Expose every REST endpoint as an MCP function. Permission-checked, locale-aware, tenant-aware. A Tier 2 plug-in can register additional MCP functions via a new `@McpFunction` annotation in api.v1 (an additive change, minor version bump).
  315 +**Acceptance:** an LLM-driven agent can call `vibe_erp.identity.users.create` and the result is identical to a REST call.
  316 +
  317 +### M1 — React Native skeleton
  318 +Shared TypeScript types with the web SPA; first screens cover shop-floor scanning and approvals.
  319 +
  320 +---
  321 +
  322 +## Tracking and ordering
  323 +
  324 +A coarse dependency view:
  325 +
  326 +```
  327 +P1.1 — RLS hook ─────┐
  328 +P1.2 — linter ──┐ │
  329 +P1.3 — child ctx ┘ │
  330 +P1.4 — plug-in liquibase ─ depends on P1.3
  331 +P1.5 — metadata seed ──┐ ──┐
  332 +P1.6 — translator ──── (depends on P1.5)
  333 +P1.7 — events ─────────┘ │
  334 +P1.8 — reports ── (depends on P1.5)
  335 +P1.9 — files
  336 +P1.10 — jobs
  337 +P2.1 — Flowable ── (depends on P1.5, P1.6, P1.7)
  338 +P3.x — metadata Tier 1 ── (depends on P1.5)
  339 +P4.x — auth
  340 +P5.x — core PBCs ── (depend on P1.x complete + P4.x for permission checks)
  341 +R1 — web bootstrap ── (depends on P4.1)
  342 +REF.x — reference plug-in ── (depends on P1.4 + P2.1 + P3.3)
  343 +```
  344 +
  345 +Sensible ordering for one developer:
  346 +P1.1 → P1.5 → P1.7 → P1.6 → P1.10 → P1.4 → P1.3 → P1.2 → P2.1 → P3.4 → P3.1 → P4.1 → P5.1 → P5.2 → ...
  347 +
  348 +Sensible parallel ordering for a team of three:
  349 +- Dev A: P1.1, P1.5, P1.7, P1.6, P2.1
  350 +- Dev B: P1.4, P1.3, P1.2, P3.4, P3.1
  351 +- Dev C: P4.1, P4.3, P5.1 (catalog), P5.2 (partners)
  352 +
  353 +Then converge on R1 (web) and the rest of Phase 5 in parallel.
  354 +
  355 +---
  356 +
  357 +## Definition of "v1.0 ready"
  358 +
  359 +v1.0 ships when, on a fresh Postgres, an operator can:
  360 +
  361 +1. `docker run` the image
  362 +2. Log in to the web SPA
  363 +3. Create a tenant (or use the default tenant in self-host mode)
  364 +4. Drop the printing-shop plug-in JAR into `./plugins/`
  365 +5. Restart the container
  366 +6. See the printing-shop's screens, custom fields, workflows, and permissions in the SPA
  367 +7. Walk a quote through the printing shop's full workflow without writing any code
  368 +8. Generate a PDF report for that quote in zh-CN
  369 +9. Export a DSAR for one of their customers
  370 +
  371 +…and the same image, against a fresh empty Postgres, also serves a customer who has loaded a *completely different* plug-in expressing a *completely different* business — without any change to the core image.
  372 +
  373 +That's the bar. Until that bar is met, the framework has not delivered on its premise.
... ...
docs/workflow-authoring/guide.md 0 → 100644
  1 +# Workflow authoring guide
  2 +
  3 +Workflows in vibe_erp are **data, not code**. State machines, approval chains, and document routing are declarative, so a new customer is onboarded by editing definitions instead of editing source. This is guardrail #2 in `CLAUDE.md`, and it shapes everything below.
  4 +
  5 +For the architectural placement of the workflow engine, see [`../architecture/overview.md`](../architecture/overview.md). For the full reasoning, see [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md).
  6 +
  7 +## BPMN 2.0 on embedded Flowable
  8 +
  9 +vibe_erp embeds the **Flowable** engine and uses **BPMN 2.0** as the workflow language. BPMN is a standard, and the standard is the contract: there is no vibe_erp-specific workflow DSL to learn or to invent. Anything a BPMN 2.0 modeling tool can produce, vibe_erp can run.
  10 +
  11 +Flowable runs in-process inside the Spring Boot host. Its tables (`flowable_*`) live in the same Postgres database as the core PBCs and are untouched by vibe_erp.
  12 +
  13 +## Two authoring paths
  14 +
  15 +Both paths are first-class. Tier 1 is preferred whenever it is expressive enough; Tier 2 is the escape hatch.
  16 +
  17 +### Tier 1 — Visual designer in the web UI (v1.0)
  18 +
  19 +Business analysts draw workflows in the BPMN designer that ships in the web SPA. The result is stored as a row in `metadata__workflow`, deployed to Flowable, and tagged `source = 'user'` so it survives plug-in install/uninstall and core upgrades. No build, no restart, no plug-in.
  20 +
  21 +This path is the right one for:
  22 +
  23 +- Approval chains that vary per tenant.
  24 +- Document routing rules that the customer wants to tune themselves.
  25 +- Workflows that compose existing typed task handlers in a new order.
  26 +
  27 +The visual designer is a **v1.0 deliverable**; v0.1 ships the underlying API only.
  28 +
  29 +### Tier 2 — `.bpmn` files in a plug-in JAR (v0.1)
  30 +
  31 +Plug-in authors ship `.bpmn` files inside their JAR under `src/main/resources/workflow/`. The plug-in lifecycle deploys them to Flowable on plug-in install. The `plugin.yml` manifest lists them so the loader picks them up.
  32 +
  33 +```
  34 +src/main/resources/
  35 +├── plugin.yml
  36 +└── workflow/
  37 + ├── quote_to_job_card.bpmn
  38 + └── reprint_request.bpmn
  39 +```
  40 +
  41 +```yaml
  42 +# plugin.yml
  43 +metadata:
  44 + workflows:
  45 + - workflow/quote_to_job_card.bpmn
  46 + - workflow/reprint_request.bpmn
  47 +```
  48 +
  49 +This path is the right one for:
  50 +
  51 +- Workflows that need new typed task handlers shipped alongside them.
  52 +- Workflows that the plug-in author wants under version control with the rest of the plug-in code.
  53 +- Workflows that ship as part of a vertical-specific plug-in (the printing-shop plug-in is the canonical example).
  54 +
  55 +## Service tasks call typed `TaskHandler` implementations
  56 +
  57 +A BPMN service task in vibe_erp does not embed scripting code. It references a typed `TaskHandler` by id, and the host routes the call to the matching implementation registered by a plug-in.
  58 +
  59 +The plug-in registers a handler:
  60 +
  61 +```kotlin
  62 +@Extension(point = TaskHandler::class)
  63 +class ReserveStockHandler : TaskHandler {
  64 +
  65 + override val id: String = "printing.reserve_stock"
  66 +
  67 + override fun handle(task: WorkflowTask) {
  68 + val itemId = task.variable<String>("itemId")
  69 + ?: error("itemId is required")
  70 + val quantity = task.variable<Int>("quantity") ?: 0
  71 +
  72 + // ... call into the plug-in's services here ...
  73 +
  74 + task.setVariable("reservationId", reservationId)
  75 + task.complete()
  76 + }
  77 +}
  78 +```
  79 +
  80 +The BPMN service task references the handler by its id (`printing.reserve_stock`). The host validates at deploy time that every referenced handler id exists and rejects the deployment otherwise — broken workflows fail at install, not at runtime.
  81 +
  82 +There is no scripting language to invent. The `TaskHandler` is just Kotlin code behind a typed interface, with the same testability, debuggability, and review surface as the rest of the codebase.
  83 +
  84 +## User tasks render forms from metadata
  85 +
  86 +A BPMN user task references a form definition by id. The host looks the id up in `metadata__form` and renders the form using the same code path as Tier 1 forms — there is no parallel form renderer for workflows.
  87 +
  88 +This means:
  89 +
  90 +- The same form can be used inside a workflow user task and outside a workflow.
  91 +- A custom field added through Tier 1 customization automatically appears on every workflow user task that uses the same form.
  92 +- Validation runs in exactly the same place whether the form is rendered inside a workflow or not.
  93 +
  94 +For details on how forms work, see the [form authoring guide](../form-authoring/guide.md).
  95 +
  96 +## Worked example: quote-to-job-card
  97 +
  98 +The reference printing-shop plug-in ships a `quote_to_job_card.bpmn` workflow that exercises every concept in this guide. In rough shape:
  99 +
  100 +1. **Start event:** a sales clerk creates a quote (user task; uses a form from `metadata__form`).
  101 +2. **Service task `printing.price_quote`:** calls a `TaskHandler` registered by the plug-in to compute the price from the customer's price list and the quote's line items.
  102 +3. **Exclusive gateway:** routes on whether the quote total exceeds the customer's pre-approved limit.
  103 +4. **User task (manager approval):** rendered from a form definition; the manager can approve, reject, or send back for revision.
  104 +5. **Service task `printing.reserve_stock`:** another `TaskHandler` that reserves raw materials.
  105 +6. **Service task `printing.create_job_card`:** materializes the approved quote as a production job card (a custom entity defined by the plug-in).
  106 +7. **End event:** publishes a `JobCardCreated` `DomainEvent` so other PBCs and plug-ins can react without coupling.
  107 +
  108 +What is worth noticing:
  109 +
  110 +- Every printing-specific concept — *quote*, *price list*, *job card*, *reserve stock for plates and inks* — lives in the **plug-in**, never in core PBCs. The core only knows about generic documents, workflows, forms, and events.
  111 +- The cross-PBC interaction in step 7 goes through a `DomainEvent`, not a direct call. PBCs never import each other.
  112 +- Steps 4 and 5 can be reordered, removed, or duplicated through the BPMN designer in the web UI without touching plug-in code, because the typed handlers are decoupled from the workflow shape.
  113 +
  114 +## Where to go next
  115 +
  116 +- Plug-in author walkthrough including a `TaskHandler` example: [`../plugin-author/getting-started.md`](../plugin-author/getting-started.md)
  117 +- Form authoring (the other half of any workflow that has user tasks): [`../form-authoring/guide.md`](../form-authoring/guide.md)
  118 +- Plug-in API surface, including `api.v1.workflow`: [`../plugin-api/overview.md`](../plugin-api/overview.md)
... ...