---
name: generated-code-extend
description: Extending Designer-generated code — the whole `{BC}/Model/{Agg}/` tree is Generator-owned and hermetic (ForceOverwrite, never edit). The BC facade's public surface exposes `{agg}(): {Agg}Read` (read-only queries + lists), `process()`, and a direct method per command door at `domain` or wider (stance rows of `Doors.json`); the write facade `{Agg}` (Commands + `event()`) is family-internal via the protected kernel seam (`$this->handle()`). Hand code stands only at the places of the one table in §10: process node bodies under `{BC}/Process/{Name}/Command/Handler/Action/`, Closure/Rule bodies under `{BC}/Closure/` (own-BC-read-only, M9), Domain Services under `{BC}/Service/`, and `App/**` delivery code without business logic. Covers computed fields (`Set{Entity}Computed`, family-internal), the aggregate entity with child getters at the Rule `aggregate` input, cross-BC reads as a derived Cross-BC-Call Service, V1–V13 prohibitions, decision tree, generated-file modes, DomainResponse construction incl. `RuleViolation` 422. ClassVersion → `generated-code-versioning`. Workflow API → `generated-code-workflow-api`. Recipes → `generated-code-recipes`.
zone: post-active
persona: C
profile: jardis
prerequisites: []
next: [generated-code-versioning, generated-code-workflow-api, generated-code-recipes, generated-code-wire-transport]
---

### 0. DDD positioning

Jardis uses DDD vocabulary (BC, Aggregate, Domain Event, Repository, VO, Ubiquitous Language) under its own axioms — Closure-Orchestrator (`foundation-architecture` §3) and model-driven generation. See `foundation-architecture` § Positioning for the Functional-DDD stance this skill operates under.

| Rule | Consequence |
|---|---|
| **V11** — no business methods on entities | Behaviour lives in Actions and Domain Services, not on entities. Aggregate invariants are protected by the generated **pipeline**; cross-aggregate behaviour is authored as a **Process** (`{BC}/Process/`). |
| **V12** — responses are `DomainResponse` envelopes with an array payload | One serialisation surface across HTTP / CLI / queue. Typing is the transport adapter's job, not the domain's. The `data` array is flat at the root; inside a read answer the children are typed `{Child}Response` objects (`->`), which `json_encode` serialises like arrays. The generated `{Agg}/Command/Response/{Command}Response.php` classes do **not** change this: they are contract types describing the payload, never a handler return type. |
| **Call chains** | Aggregate reads (public surface): `$bc->{agg}()->get{Agg}ById\|ByIds\|By{Key}\|By{PluralKey}\|{agg}List(...)` (an aggregate WITH a public unique key carries `By{Key}`/`By{PluralKey}` and NO `ById`/`ByIds` on `{Agg}Read` — those two stay family-internal via the kernel seam; one without a key carries `ById`/`ByIds`) — the `{Agg}Read` facade is the call surface, no aggregate-root hop; an aggregate with no read door at `domain` or wider (every read row of its block `closed` in `Doors.json`) has no `{agg}()`/`{Agg}Read` at all and is reached only family-internally. Aggregate writes are **not** on the BC facade (G2) — the write facade `{Agg}` is reachable only family-internally, via the kernel seam: `$this->handle({Agg}::class)->{command}($dto)` (typically from a Process node body). BC-level processes (the other half of the public surface): `$bc->process()->{process}($dto)`. A command door at stance `domain` or wider (row of `Doors.json`) is a third entry of the public surface: `$bc->{command}($dto)` (incl. `$bc->create{Agg}($dto)`; Rules-Layer G10). A column declared `computed` in `Aggregate.json` is in no Create/Set/Add/Update command or request body; its one write path is the family-internal `$this->handle({Agg}::class)->set{Entity}Computed($dto)` (raises its own `{Agg}[{Fragment}]ComputedUpdated` event like every command, no outer door — `generated-code-recipes` Recipe 13). Taxonomy is discoverable; `handle()` / ClassVersion resolution anchors at each level. |

Upheld without compromise: aggregates are transaction boundaries with a single root; BCs are strict language boundaries (V6). The aggregate is **100 % generated** — you do not extend it in place. A Rich-Model instinct (custom validation, a VO, a domain service, coordinating two aggregates) is satisfied by **modelling a Process** in the Process Designer, never by editing the aggregate tree.

When Domain logic needs a Jardis runtime tool (cache, mail, queue, repository, HTTP client, …), look up the responsible package skill in §8 (Toolbox) before re-deriving the API.

### 1. Generated baseline — Platform-free hermetic layout

Facade chain: Domain → BC → **Aggregate Read facade** (`{Agg}Read`, reached via `$bc->{agg}()`) → query method — this is the BC facade's public surface, plus a parallel Domain → BC → Process facade → process chain (`$bc->process()`) and one direct method per command door at `domain` or wider (`$bc->{command}($dto)`, delegating into the command handler). The **Aggregate Write facade** (`{Agg}`, same directory) hosts the commands + `event()`; it sits behind the kernel seam (`handle()`/`context()`) and is reachable only from inside the Context family (classes extending the Domain Context) — never from the BC facade. Example: aggregate `Counter` in BC `Counter` in domain `MeterDevice`.

**Route convention of the public surface.** Every build also writes a machine-readable description of this same surface — one OpenAPI 3.1 document per domain at `Api/{Domain}/openapi.yaml`, a sibling of `App/` outside the PSR-4 tree. The routes it declares are the canonical HTTP shape of the public surface, and a transport layer (hand-written or generated) is expected to mount exactly these:

| Command / entry | Route |
|---|---|
| `Update{Agg}` | `PUT /{bc}/{aggs}/{identifier}` *(full replacement of the root scalars; `PATCH` stays free for a future partial-update concept)* |
| `Remove{Agg}` | `DELETE /{bc}/{aggs}/{identifier}` |
| `Add{Agg}{Child}` | `POST /{bc}/{aggs}/{identifier}/{children}` |
| `Remove{Agg}{Child}` | `DELETE /{bc}/{aggs}/{identifier}/{children}/{childKey}` |
| `Set{Something}` | `PUT /{bc}/{aggs}/{identifier}/{something}` |
| Process | `POST /{bc}/{name}` *(lcfirst of the process name — `processes/` no longer appears in the path; `operationId` is unchanged, still `{bc}_processes_{name}`)* |

**Exposing child commands.** Exposure is not limited to root commands: every aggregate command — root (`Update{Agg}`, `Remove{Agg}`) and child (`Add{Child}`, `Set{Entity}`, `Update{Entity}`, `Remove{Child}`) — opened on the BC facade by the stance of its row in `Doors.json` (block = the aggregate, row = the class name of the command, `closed` by default): at `domain` or wider it is the facade door `{lcfirst(Command)}({Cmd}DTO)` (e.g. `addGoodsReceiptLine()` on the generated `Purchasing` facade), at `outside` also a route and a right. A command door at `domain` or wider without a bound Rule only raises the warning `V-RULE-5` (its message says that process rules do not apply on this path). The key `bindings.{cmd}.expose` of `Closures.json` is retired (`V-DOOR-0`).

**Create is a door like every other.** The row is `Create{Agg}` (one name in every form: facade method `create{Agg}()`, handler `Create{Agg}`, guard `GuardCreate{Agg}`, right `{bc}.{agg}:create{Agg}`). At `domain` or wider it is the facade door `{BC}::create{Agg}({Agg}DTO)`; at `outside` it also has the route `POST /{bc}/{aggs}` (answer 201) and a right. A method name that collides with another facade method of the BC (also with `{agg}()`) is `V-QDEF-23` (blocker). A creation that needs more than the aggregate's own Create still runs through a Process — model one, open its row, and it appears as `POST /{bc}/{name}`.

**`Remove{Agg}{Child}` is not emitted for every child.** The table above gives the route *shape*; whether the command exists at all depends on the child. Two independent rules suppress the remove mutation of an `erm:one` child — and when either applies, there is **no** `Aggregate/Action/Remove{Child}.php`, **no** AggregateHandler method, **no** command and therefore **no** route:

| Case | Why |
|---|---|
| **DEPEND leaf whose depend-FK is NOT NULL** | removing it would null a mandatory column — the database would reject it |
| **`required` containment child** | there is no valid removed state |

A depend leaf whose FKs are all nullable keeps its remove mutation. `Remove{Agg}` (the root) is always emitted and is unaffected by this rule. So: if you expect a `remove{Child}()` on the write facade and it is missing, check the depend-FK's nullability first — that absence is the rule working, not a defect. Model the removal as a Process if the business really needs it (which then also has to say what replaces the mandatory reference).

Path parameters carry the **business identifier** wherever the aggregate or child has one; the internal id is a documented fallback only, and the build emits a warning when it has to fall back, so the gap stays visible. A door appears in the document only at the stance `outside` or wider (`Doors.json`; `domain` is facade only, `closed` nothing) — changing a stance moves it in and out of the document without touching a line of PHP. The document carries no `servers:` block: base URL and mount prefix are the surrounding app's business.

**`openapi.yaml` has a sibling artefact:** `Api/{Domain}/routes.php`, a hermetic `jardiscore/app` route-registration closure built from the same route table — how a transport layer wires it, decoding/dispatch/envelope details, and its `Api/{Domain}/` neighbourhood: `generated-code-wire-transport` §4 "Generated route registration".

**Notation:** aggregate code lives under `{BC}/Model/{Agg}/`; this skill abbreviates that directory as `{Agg}/`. Processes live one level up, under `{BC}/Process/` — a sibling of `{BC}/Model/`, **not** under any aggregate.

**The one rule:** The **entire** `{Agg}/` tree (`{BC}/Model/{Agg}/…`) is regenerated on every build (ForceOverwrite per file) — never edit anything inside it. (The collector segment is named `Model/`, not `Aggregate/`, renamed 2026-09-23; a BC-wide build cleans up a pre-existing `{BC}/Aggregate/` tree left over from before this rename, and dev-code imports of the old `…\Aggregate\{Agg}\…` namespace are rewritten to `…\Model\{Agg}\…` automatically by the merge.) There is **no `Platform/` segment**, in neither path nor namespace; the directory `{Agg}/` *is* the hermetic generator output. The directory hosts **two** facades: the write facade `{Agg}/{Agg}.php` (Commands + `event()`) and the read facade `{Agg}/{Agg}Read.php` (`get{Agg}ById`/`ByIds`/`By{Key}` + lists). Only the read facade is reached via the BC accessor `$bc->{agg}()` (the public surface, G2); the write facade is reached exclusively family-internally via the kernel seam, `$this->handle({Agg}::class)` — the same `protected` `handle()`/`context()` corridor every generated class in the Context family shares (code outside the Context family cannot call either method; PHP visibility enforces it at the type-system level, not just by convention). Hand code stands only at the places of the one table in §10 — never in the aggregate tree. The Generator emits the command-path segment: `Command/{Name}.php` DTO (`ForceOverwrite`), `Command/Handler/{Name}Handler.php` orchestrator (`ForceOverwrite`), `Command/Handler/Action/<NodeClass>.php` node stubs (`CreateIfNotExists` + `@node-id` body-preserve for custom and sub-process nodes), and event-data classes under `Event/` (`ForceOverwrite`, hermetic). Custom-node stubs are `CreateIfNotExists` with `@node-id` body-preserve, where your process logic lives. A versioned variant exists only for a stub class (node, Closure/Rule, Domain Service): it lives in a per-class `v{N}/` subdir resolved by ClassVersion (see `generated-code-versioning`); a hermetic class has none.

```
src/                                              ← namespace root = <Domain>
├── <Domain>.php                                  ← Domain facade — Generated, ForceOverwrite
│                                                    (carries the classVersion() override → LoadClassFromSubDirectory)
├── <Domain>Context.php                           ← generated hermetic Context base (implements GeneratedContextInterface, no package base class) — Generated, ForceOverwrite
├── Config/
│   ├── .env
│   ├── .env.database, .env.database.{dev,test}
│   └── .env.logger, .env.logger.{dev,test}
└── <BC>/
    ├── <BC>.php                                  ← BC facade — Generated, ForceOverwrite
    │                                                (exposes ->{agg}() and ->process(), plus one
    │                                                 direct method per command door at `domain` or wider
    │                                                 — Rules-Layer G10)
    ├── Closure/                                   ← ★ HAND-CODE PLACE — BC-level Closure catalog (sibling of Process/)
    │   ├── <RuleName>.php                        ← Closure stub, DeveloperOwned (tag RuleClass)
    │   │                                            __invoke(...) — 0..n typed parameters per the
    │   │                                            catalog's declared `input[]`, returns the judgement
    │   │                                            `RuleResult` or a plain scalar per `output` (own BC
    │   │                                            only via the kernel seam, M9) ← your logic
    │   ├── Guard/Guard<Command>.php               ← hermetic AND-chain closure per bound endpoint
    │   │                                            — Generated, ForceOverwrite
    │   └── v{N}/<RuleName>.php                   ← versioned Closure override, ClassVersion-resolved
    ├── Service/                                  ← ★ HAND-CODE PLACE — Domain Services (sibling of Closure/ and Process/), the one location
    │   ├── <ServiceName>.php                     ← Domain Service stub, body yours (own BC read-only; a foreign BC only through its doors, §10)
    │   └── v{N}/<ServiceName>.php                ← versioned Service override, ClassVersion-resolved
    ├── Process/                                  ← BC-level Processes (siblings of Model/); hand code only in the ★ Action/ node bodies
    │   ├── <BC>Process.php                       ← Process facade — Generated, ForceOverwrite, HERMETIC
    │   │                                            (thin dispatch, one method per process, no body merge)
    │   └── <Name>/                               ← one process
    │       ├── Command/                          ← Generator-owned (ForceOverwrite, hermetic)
    │       │   ├── <Name>.php                    ← Input DTO
    │       │   └── Handler/
    │       │       ├── <Name>Handler.php         ← the door; the graph is its definition()
    │       │       │                                (regenerated, not edited)
    │       │       ├── <Name>Context.php         ← typed run view, one getter per node
    │       │       ├── <Name>Validator.php       ← input gate (only when the input carries a rule)
    │       │       ├── <Name><Value>.php         ← object value of a `gives` with fields
    │       │       └── Action/<NodeClass>.php    ← Node stubs, one file per node
    │       │                                        (custom: CreateIfNotExists + @node-id body-preserve;
    │       │                                         sub-process: CreateIfNotExists + @node-id body-preserve,
    │       │                                         typed Dev-Stub — see generated-code-recipes §2 Recipe 8) ← your logic
    │       └── Event/<EventClass>.php            ← Event-data classes (Generator, ForceOverwrite)
    ├── FieldMap.php                              ← ★ BC-LEVEL FieldMap — Generated, ForceOverwrite
    │                                                (ONE per BC, namespace <Domain>\<BC>; one {table}Columns() method per BC table, all BC tables)
    ├── Data/<ValueListName>.php                  ← value-list enum, one per entry of the BC's ValueLists.json — Generated, ForceOverwrite
    ├── Entity/                                   ← ★ BC-LEVEL schema layer — Generated, ForceOverwrite
    │   ├── <Entity>.php                          ←   persistence entity (#[Table]/#[Column]), one per BC table
    │   └── Validation/<Entity>Validator.php      ←   field-level validator, one per BC table
    └── Model/<Agg>/                              ← ★ GENERATED, NEVER EDIT (whole tree ForceOverwrite)
        ├── <Agg>.php                             ← WRITE facade (extends <Domain>Context), hosts inline
        │                                           Command methods + event(); family-internal only
        │                                           ($this->handle(<Agg>::class) — no BC-facade accessor)
        ├── <Agg>Read.php                         ← READ facade (extends <Domain>Context), hosts inline
        │                                           Query/list methods; reached via $bc->{agg}() (the public surface)
        ├── Aggregate/{<Agg>.php, Entity/<Entity>.php, Action/{Set,Add,Remove}<Entity>.php}   ← DOMAIN aggregate entity (#[Aggregate]/#[Relation]) — distinct from the BC-root persistence Entity above
        ├── Command/{<DTO>.php}  +  Handler/{Create<Agg>,{Set,Add,Remove}<Entity>}.php  +  Handler/Action/{Build*Data,HydrateCreate<Agg>Entities}.php  +  Validation/{<DTO>Validator,Validate<Action>}.php
        ├── Query/{<Agg>ById,<Agg>ByIds,<Agg>By<Key>,<Agg>By<PluralKey>,<Agg>By<Custom>…,<Agg>ListFilter}.php  +  Handler/Get<Agg>{ById,ByIds,By<Key>,By<PluralKey>,By<Custom>…,List}Handler.php  (`By<PluralKey>` only with a unique key)
        ├── Event/{<Agg>{Created,Removed,Updated,…}.php, <Agg>EventRouter.php, <Agg>Events.php}
        └── Repository/{<Agg>Repository,Query<Agg>{ById,ByIds,By<Key>,By<PluralKey>,…},Transform<Agg>,Validate<Agg>,Persist<Agg>,Query<Agg>Additions,Query<Agg>List}.php
```

> **Note:** there is no `{Agg}/Api/` directory and no `Platform/` directory. The aggregate use-case API surface is split across two sibling facades in the same directory: the **write** facade `{Agg}/{Agg}.php` (FQN `<Domain>\<BC>\Model\<Agg>\<Agg>`, Commands + `event()`, family-internal via the kernel seam) and the **read** facade `{Agg}/{Agg}Read.php` (FQN `…\<Agg>\<Agg>Read`, Queries/lists, reached directly via `$bc->{agg}()` — the public surface). When BC and aggregate share a name, the BC facade imports the read facade under an alias `{Agg}AggregateRead` (e.g. `CounterAggregateRead`) to avoid a class-name collision with the BC's own class name; the accessor itself stays `{agg}()` either way (same pattern the write facade already used: `{Agg}Aggregate`). The Events registry lives under `{Agg}/Event/{Agg}Events.php`. **Processes** are BC-level (`{BC}/Process/{Name}/`, sibling of `{BC}/Model/`), reached via `$bc->process()->{process}()` through the hermetic `{BC}Process` facade. The process follows the same segment convention as the aggregate: Generator emits `Command/` (DTO + `Handler/` + `Handler/Action/`) and `Event/`; hand code stands only in the node bodies of `Handler/Action/` (§10).

**Vocabulary glossary** (binding for this skill set — the canonical term table; sibling skills point here):

| Term | Meaning |
|---|---|
| **Aggregate facade** (write) | `{Agg}/{Agg}.php` — Commands + `event()`; the aggregate is the write model (DDD). Family-internally reachable via the kernel seam, not via the BC facade. |
| **Read facade** `{Agg}Read` | `{Agg}/{Agg}Read.php` — one method per read door at `domain` or wider (`Doors.json`): `get{Agg}ById`/`ByIds` for an aggregate WITHOUT a public unique key, `By{Key}`/`By{PluralKey}` (no `ById`/`ByIds`) for one WITH it; `{agg}List` only if `Queries.json` declares it; declared queries of the root entity at `domain` or wider. Incl. `$version`, reachable via `$bc->{agg}()`. |
| **Public surface** | The public methods of the BC facade: `{agg}(): {Agg}Read` per aggregate + `process(): {BC}Process` + one direct method entry per **command door at `domain` or wider** (incl. `create{Agg}()`; Rules-Layer G10). No general write accessor. |
| **Kernel seam** | `handle()` / `context()` on `<Domain>Context` (the generated, hermetic base-class body — no package base class) — the family corridor through which all generated classes reach one another (incl. the aggregate write facade). `protected` — code outside the Context family cannot call `handle()`/`context()`; PHP visibility enforces it at the type-system level. |
| **Context family** / family-internal | Classes that extend `<Domain>Context` (aggregate facades, Process handlers, node actions, services, Rules, …) — they share the kernel seam among themselves. Code outside the family (transport layer, controllers) has no `handle()` access and must go through the public surface. |
| **Closure** | `{BC}/Closure/{Name}.php` — a `Closures.json` catalog entry (DeveloperOwned body), reusable across aggregates. Its signature follows the entry's declared contract: **0..n** named, typed `input[]` parameters (`command`/`aggregate`/`scalar`/`valuelist`, each optionally `optional`/a list) and one `output` — either the judgement type **verdict** (`RuleResult`) or a plain scalar from the closed column-type vocabulary. A **Rule** is simply the verdict-output case — in DDD vocabulary a **Specification**: input a subject, output `passed` or `rejected(reason)`. Callable exclusively via `$this->handle({Closure}::class)` (ClassVersion-capable, `Closure/v{N}/`) — never `new`. **Guard-bindable** (usable in `bindings.{Command}.chain`) requires additionally at most one `command` input **and** at most one `aggregate` input, no `scalar`/`valuelist` input (V-RULE-11) — the single-input `__invoke({Cmd}DTO): RuleResult` shape used throughout this section is exactly that bindable case, not the only signature the catalog allows; a freely composed Closure (e.g. a calculation with several scalar inputs) is equally valid but only reachable via `uses`, never via a Guard chain. Reads existing data only from its **own** BC (M9 prohibition: no foreign-BC import) — via its own read facade for facade-routed queries, or directly via the kernel seam (`$this->context({Handler}::class, $payload)()`) for a BC-internal read — the declared list with `limit: 1`, decided over `total` (see the Rule stub row below). |
| **`aggregate` input** | Carries the **aggregate entity** (`{Agg}\Model\{Agg}\Aggregate\Entity\{Agg}`) — the root plus its children through typed getters `get{Kind}()` (a list for `erm: many`) — never the read-facade DTO. The same type is in the stub parameter, the Guard `$root` and the Rule-node loader, so a Rule checks children it already holds instead of declaring a helper query (`reads`) for data that is loaded anyway; a body that uses only root getters runs unchanged. An example's `given.root` (for an input named `root`) carries the root fields plus the children under their relation names (nested objects / lists; V-RULE-22 reports an unknown key), and `ClosureTestCase::aggregate()` builds the entity from it; the work package lists the child getters in `types[].children`. At a Guard chain (a Command endpoint), the generated CommandHandler hands in the root it already loaded (`$handler->getData()`) — the Closure never loads it itself. A Create-Command has no root yet, so an `aggregate` input cannot bind to a Create-Command's chain (V-RULE-11). A Rule-node in a Process loads the root itself, through a generated, hermetic `{Agg}/Repository/Load{Agg}ForRule.php` closure — the same Query+Repository path the CommandHandler uses, never the read facade. When that lookup finds nothing, the node rejects (`NodeResult::reject`) with `RuleViolation` (422), a fixed `{lcfirst(bc)}.{lcfirst(agg)}.notFound` messageKey, `context: {identifier}` and no `rule` key (the rejection did not come from any catalog Closure's predicate). |
| **Guard** | `{BC}/Closure/Guard/Guard{Command}.php` (for the Create command `GuardCreate{Agg}.php`; a `Guard{Agg}.php` of an older build stays orphaned — delete it by hand) — generated, hermetic Chain-of-Responsibility Closure per bound endpoint: runs the configured Rule chain as AND with short-circuit. The generated CommandHandler only calls `$this->handle(Guard{Command}::class)($cmd)` — no Guard logic of its own. |
| **RuleViolation (422)** | Rejection status of a Rule (requires `jardiscore/kernel` ≥ v1.1.0) — payload `{rule, messageKey, context}`, stable across Rule versions. When the catalog entry declares a `messageKey`, the 422 carries exactly that key (set as a literal by the generated Guard and Process Rule node) — the `messageKey` passed to `RuleResult::reject()` is then not taken over; without a catalog key the body's key passes through. No exception path; the handler builds the 422 response directly. A Command door declares the `messageKey` of its 422 as a closed enum of the catalog keys of all Rules that can reject it (Rule-Sets resolved), as does a Process door while only Rules with catalog keys are reachable; the 422 stays open (plain `RuleViolation`, no enum) as soon as a reachable Rule has no catalog key or a dev body of the door's own BC is reachable, or a cross-BC call, external call or custom node with a foreign-BC call is reachable next to the Rules (its service body can answer 422 with its own key); a cross-BC or external call alone declares none. |

The seam is `protected` — hard at runtime **and** at the type-system level: code outside the Context family cannot call `handle()`/`context()` (PHP visibility error on a foreign call).

| Class | Path | Mode | Purpose |
|---|---|---|---|
| Domain Facade | `<Domain>.php` | Generator: ForceOverwrite | `final class`, no `extends` (JardisCore-free). Constructor takes the DomainKernel `DomainKernelInterface $kernel`; self-registers every aggregate's event router via `$kernel->eventListenerRegistry()` (nullable-safe, unconditional); each BC accessor `new`s the BC facade directly (`new <BC>($this->kernel)`) — the Domain facade itself sits outside the Context family. The `classVersion()`/`classVersionConfig()` hooks live on the generated `<Domain>Context` (next row) — see `generated-code-versioning` |
| FieldMap (BC-level) | `{BC}/FieldMap.php` | Generator: ForceOverwrite | **ONE FieldMap per BC** (namespace `<Domain>\<BC>`), built from the whole BC schema — one `{table}Columns()` write-map method per BC table (each BC table appears exactly once; shared tables collapse losslessly across the BC's aggregates). **NOT aggregate capsule:** BC-rooted and BC-complete, not emitted per aggregate. The read path (root-id normalisation) lives at the aggregate read edge (query handler), not in the FieldMap. |
| Config | `Config/.env*` | Generator: CreateIfNotExists | Per-domain ENV defaults — kept on rerun |
| Persistence Entity (BC-level) | `{BC}/Entity/<Entity>.php` | Generator: ForceOverwrite | Schema-driven persistence entity (`#[Table]`, `#[Column]`, `__snapshot`), namespace `<Domain>\<BC>\Entity`, one per BC table. **Distinct** from the DOMAIN aggregate entity (`#[Aggregate]`/`#[Relation]`) under `{Agg}/Aggregate/Entity/<Entity>.php` — the schema-driven layer (Entity/Validation/Data) lives on the BC root, the domain aggregate entity stays in the aggregate tree. |
| Entity Validator (BC-level) | `{BC}/Entity/Validation/<Entity>Validator.php` | Generator: ForceOverwrite | Field-level validation for the BC-root persistence entity, one per BC table |
| Domain Context | `<Domain>Context.php` | Generator: ForceOverwrite | Generated, hermetic base class every BC facade and both Aggregate write/read facades in the domain extend — `implements GeneratedContextInterface` (the `jardissupport/contracts` marker), **no package base class**. Hosts the kernel seam `handle()`/`context()` (`protected`), `resource()/payload()/version()/result()`, and the `classVersion()`/`classVersionConfig()` hooks. Sits directly under the Domain root — no `Foundation/` subdir. **Measuring stick:** this generated Context body is judged by the **generated-code rules in this skill** (hermetic, ForceOverwrite, never hand-edited) — not by the Closure-Orchestrator constitution (`foundation-architecture` §3) that governs hand-written Jardis code |
| BC Facade | `<BC>/<BC>.php` | Generator: ForceOverwrite | `extends <Domain>Context`; accessors only, **no caching**. This is the public surface (G2): exposes `->{agg}()` for every aggregate with a read door at `domain` or wider in `Doors.json` (returns the **read** facade `{Agg}Read` via `handle(<Agg>Read::class)` — aliased `{Agg}AggregateRead` when BC and aggregate share a name) and `->process()` (returns the `<BC>Process` facade). No general write accessor — the write facade is not reachable from here. **Rules-Layer addendum (G10):** for every Command whose `Doors.json` row stands at `domain` or wider, the facade additionally carries a direct method `{lcfirst(Command)}({Cmd}DTO): DomainResponseInterface`, delegating via `$this->context({HandleClass}::class, $cmd)()` into the same generated CommandHandler — the Rule chain (Guard) runs structurally, in the dispatch, not at this door. The Create command is such a door too (row `Create{Agg}`, method `create{Agg}()`); a facade method name that collides with another is `V-QDEF-23`. |
| Rule stub | `{BC}/Closure/<RuleName>.php` | DeveloperOwned (tag `RuleClass`) | `extends <Domain>Context`. `__invoke(...)` — 0..n typed parameters in exactly the catalog entry's `input[]` declaration order, returning `RuleResult` (verdict output) or a plain scalar (any other `output.type`); the single-input `__invoke({Cmd}DTO): RuleResult` guard shape is the most common case, not a fixed rule — your existing-data check logic, reading only your own BC's state (M9) — via `$this->handle({BC}::class)->{agg}()` for a query at `domain` or wider, or directly via `$this->context({Handler}::class, $payload)()` for a BC-internal read — the declared list with `limit: 1`, decided over `total` (e.g. `$this->context(Get{Agg}ListHandler::class, new {Agg}ListFilter(limit: 1, …))()`, see example below). **Docblock — two forms:** the classic shape (exactly one `command`/`aggregate` input, verdict output, `uses` only as a `compose`-bound Rule-Set) renders byte-identical to the earlier single-input docblock; every other shape (n inputs, a scalar output, a freely composed `uses`, a `scalar`/`valuelist` input) renders the fuller form instead — description, an optional policy/message-key line, an optional `reads`/`uses` recipe, and the path of the test you write. Merged on every rebuild by the RuleClass Header-Splice. In practice: a rewritten docblock, a renamed Command DTO or a renamed Rule reach an already-materialised stub; your `__invoke` BODY, your own properties/constants and any helper method you added stay 100% verbatim, and your own imports are merged in additively. A docblock you write directly above `__invoke` survives too. **A freshly generated, not-yet-implemented body throws** — `throw new \RuntimeException('Not implemented: write the rule predicate for ' . self::class)` — instead of returning `RuleResult::pass()` (G03). A Guard-Closure never wraps its Rule dispatch in try/catch, so this propagates uncaught and surfaces through the generated Command handler's `catch (\Throwable $e)` branch as a 500 InternalError — never the 422 `RuleViolation` a bound Rule is meant to produce — until you implement the predicate. ClassVersion-capable (`Closure/v{N}/<RuleName>.php`) — the **payload structure + messageKey** are the stable API across versions, only the accepted set may tighten (M5). **Test (yours to write):** `examples[]` is the contract; the build generates NO test. You write `tests/Unit/Closure/{Domain}/{BC}/{Name}Test.php` from `examples[]` (one test case per example) against the generated `tests/Unit/Closure/{Domain}/ClosureTestCase.php` (helpers `make()`/`readsReturn()`/`usesReturn()`; the stand-ins go through the `{Domain}Context`'s `CLOSURE_STAND_INS` container key). The stub docblock's `Test:` line names the path; the `__invoke` body is finished when that test is green. |
| RuleResult | `JardisSupport\Response\Data\RuleResult` (package `jardissupport/response`) | Package — **not generated** | Readonly VO, named constructors `RuleResult::pass()` / `RuleResult::reject($rule, $messageKey, $context)` — the response takes `$messageKey` over only when the catalog entry declares none; otherwise the catalog key wins (declare it in the catalog and use the same key in the body). The generated stub header, Guard and handler import it from the package. **Migration:** the generator used to write `{BC}/Closure/Data/RuleResult.php` per BC. The next build removes that file when it is byte-identical to the generated one (an edited copy stays and is reported once). A hand-written body that names the old class directly (`use {Domain}\{BC}\Closure\Data\RuleResult;`) is NOT rewritten: switch that `use` to `JardisSupport\Response\Data\RuleResult` (the stub header already imports the package class, so the old `use` can go). |
| Guard closure | `{BC}/Closure/Guard/Guard<Command>.php` | Generator: ForceOverwrite | Chain-of-Responsibility: runs the bound Rule chain as AND with short-circuit; a Guard whose chain reads the aggregate root takes it as a **required, typed** second parameter (`Order $root`) that the handler passes in — no null branch, no throw in the Guard; dispatched from inside the generated CommandHandler at a per-handler-family anchor (Create/Set/Add: after `Validate`, before Apply/persist; Remove: after payload+Load, before Apply, since Remove has no `Validate` call). A rejection never throws — the handler builds a `RuleViolation` (422) `DomainResponseInterface` directly, payload `{rule, messageKey, context}`; a catalog-declared `messageKey` is set as a literal. |
| Aggregate Write Facade | `<BC>/Model/<Agg>/<Agg>.php` | Generator: ForceOverwrite | `extends <Domain>Context`. Hosts the inline Command methods + `event(): <Agg>Events`; each command delegates `return $this->context(<Handler>::class, $dto, $version)();`. **Family-internal only** — reached via the kernel seam (`$this->handle(<Agg>::class)`), never via the BC facade (G2/G3). Fully regenerated — no merge, no `@flow-id`. |
| Aggregate Read Facade | `<BC>/Model/<Agg>/<Agg>Read.php` | Generator: ForceOverwrite | `extends <Domain>Context`. The aggregate's call surface for the public surface, reached directly via `$bc->{agg}()`. Hosts the inline Query/list methods of the read doors at `domain` or wider: `get<Agg>ById`/`ByIds` on an aggregate WITHOUT a public unique key, `By<Key>`/`By<PluralKey>` (instead of ById/ByIds) on one WITH it, `<agg>List` only where `Queries.json` declares it. Each delegates `return $this->context(<Handler>::class, $dto, $version)();`. Thin facade, no logic of its own (review panel B5). Fully regenerated — no merge, no `@flow-id`. |
| Process Facade (BC-level) | `<BC>/Process/<BC>Process.php` | Generator: ForceOverwrite (HERMETIC) | `extends <Domain>Context`. Bundles every process of the BC behind one facade, reached via `$bc->process()`. One thin-dispatch method per process: `return $this->context(<Name>Handler::class, $in, $version)();`. Fully regenerated — **no body merge, no `@process-id`**; process logic lives in the orchestrator's nodes, not here. |
| Aggregate (Root + Entity + Action) | `{Agg}/Aggregate/{<Agg>.php, Entity/<Entity>.php, Action/{Set,Add,Remove}<Entity>.php}` | ForceOverwrite | Invariants, ONE/MANY accessors, mutation rules |
| Command tree | `{Agg}/Command/{<DTO>.php, Handler/<Action>.php, Handler/Action/{Build*Data,Hydrate*Entities}.php, Validation/{<DTO>Validator,Validate<Action>}.php}` | ForceOverwrite | DTO (`readonly`), Orchestrator (no `Handler` suffix), DTO→entity mapping, validators |
| Query tree | `{Agg}/Query/{<Agg>ById,<Agg>ByIds,<Agg>By<Key>,<Agg>By<PluralKey>,<Agg>By<Custom>…,<Agg>ListFilter}.php` + `Query/Handler/Get<Agg>…Handler.php` (**WITH `Handler` suffix**) | ForceOverwrite | Read-base DTOs (ById/ByIds/By{UniqueKey}/By{PluralKey}, the last only with a unique key) + hand-modelled Source-query DTOs + handlers — no suffix-less `get<Agg>` exists |
| Event tree | `{Agg}/Event/{<Agg>{Created,Removed,Updated,…}.php, <Agg>EventRouter.php, <Agg>Events.php}` | ForceOverwrite | Event DTOs (identifier first, `occurredAt` last); router has empty `on<Event>()` bodies; `<Agg>Events.php` is the events registry. Transport is delivery code in `App/**` (§10), not an edit of the router (see `generated-code-recipes` §1). |
| Repository tree | `{Agg}/Repository/{<Agg>Repository,Query<Agg>{ById,ByIds,By<Key>,By<PluralKey>,…},Transform<Agg>,Validate<Agg>,Persist<Agg>,Query<Agg>{Additions,List}}.php` | ForceOverwrite | Read / write pipeline base; one root-fetch class per read variant (`WHERE pk =` / `pk IN` / `unique =` / `unique IN` [only with a unique key] / hand-modelled); custom queries (generic CRUD = pipeline) |
| Process DTO | `<BC>/Process/<Name>/Command/<Name>.php` | Generator: ForceOverwrite | Input DTO for a BC-level process. A field may be a **typed Command field** (typed batch input): declared in `input.fields` as `accepts: [{Agg}.{Command}, …]` (+ optional `list: bool`), it renders as a Composition over the **same-BC** aggregate Command DTOs — a Union `A\|B $field` (single) or `public array $field` + `/** @param array<A\|B> */` (list), Command FQCNs pulled in via `use`. It only **references** the hermetic Command DTOs (never `extends`); no per-entry class is generated, no dispatch loop is emitted (the `foreach … match(instanceof)` is your node code). Command names are the **DTO class names** (e.g. `Counter.Counter`), not handler names. Namespace: `…\Process\<Name>\Command\<Name>`. |
| Process Orchestrator | `<BC>/Process/<Name>/Command/Handler/<Name>Handler.php` (+ `<Name>Context.php`, `<Name>Validator.php` when the input carries a rule, an object-value class `<Name><Value>.php` per `gives` value with fields) | Generator: ForceOverwrite | `extends <Domain>Context`. The handler is the **door, wiring only** (it orchestrates: calls onto closures and a branch on what they hand back, no logic of its own): it builds the `ProcessDefinition` in its own private `definition()` (the whole painted graph, ending in `answerFrom([...])`), checks the input (`ValidateInput` → `AnswerFindings`, 400), checks the `requires` resources (`RequireResource`), opens the transaction bracket `RunOwnedTransaction` (package `repository`) around `ProcessRunner::run($definition, $input, fn(string $class): object => $this->handle($class), $depth)` (it hands back an `OwnedResult`) and answers from it: `AnswerOutcome` (adds the events of the `ProcessOutcome` with `addEvent(…, EventScope::Domain)`, writes the envelope data through `BuildResponseData`, returns the transformed `DomainResponse`); a failure the run ended with goes through `AnswerThrowable` (`MapThrowable`) and the own log step `LogFailure` (`LogCause`). A Process answers **200** with the answer its model names in `end` — the identifier of the changed thing or the declared `fields` of object values, assembled from the nodes that `give` them (never 201; `generated-code-recipes` Recipe 6) — a 4xx `reject` of a node wins in its typed form (`data` = `@type` + the form's fields, plus the node's declared `messageKey`; a 4xx without a form, e.g. 401/403/405, passes through unchanged), an uncaught `Throwable` is a 500. Engine API: `generated-code-workflow-api` §3/§6. **Regenerated, not edited** — the graph is the Process Designer truth. Namespace: `…\Process\<Name>\Command\Handler\<Name>Handler`. |
| Process Node | `<BC>/Process/<Name>/Command/Handler/Action/<NodeClass>.php` | Generator: CreateIfNotExists + `@node-id` body-preserve (custom nodes + sub-process nodes) | Custom node: action stub (`__invoke(<Name>Context $ctx): NodeResult`) — Generator writes the stub + Mini-PRD-DocBlock + an `@node-id` marker once; the body is yours and survives rebuilds via the `@node-id` body-preserve merger. **Aggregate-call nodes get a generated default body instead of the throw stub** at first generation, when the node declares **exactly one** same-BC aggregate-Command `consumedCalls` entry with a green drift verdict — what counts as green and what falls back to the throw stub (cross-BC calls, multiple calls, non-Command/non-aggregate calls, a non-green verdict). The default body calls the aggregate's **write facade directly through the kernel seam** (`$this->handle({Agg}::class)->{useCase}($ctx->input()->{field})` — not via the BC facade, which exposes no write accessor, G2/A5), the argument taken from the single unambiguous input Command field — an `@todo`-only resolver stub covers none-or-ambiguous), and answers `(new MapResponseInto())($response, {Command}Response::class)`: 2xx becomes `ok(<typed {Command}Response>)` (the command's reference value; the getter of the node in `<Name>Context` returns it), a 4xx a `reject` with the callee's context (the run ends, no edge needed), every other status throws. It carries a **marker comment** ("Generierter Default-Body — gegen die Knoten-Requirements pruefen, erweitern oder ersetzen", in English: generated default body — check against the node requirements, extend or replace) — the AI implementation phase's cue that mechanics are done but domain correctness is unchecked; treat it exactly like a stub that already compiles, review it against the node's actual requirements before trusting it. There are no generated failure sinks: a refusal is a `reject` (not a route), so an end node for "not found"/"rejected"/"duplicate" needs no body. An untouched open stub migrates to the generated body on the next build; a body you edited stays byte-for-byte. Sub-process node: **typed Dev-Stub** (`CreateIfNotExists` + `@node-id` body-preserve) — the Generator writes the whole call frame (`$this->context(<Inner>Handler::class, <input>, depth: $ctx->depth() + 1)()` through the inner door, the answer through `MapResponseInto` into the object value the node `gives`) with the real Sub-input field names as named constructor args and open places for the values it cannot derive; only the input values are your code (see `generated-code-recipes` §2 Recipe 8). A node flagged **Event ◇** (`mode: async`) instead is pre-filled to emit a Domain event: the Designer's `ProcessEventFieldEditor` authors `eventFields: [{label, source}]` bindings — either a process-input Command field, or a **collector source** pointing at another node's aggregate-call result (`kind: latest` = that node's last run, `kind: all` = every run as a list; the leading use case is binding the post-persist id of a keyless Create) — the Generator resolves those to the affected aggregate's model identity and renders a `final readonly` event-data class under `Event/<EventClass>.php` with one property per resolved identity chain link plus the two automatic standard fields `eventId` (UUID7) and `occurredAt` — the node body is **fully generated, no hand-fillable stub**: it reads the identity explicitly from the earlier node (`$ctx-><node>()` / `$ctx->all(...)`), ends `NodeResult::route(Route::OnTrue)->withEvents(new <Event>(...))` and routes without data (see `generated-code-workflow-api` §5 Event node). Namespace: `…\Process\<Name>\Command\Handler\Action\<NodeClass>`. |
| Cross-BC-Call / External-Call node | `<BC>/Process/<Name>/Command/Handler/Action/<NodeClass>.php` | Generator: `CreateIfNotExists` + `@node-id` body-preserve (**same mechanism as Custom/Sub-Process nodes** — no new write mode for the Action file itself) | A node whose `consumedCalls` resolve into a **foreign BC** is always a Cross-BC-Call node — with or without an explicit `crossBcCall`; without it the Service is derived from the target — one shared stub per target, named from the facade segments after the domain prefix and the call key (never written into the model), the target is the first foreign call, and the Action imports nothing from the foreign BC. A node declaring `crossBcCall: {ServiceName}` (the cross-BC target stays in the node's own `consumedCalls[0]` — no duplicate facade/method field) or `externalCall: {service}` gets a **working body at first generation instead of the throw stub** — unconditionally, no drift-verdict gating (unlike the collector sources default-body case, because the delegation target here is always the generated Service, never an aggregate directly): `return $this->handle({Service}::class)->__invoke($context);`. The body is still `@node-id`-preserved like any other node, so you may customize it, but there is normally nothing to fill in — the business logic lives in the Service (below), not here. Mutually exclusive with `subProcess` on the same node; `mode: async` excludes all three specializations. |
| Service (Closure) stub | `{BC}/Service/<Name>.php` of the calling BC — the one location (a Cross-BC-Call without an authored `crossBcCall` shares ONE stub per target, named from the target) | Generator: `DeveloperOwned=true` + `ForceOverwrite=false`, **header-aware merge** (`ServiceClass` tag — a third write mode distinct from `@node-id` body-preserve and from `MergeDeveloperFile`) | One `__invoke()` Closure (Closure-Orchestrator, `foundation-architecture` §3) per declared service name — reused across every node/process that targets the **same** thing (same Facade+Method for Cross-BC-Call, same name for External-Call; a different Cross-BC target is a different service, `V-SVC-TARGET-CONSISTENT`). Pre-wired scaffolding: **Cross-BC-Call** gets `$this->handle({CallerBC}::class)`/`handle({TargetBC}::class)` calls plus an **ACL docblock nudge** ("translate the response into your own vocabulary, never pass the foreign DTO through unchanged" — a Nudge, not a static enforcement); **External-Call** gets a PSR-18 scaffold via `$this->resource()->httpClient()` (null-guarded) plus a docblock documenting the transport-error → `Route::OnFalse`/`Route::OnTimeout` convention (and the built-in `jardisadapter/http` retry). **Cross-BC write target (G7):** when the Service call is a **write**, the target is a door of the foreign BC at stance `domain` or wider — its `process()` (a modelled Process there) or a command door (direct BC-facade method) — never its aggregate write facade, which is family-internal; `V-CONSUMED-EXISTS` checks the owner's stance (identical for UI and MCP). Reads may target the foreign `{Agg}Read` methods at `domain` or wider. See `generated-code-recipes` §2 Recipe 9 for the end-to-end DTO-translation → foreign door → response-mapping pattern. Extends `{Domain}Context` like every generated behaviour class — **no constructor**. The write mode is the interesting part: on every rebuild the **class-level docblock is regenerated** (union of every declaring node's Mini-PRD description, deterministically sorted — "the docblock follows the node"), while the `__invoke()` body and any helper methods/`use`-imports you add are preserved exactly, same as a normal developer-merged file. This is the one place in the generated tree where a file's *header* is generator-owned and its *body* is dev-owned within the **same** file. The Service never appears in the generated Api-Spec. |
| Dev-Code (process) | `<BC>/Process/<Name>/Command/Handler/Action/<NodeClass>.php` | Developer-owned (custom-node body) | **The hand-code place of a process** (§10 row 5). Process logic lives in the custom-node bodies — orchestrator + facade are hermetic. No other hand-code place exists (§10); the generated Service stubs a Cross-BC-Call/External-Call node declares sit at `{BC}/Service/`, and their `__invoke()` body is yours to fill. **After filling bodies (custom nodes, Closure/Rule stubs, Service stubs): run `build` / `build_domain` first, then PHPStan** — the stubs carry generated `@throws` tags and Not-implemented markers, and the next build reconciles them against your body (an unproven tag from the stub is dropped, hand-written ones are kept). PHPStan run before that build reports `throws.unusedType` on the leftover stub tags. |

**AI path: a Rule guards a Command with a list existing-data check**. Prose in the Rule-stub docblock → the AI designs a Query that stays `closed` (the default stance) with `limit` → MCP `save_queries` → `save_closures` with `reads:` pointing at that query → `build` → the Rule body reads over the kernel seam with `limit: 1` and decides over `total`. Example: "Customer has open invoices" — Query `openInvoicesByCustomer` (`closed`, `limit: 1`) → `total > 0` → `RuleResult::reject(...)`. **Reader hint:** if a Rule or Process node body names a `Get…Handler::class` the build does not generate (e.g. a stale `Get{Agg}SelectorHandler` reference), the drift category `dev_code_dangling_handler` (`internal/devcodescan`, `appsvc/rules_drift.go`) flags it as "Dev-Code anpassen" (adapt dev code) — comment-blind, it does not read the docblock recipe.

**Three file modes from the generator:**

- **ForceOverwrite** — rewritten on every build. The **entire** `{Agg}/` tree, Domain/BC facades, the hermetic `{BC}Process` facade, Process DTO (`Command/<Name>.php`) + orchestrator (`Command/Handler/<Name>Handler.php`) files, event-data classes (`Event/<EventClass>.php`), and the Rule-Layer hermetic Guard `{BC}/Closure/Guard/Guard<Command>.php` (the verdict class `RuleResult` is the package's `JardisSupport\Response\Data\RuleResult`, not generated). A domain with ≥1 catalog Closure additionally gets exactly ONE hermetic file outside `src/`, `tests/Unit/Closure/{Domain}/ClosureTestCase.php` (the stand-in helpers, regenerated on every build); the per-Closure test next to it is yours, never generated. Every build of a domain with a BC that has an aggregate also generates the test scaffold `tests/Support/{Domain}/` (namespace `Tests\Support\{Domain}`): `{Domain}TestCase.php`, `Test{Domain}.php` and one `{BC}WriteHarness.php` per built BC with an aggregate — never hand-edited; Fakes you add in the same folder stay untouched. How to use it → `foundation-testing` §6.
- **CreateIfNotExists** — written once, then preserved. `Config/.env*` files; Process custom-node stubs `{BC}/Process/{Name}/Command/Handler/Action/<NodeClass>.php` (one file per node).
- **`@node-id` body-preserve** — Process custom nodes **and sub-process nodes**. On rebuild the developer body is preserved, keyed by the `@node-id` marker. Methods the developer changed and the developer's own methods always stay. An untouched generated helper method (anything but `__invoke`, which always stays) stays only while a kept method uses it — a call (`$this->`, `self::`, `static::`, also nullsafe) or its name inside a string literal (case-insensitive); a previously appended, unused helper disappears at the next build. For sub-process nodes: the first build emits a typed Dev-Stub (real Sub-input field names, the inner-door call, the `MapResponse` mapping with the inner events, open places for underivable values); subsequent builds preserve the developer body unchanged. (The Process-Designer "Force" build path can deliberately overwrite a node body instead.) **Rule stubs** (`{BC}/Closure/<RuleName>.php`) use a related but distinct, per-method merge rule (no `@node-id` — a Rule has no single declaring node): an **untouched** generated method migrates wholesale to the new signature/body, a **hand-edited** method is kept 100% verbatim (including its docblock) — for every generated method on a DeveloperOwned class, not just `__invoke`.

**Aggregate facade mechanics.** The aggregate's public surface for reads (absent for an aggregate with no read door at `domain` or wider — no `{Agg}Read`, no route; commands and queries stay usable through the kernel seam and from processes) is `{Agg}/{Agg}Read.php` (`extends <Domain>Context`), reached via `$bc->{agg}()` → `handle(<Agg>Read::class)` — the BC facade's public surface (G2), public FQN `<Domain>\<BC>\Model\<Agg>\<Agg>Read`. The write facade `{Agg}/{Agg}.php` (public FQN `<Domain>\<BC>\Model\<Agg>\<Agg>`) hosts the commands + `event()` and is reached only family-internally via the kernel seam, `$this->handle(<Agg>::class)` — never through `$bc->{agg}()`. Both facades are fully regenerated (ForceOverwrite) — there is no body-merge, no `@flow-id`. You never add methods to either; new aggregate behaviour is modelled in the Designer (structure) or authored as a Process (behaviour). ClassVersion resolves a versioned variant `<Domain>\<BC>\Model\<Agg>\v{N}\<Agg>` (write) or `…\v{N}\<Agg>Read` (read) when the per-call `$version` argument is non-empty (see `generated-code-versioning`).

**Read base — the read ways of an aggregate (hermetic, emitted on every build; the stance of each way is its row in `Doors.json`, default `outside`, and only a way at `domain` or wider is a method of `{Agg}Read`).** Without a public unique key the ways are `ById`/`ByIds`, with one `By<Key>`/`By<PluralKey>` only — `ById`/`ByIds` then stay internal (kernel seam) and have no row:

| Method | Query DTO | Response data block |
|---|---|---|
| `get<Agg>ById($q)` | `Query\<Agg>ById { id }` — type follows the PK | `['@type' => '<agg>'] + <root fields flat>`, children `{Child}Response` objects; nothing found → `[]` |
| `get<Agg>ByIds($q)` | `Query\<Agg>ByIds { ids }` — `list<PK type>` | `['@type' => '<agg>List', 'items' => list<{Agg}Response>]` — objects, **no `[0]` collapse** |
| `get<Agg>By<UniqueKey>($q)` | `Query\<Agg>By<Key> { <keyField> }` — camelCase column, no entity prefix | `['@type' => '<agg>'] + <root fields flat>`, children `{Child}Response` objects; nothing found → `[]` |
| `get<Agg>By<PluralKey>($q)` | `Query\<Agg>By<PluralKey> { <keyField>s }` — `list<key type>` | `['@type' => '<agg>List', 'items' => list<{Agg}Response>]` — objects, **no `[0]` collapse** — only on an aggregate WITH a unique key |

**Reading the data block in PHP.** Only the root level is spread into the array: `getData()['counterNumber']` is correct for a root field, but every child is a typed `{Child}Response` object (a list of them for `many`) — `getData()['registerRelation'][0]->register->obis`, never `…[0]['register']` (an array access on an object throws at runtime; PHPStan does not see it, `getData()` is `array<string, mixed>`). The `items` of a bulk read are `{Agg}Response` objects as well (`$item->counterNumber`). The flat `<agg>List` is the exception: its `items` stay plain arrays (`$list['items'][0]['id']`). Shape reference: `generated-code-recipes` Recipe 6.

`By<UniqueKey>` exists exactly once per aggregate — the identifier chain picks the unique business key (e.g. `getCounterByIdentifier(new QueryCounterByIdentifier(identifier: $i))`); an aggregate without a unique key gets only ById/ByIds on its read facade. An aggregate WITH a unique key gets `By<UniqueKey>` and the bulk variant `By<PluralKey>` (e.g. `getOrderByOrderNumbers`) INSTEAD of ById/ByIds (see Bulk-read recipe below). Further `get<Agg>By<Custom>` methods come from queries DECLARED in the BC's `Queries.json` (`Source.json` has no `queries:` text path — a leftover one is rejected, `V-QDEF-23`); `<agg>List(<Agg>ListFilter)` is the flat list — **not** part of the uniform read base above: it is an ordinary DECLARED query in the BC's `Queries.json` (the base list, seeded there once when an aggregate is first saved), so an aggregate whose `Queries.json` carries no such query has no `<agg>List` method. A suffix-less `get<Agg>` does not exist. The ids are the **internal**, family-wide handle of the domain API (the ById/ByIds query classes and handlers keep being emitted regardless of a unique key and are reached over the kernel seam; only without a unique key are they also read doors) — the facade is no public REST surface; exposure is the surrounding app's decision — and FK/child internals stay encapsulated either way.

**Tenant and user markers (`tenant` / `user` on the aggregate entity).** For an entity with a `tenant` or `user` marker, the Query and Command DTOs that address or write it do not carry the marked column as client input; the tenant is the first constructor argument of the generated `{Query}Filter` (no default), so a read over the kernel seam without the scope is a type error. The generated `routes.php` fills `tenant`/`user` from the request attributes (`RequestAttribute::TENANT` / `::SUBJECT_ID`, `jardissupport/contracts` >= 2.17) and puts the per-route middleware `RequireRight` (right of the endpoint, `jardiscore/app` >= 3.2) and `RequireRequestAttribute` in front: a missing right or attribute answers 403 before the handler, never 500. Hand code reads tenant and user from the process DTO (`$ctx->input()->tenant` / `->user`, filled by the door) and passes them on explicitly, never from client input. A `tenant` marker filters every read variant of the root, and a child entity whose column `adopt`s the effective tenant column of an ancestor is filtered by derivation (every query rooted at it gets the tenant filter, the value comes from `adopt`); a `user` column is only a write value. The write answer and a 404 context carry only the key, not the scope. A `writeOnly` column is absent from every Read DTO, public list item, event payload and command answer. A hand-written family-internal check (verify a stored digest) reads it through a declared query that stays `closed` or `domain` (no `Doors.json` row at `outside`), called over the kernel seam (`$this->context({Handler}::class, $filter)()`); the generated internal query handler projects the column, no outer form ever does.

> **DTO class names vs. alias.** The read/write DTOs are really named **`<Agg>`** (Command, e.g. `Counter`) and **`<Agg>ById` / `<Agg>ByIds` / `<Agg>By<Key>`** (Query, e.g. `CounterById`) in their own `Command\` / `Query\` namespaces. The `Command…` / `Query…` prefix used throughout this skill (e.g. `CommandCounter`, `QueryCounterById`, `QueryCounterByIds`) is a **`use … as …` alias defined inside the aggregate's write facade** (Command) **or read facade** (Query), not the real class name. Code **outside** either facade must import the real name (`use …\Query\CounterById;`) or add its own `as` alias.

**Bulk-read recipe** (the recipe forks on whether the aggregate has a public unique key):

An aggregate **WITHOUT** a unique key: list items lead with the root id, so the set pattern is:

```php
$list    = $bc->heartbeatSignal()->heartbeatSignalList($filter);
$ids     = array_values(array_unique(array_column($list['items'], 'id')));
$records = $bc->heartbeatSignal()->getHeartbeatSignalByIds(new QueryHeartbeatSignalByIds(ids: $ids))->getData()['items'];   // list<HeartbeatSignalResponse> objects
// match per id (`$record->id`) — every aggregate record carries it
```

An aggregate **WITH** a unique key: the list SELECT leads with that key column instead of `id`, and the bulk read goes through the `By<PluralKey>` variant:

```php
$list    = $bc->order()->orderList($filter);
$keys    = array_values(array_unique(array_column($list['items'], 'orderNumber')));
$records = $bc->order()->getOrderByOrderNumbers(new QueryOrderByOrderNumbers(orderNumbers: $keys))->getData()['items'];   // list<OrderResponse> objects
// match per orderNumber (`$record->orderNumber`) — every aggregate record carries it
```

Edge behaviour (existing infrastructure semantics, nothing new underneath): empty input → empty list · duplicates → deduped (IN semantics) · missing keys/ids → partial result, no error (consistent with ById/By<Key> → `null`) · no order guarantee. Family-internal callers (kernel seam) can still use the ById/ByIds handlers either way — the read facade, the list-item handle and the public surface (OpenAPI) are key-only when a unique key exists.

**Process facade mechanics.** `{BC}/Process/{BC}Process.php` (`extends <Domain>Context`) is hermetic — fully regenerated on every build (`ForceOverwrite`, no body merge, no `@process-id`). It carries one thin-dispatch method per process; the process logic lives in the orchestrator's custom-node bodies under `{BC}/Process/{Name}/Command/Handler/Action/`. **Process stance:** a process whose row in the block `process` of `Doors.json` is `closed` is a pure sub-process of its own BC — the facade emits **no method** for it (it is only reachable as a sub-process node inside another process of the same BC). `domain` gives the facade method (also for the other BCs of the domain), `outside` adds route and right, `open` a route without login and right (default `outside`). The stance controls only the doors; the process DTO, orchestrator, and node stubs are always generated. A sub-process node or Cross-BC-Call onto a FOREIGN door needs the stance `domain` or wider there (`V-CONSUMED-EXISTS`, blocker). The stance is set in the door table of the BC page or with `save_doors`; the keys `subprocessOnly` and `visibility` are retired (`V-DOOR-0`).

**BC facade caches nothing** (each `->{agg}()` / `->process()` / `->{command}()` is a fresh `handle()` resolve). **All facades** delegate through `context()` / `handle()`. **Never** instantiate handlers directly (V2/V3).

### 2. Where customization goes — the aggregate has no edit slot

The aggregate tree is hermetic; **nothing** under `{Agg}/` is a developer override point. There are exactly four sanctioned ways to change behaviour:

| You want to… | Do this |
|---|---|
| Add or change **behaviour** (validation, calculation, cross-aggregate coordination, event transport, external calls) | Model a **Process** in the Process Designer. Generator emits the DTO + orchestrator + node skeleton under `{BC}/Process/{Name}/Command/` and `Command/Handler/Action/` (plus `Event/` for event nodes), and a thin-dispatch method on the hermetic `{BC}Process` facade (`$bc->process()`). Your logic lives in the custom-node bodies (`@node-id` preserve); further hand code exists only at the places of §10. A process has **no aggregate ownership** and may coordinate several aggregates. |
| Change the aggregate's **structure / invariants** (entities, relations, keys, `required`/`depend` rules, commands/queries) | Re-model the aggregate in the Aggregate Designer (`Aggregate.json`) and rebuild. The generated tree changes accordingly. |
| Provide a **versioned variant** of a stub class (node, Closure/Rule, Domain Service; tenant / feature-flag) | Put the variant in a `v{N}/` subdir next to the stub (`Action/v{N}/<Node>.php`, `Closure/v{N}/<Name>.php`, `Service/v{N}/<Name>.php`) and select it **per call** via the `$version` argument threaded through every facade method — there is no domain-wide default (the Domain facade is `final`, `{Domain}Context` hermetic). ClassVersion (`LoadClassFromSubDirectory`) resolves `…\v{N}\<Class>` before the baseline. A hermetic class (anything under `Model/{Agg}/`, a handler, a facade) takes no variant: add behaviour in a Process or re-model instead. **Note:** a versioned build (`version: "v2"` on `build`/`build_process`/`build_domain`, CLI `--version`) writes the fresh `final` stub frame of each stub class into its `v{N}/` subdir (same signature, namespace `…\v{N}`, no `extends`; the body you write survives rebuilds) — never a hermetic class. See `generated-code-versioning`. |
| Guard a Command with a **business rule / existing-data check** before it runs (Rules-Layer) | Declare a catalog entry + endpoint binding in `Closures.json` (BC-level, sibling of Process/Steckbrief). Generator emits a Rule stub under `{BC}/Closure/{RuleName}.php` (guard-bindable shape: `__invoke({Cmd}DTO): RuleResult` — one of several signatures the general Closure contract allows, §1 "Vocabulary glossary") plus a hermetic Guard closure that runs the bound chain (AND, short-circuit) ahead of the Command. Your logic lives in the Rule stub body — existing-data checks stay within your **own BC** (M9), via the read facade or directly via the kernel seam (`context()`) for a BC-internal read; anything needing a cross-BC read or a multi-step/side-effecting flow is process territory, not a Rule. **Until you implement it, the stub throws** (500), it does not fail open — see the Rule stub row in §1 and `generated-code-recipes` §3 Troubleshooting. |

**Migration note (path collision).** In the pre-Platform-removal layout a file directly under `{Agg}/Command/…` was a **shadow override** that the ClassVersion reader (`segmentNames: ['', 'Platform']`) found *before* the generator class under `{Agg}/Platform/Command/…`. The Generator writes to `{Agg}/Command/…` itself (ForceOverwrite) — an old baseline override at that path **collides** and is overwritten. There is no auto-migration. To migrate such an override: move the behaviour into a Process (`{BC}/Process/`), or re-model in the Designer. The build deletes the orphan `{Agg}/Platform/` directory on regen.

**Domain support code.** There is no place for value-object or helper classes: a value is checked by the model (column type, value list, process input type, Validator) or by a Closure/Rule; a Domain Service lives at `{BC}/Service/{Name}.php`, the one location of §10 — the rule fixes the place, nobody chooses it by convention. The Generator owns `Command/`, `Command/Handler/`, `Command/Handler/Action/`, and `Event/` of a process; hand code in it stands only in the node bodies of `Action/`. A directory directly under `{BC}/Process/` that is not a process of the model and holds no generated node file (no `@node-id` marker) is **not deleted** — the build warns about it with its path in the build result; it is a stray, no hand-code place. An orphaned, generator-created process directory (the process was removed from the model) is deleted **with its whole content**, developer code included.

**Generated Service stubs sit at one place.** The Service stub of a Cross-BC-Call or External-Call node is `{BC}/Service/<Name>.php` of the calling BC — you choose only the business service name (or leave it to the target-derived default), never a directory or a tier. A developer body an earlier build left at an old place moves to `{BC}/Service/` with the next build. See §1 for the write mode (header-aware merge).

**FieldMap + Entity Validators are generated** (`{BC}/FieldMap.php`, `{BC}/Entity/Validation/<Entity>Validator.php`, ForceOverwrite) — both are **BC-level** (BC-rooted, one per BC table over all BC tables), not aggregate-scoped. The FieldMap exposes one `{table}Columns()` write-map method per BC table. Logic that must participate in those generated entry points belongs in a **Process node** that runs before/after the aggregate call — never as an in-place edit of the FieldMap or validator.

**Validators for "absent" (rule).** A developer-written validator that reports an absent value must implement `JardisSupport\Contract\Validation\MissingValueValidatorInterface` (jardissupport/contracts ≥ 2.3.0); the generated `ValidationException` takes `fields[].reason` from the validator kind (validation ≥ 1.2.0), so only this marker yields `missing` — wording of the message is irrelevant, a finding without a kind is `invalid`.

**Bulk-read warning.** Every generated collection query handler (`get{Agg}ByIds`, `get{Agg}By{Key}s`) logs one warning through the Kernel logger when called with more than 100 keys (`BULK_WARN_ABOVE`); it never aborts — chunk large key lists in the caller.

**Rename & rebuild (object rename).** Domain, BC, aggregate, and process can be renamed in the Designer **even after a build exists**. A rename updates the definition and all referencing JSON documents immediately (`subProcess`, `consumedCalls` — cross-BC included, `accepts`), stamps the renamed artefact and every affected consumer "rebuild needed", and leaves a rename trace; the **next build** pulls the code along. What that means for your code:

- **Developer code survives every rename.** Dev-code-bearing process trees are **moved** (not deleted + regenerated): file **bodies** stay byte-identical; only the mechanical header (`namespace` line + `use` lines pointing at renamed generated namespaces) is patched — PSR-4 stays intact, PHPStan stays green. Hermetic trees (aggregate) are simply regenerated under the new name and the old tree is cleaned up. `v{N}/` directories move physically with the tree (no version-specific namespace follow-up yet).
- **Consumers show a reason note.** A consumer process whose `consumedCalls`/`subProcess` reference was rewritten carries a persisted note (old → new, affected calls), visible in its detail panel; a successful build of that consumer clears note + stamp.
- **Hand code outside the output tree is NOT rewritten.** App/bootstrap wiring or external event listeners that mention renamed FQCNs by hand get a naming warning in the rename preview — updating them is your job.
- **Node rename** (Ticket panel) keeps the `@node-id` body across the file rename; event-mode nodes get their `Event/` class swept along.
- **Node class names are PascalCase, derived from the model name.** The generator derives ONE class name per node: `ucfirst(name)` (an Event ◇ node `mode: async`: node class `ucfirst(name)` + `Node`, event-data class `ucfirst(name)`). File, class and FQCN of node **and** event all come from that one derivation — a model name `checkStock` yields `CheckStock.php` / `final class CheckStock` (PSR-12). Write node names in PascalCase; a lowercase name still builds but the validator warns `V-PROC-NAME-CASE` and names the derived class. A project built before this rule (`checkStock.php`, `Event/stockChecked.php`) migrates on its **next build**: the node file is renamed by its `@node-id` marker with the developer body byte-identical and the old file removed, the event data class is rewritten under the new name — and **every rename is reported as a build Hinweis** (`Knoten <id>: checkStock.php → CheckStock.php, Body übernommen`; for an Event `… FQCN geändert: Listener-Importe prüfen`). The event FQCN changes with it: **hand-written listeners/imports of `…\Event\stockChecked` are yours to update** (they sit outside the output tree and are not rewritten).
- Renaming back (new → old) is a normal rename with the same rules; a rename preview (impact list incl. foreign-BC consumers and dev-surface hints) runs before every level rename in the UI.

**A Closure/Rule catalog-entry rename does NOT follow the rule above — your stub is not moved.** Renaming a `Closures.json` entry (Closure-Editor or MCP `rename_closure`) rewrites the JSON catalog key, every binding chain naming it, every other Closure's `uses` (Set member or free reference alike), and every process Rule-node's `rule` field — but the **developer-owned stub file itself stays where it is**, under its old class name; the rename response carries an explicit warning ("generated dev stub … was not moved — rename it by hand, or let the next build regenerate a fresh one under the new name"). Move your `__invoke()` body yourself, or accept a freshly generated (unimplemented, throwing) stub at the new path. Deleting a Closure still bound to ≥1 chain, referenced by ≥1 `uses`, or used by ≥1 process Rule-node is blocked with `409 IN_USE` and the list of referencing bindings/processes/sets; `force=true` deletes anyway and leaves those references dangling.

### 3. Prohibitions (V1–V13)

| # | Rule |
|---|---|
| V1 | Never edit anything under `{BC}/Model/{Agg}/` — the whole aggregate tree is hermetic (ForceOverwrite). Hand code stands only at the places of §10 (node bodies, Closure/Rule bodies, `{BC}/Service/`, `App/**`). |
| V2 | No `new` for Services / VOs / Entities — only `Exception`, `RuntimeException`, `DateTimeImmutable`, `DateTime` |
| V3 | Never bypass `handle()` |
| V4 | No logic in Command / Query Handler Orchestrators — logic belongs in Actions |
| V5 | Never access Aggregate internals from outside — only via AggregateHandler |
| V6 | No foreign-BC internals — a foreign BC is reached only through its doors (`{Agg}Read` reads; writes go through `process()` or a command door at `domain` or wider, `{BC}::{command}()`), from a node body or a Domain Service, e.g. through a modelled **Cross-BC-Call** node (generator emits the Service scaffolding, §1); a foreign aggregate, repository or SQL is prohibited |
| V7 | No traits, no `abstract` (except Exception hierarchies) |
| V8 | Never skip validation in Command → Entity → Aggregate chain |
| V9 | No persistence in Domain Services — a Domain Service writes nothing in its own BC (own BC read-only, M9); a foreign BC is written only through its doors at `domain` or wider (`process()` or a command door) |
| V10 | No state in `{Domain}Context` subclasses (transient per call) — the generated base class of the Context family (§1) |
| V11 | No business methods on Entities |
| V12 | The `data` payload is an array (flat at the root), the handler returns `DomainResponseInterface` — not a typed DTO; read answers carry their children as `{Child}Response` objects (`->`, see the read-base table in §1). ⚠️ **Precision:** the generator also emits one `{Agg}/Command/Response/{Command}Response.php` per command — a plain data class carrying the reference values (root identifier + affected child reference) the handler writes into `$this->result()->setData([...])`. **Inside vs. outside:** inside the BC (handler, write facade, a process node through `handle()`/`context()`) the command answer additionally carries the root `id` when the root has a public key, so a node can pass it on without re-reading; outside (`{BC}::{command}()`, `{BC}Process::{name}()`, `routes.php`, OpenAPI, cross-BC) `ProjectAnswer` projects the answer onto `{Command}Response` — there no `id` as long as a key exists; without a key `id` is the identifier, inside and outside. That class is a **contract type**, published so a client can be typed against the write door; it is **not** a return type. The handler signature stays `DomainResponseInterface`, its payload stays the array, and nothing about the dispatch changes — **do not rewrite a handler to return one of these classes.** They are hermetic like the rest of the tree (never edit), and derived from the same source the handler body is rendered from, so they cannot drift from what the handler actually writes. |
| V13 | A Rule (`{BC}/Closure/{Name}.php`) reads existing data only from its **own BC** — via that BC's read facade (`$this->handle({BC}::class)->{agg}()`) for a query at `domain` or wider, or directly via the kernel seam (`$this->context({Handler}::class, $payload)()`) for a BC-internal read such as a list read with `limit: 1` — no foreign-BC import, no cross-BC read from inside a Rule (M9). Enforced by convention + a stub docblock nudge, not yet by static analysis. A cross-BC existing-data check belongs in a Process, not a Rule. |

### 4. Decision tree — "I need to…"

Every row resolves to a **modelled artefact** (Process, Query, Closure), a **Designer re-model**, or a **versioned stub variant** — never an in-place edit of the aggregate.

| Need | Action | Pattern |
|---|---|---|
| Validate a domain concept (OBIS, IBAN) | Express it in the model: a column type or value list in the Schema, the process input type, or a Closure/Rule with the check in its body, used from a node. No free value-object class exists (§10); a check the model cannot express is a missing Jardis capability — report or build it, do not substitute | Specification (Closure) |
| Load data from an external API | Model an **External-Call** node in the Process Designer — the generator emits a Service stub at `{BC}/Service/`, pre-wired with a PSR-18 `resource()->httpClient()` scaffold + a transport-error docblock (§1). If no node kind fits, a Jardis capability is missing: build it in the package or generator (when working on Jardis itself) or report it as an issue (when working on an app) — never a hand-built substitute. | Adapter |
| Read from another BC | Model a node whose `consumedCalls` point at the foreign BC's `{Agg}Read` — that is a **Cross-BC-Call** node by itself — declare `crossBcCall: {ServiceName}` on it so that one shared Service serves every node with the same target (without it the generator derives one shared Service per target, named from the target's facade segments after the domain prefix and its call key); the generator emits the target-facade wiring (reusing the node's `consumedCalls[0]`) plus a Service stub with pre-wired `handle()` calls and an ACL docblock nudge (§1); never import another BC directly. Target: the foreign `{Agg}Read` methods. | Adapter |
| Read a BC-specific setting from the environment | Declare the key in `BC.json` `env[]` (MCP `save_bc_env`), write the value through `save_runtime_env` block `domain` into the one project `.env`, read it in the body with `$this->resource()->env('<KEY>')`; an undeclared key is refused (`V-RT-7`) | — |
| Store a value the process derives (sum, counter) | Mark the column `computed` in the Aggregate Designer (field table switch / MCP `save_aggregate` node field `computed`): it leaves every Create/Set/Add/Update command and the OpenAPI request (a body naming it is `400 unknown`; Read, list and Akte keep it). Write it from a node body through the family-internal `$this->handle({Agg}::class)->set{Entity}Computed($dto)` — own event `{Agg}[{Fragment}]ComputedUpdated`, no route, no BC-facade method | Facade / Command (`generated-code-recipes` Recipe 13) |
| Write to another BC | Model a **Cross-BC-Call** node targeting a door of the foreign BC at stance `domain` or wider — its **process** or a **command door** (never its aggregate write facade; `V-CONSUMED-EXISTS` checks the owner's stance); the generated Service DTO-translates your input, calls `$this->handle({TargetBC}::class)->process()->{process}($dto)` (or the command door `{lcfirst(command)}($dto)`), and maps the response back into your own vocabulary (ACL) — never a direct aggregate write on the foreign BC. See `generated-code-recipes` §2 Recipe 9. | Adapter |
| New BC-level operation / coordinate several aggregates | Model a **Process**. Generator emits DTO + orchestrator + node skeleton under `{BC}/Process/{Name}/Command/` and `Command/Handler/Action/`, and a thin-dispatch method on the `{BC}Process` facade (`$bc->process()`). Logic lives in the custom-node bodies. | Facade + Workflow |
| New aggregate command/query, or tighten an aggregate's invariants | Re-model the aggregate in the Aggregate Designer (`Aggregate.json`) and rebuild — the generated command/query/validator tree changes with it. A new **query** surfaces inline on the read facade `{Agg}Read.php` (`$bc->{agg}()`, the BC facade's public surface); a new **command** surfaces inline on the write facade `{Agg}.php`, reachable family-internally (`$this->handle({Agg}::class)`) and from outside only through the stance of its row in `Doors.json` (`domain`/`outside` = facade door `{BC}::{command}()`) — or model a Process around it to expose it via `$bc->process()`. There is no slot to hand-edit either facade. | Designer |
| Something that applies to every door equally and needs no domain model (session check, CORS, request id, rate limit) | **Not a process node, not a closure**: PSR-15 middleware at the HTTP front door, registered with `Routes::middleware()` — see `core-app`. A rule that needs the domain model ("may this user see this project") stays a Rule/Process. A missing general middleware is a gap to report in `jardisCore/app`, not project code. | Chain of Responsibility |
| Add a calculated field to a query response | Declare the column `computed` in the Aggregate Designer and write it with `Set{Entity}Computed` (Recipe 13): Read, list and Akte then carry it. A value the model cannot express is a missing Jardis capability — report or build it; no enrichment in a node body | Designer |
| Emit / route a domain event to a transport (Kafka, webhook, …) | Announce it with an Event ◇ node; the transport is delivery code in `App/**` that publishes the answer's Domain events after the commit and carries no business logic — see `generated-code-recipes` event-transport recipe. The generated `<Agg>EventRouter` is hermetic. | Adapter (delivery layer) |
| Remove an entity with a NOT-NULL `depend:` parent | **Not allowed** — the Build rejects Remove on an entity another entity depends on via NOT-NULL FK (F3.6 / V8-sibling). Re-model the aggregate to drop the NOT-NULL constraint, or model a soft-delete Process so dependents stay resolvable. | Designer + Process |
| Remove a `required` child (no `Remove…` command exists, or the last entry refuses to delete) | **By design** — a relation marked `required` (default; opt-out `required: false`) is an existence invariant. **`required·one`** emits **no `Remove…`** (only `Set…`); **`required·many`** keeps `Add…`/`Remove…(id)` but the Remove handler carries a **min-guard** that throws when count would hit 0 (Create also demands ≥1, `Count::min(1)`). To allow emptying, re-model as `required: false` and rebuild. | Designer (Aggregate.json) |
| Tenant- / feature-flag variant of a stub class (node, Closure/Rule, Domain Service) | Put the variant in the stub's `v{N}/` subdir and select it **per call** via the `$version` argument — no domain-wide default (see `generated-code-versioning`). Resolution is wired; a versioned build writes the fresh stub frame, you write its body. | ClassVersion |

### 5. New-code locations — the places of the one table (§10)

```
<Domain>/<BC>/                               ← Bounded-Context dir
├── Closure/                                  ← ★ Closure catalog (sibling of Process/ and Model/)
│   ├── <RuleName>.php                       ← ★ your body (DeveloperOwned, tag RuleClass) — 0..n typed
│   │                                            params per `input[]`, RuleResult or scalar per `output`
│   ├── Guard/Guard<Command>.php             ← hermetic AND-chain closure (Generated, ForceOverwrite)
│   └── v{N}/<RuleName>.php                  ← versioned Closure override (ClassVersion)
│                                               (the per-Closure test under tests/Unit/Closure/ is yours to write from examples[],
│                                                against the generated ClosureTestCase — §1)
├── Service/                                 ← ★ Domain Services (sibling of Closure/ and Process/), the one location
│   ├── <ServiceName>.php                    ← ★ your body (Domain Service stub); own BC read-only, foreign BC only through its doors
│   └── v{N}/<ServiceName>.php               ← versioned Service override (ClassVersion)
├── Process/                                 ← BC-level Processes (sibling of Model/); hand code only in the ★ Action/ bodies
│   ├── <BC>Process.php                      ← hermetic facade (Generated, ForceOverwrite) — $bc->process()
│   └── <Name>/
│       ├── Command/                         ← Generator-owned (ForceOverwrite, hermetic)
│       │   ├── <Name>.php                   ← Input DTO
│       │   └── Handler/
│       │       ├── <Name>Handler.php        ← orchestrator (regenerated, not edited)
│       │       └── Action/<NodeClass>.php   ← ★ custom-node body — your process logic
│       │                                      (CreateIfNotExists + @node-id preserve)
│       └── Event/<EventClass>.php           ← event-data classes (Generated, ForceOverwrite)
└── Model/<Agg>/                             ← ★ GENERATED, NEVER EDIT (whole tree, no Platform/ segment)
    └── … (read/write facades, Aggregate, Command, Query, Event, Repository)   ← FieldMap + Entity/ are BC-level (one level up), NOT in the aggregate tree
```

Outside `src/`, a build additionally writes the test-support scaffold `tests/Support/{Domain}/` (never hand-edited, see §1 "ForceOverwrite" and `foundation-testing` §6).

Hand code stands only at the places of §10: node bodies under `{BC}/Process/{Name}/Command/Handler/Action/`, Closure/Rule bodies, Domain Services under `{BC}/Service/`, and `App/**` delivery code. The Generator owns the rest of the process tree (`Command/` DTO + orchestrator, `Event/`). The aggregate tree `{BC}/Model/{Agg}/` is hermetic: never add or edit files there; it takes no `v{N}/` variants (§2).

There is no aggregate-root file to extend, no `{Agg}/Api/` registry, and no `Platform/` directory. The aggregate's two facades host the inline registries of generated Commands (write facade, family-internal via the kernel seam) and Queries/lists (read facade, reached via `$bc->{agg}()`, the BC facade's public surface); BC-level operations route through the `{BC}Process` facade via `$bc->process()`.

### 6. Implementation levels

No skipping, no parallel levels. PHPStan L8 + tests + review green between levels. All hand-code levels are expressed at the places of §10 — the aggregate itself is generated.

| # | Level | Where | Anchor |
|---|---|---|---|
| 1 | Aggregate structure + invariants | Aggregate Designer (`Aggregate.json`) → generated `{Agg}/` tree | `design-draft-schema` |
| 2 | BC-level processes (behaviour) | Process Designer: `{BC}/Process/{Name}/Command/` (DTO) + `Command/Handler/` (orchestrator) + `Command/Handler/Action/` (one `<NodeClass>.php` per node) — your logic in the custom-node bodies (`@node-id` preserve) | `jardissupport/workflow` |
| 2a | Endpoint business rule (existing-data check gate before a Command runs) | `Closures.json` catalog + binding → `{BC}/Closure/{Name}.php` stub, your logic in `__invoke()` (own BC only via the kernel seam, M9). **A stub not yet implemented throws (500), never `RuleResult::pass()`** — implement it before binding it live. | — (Rules-Layer, no separate package skill) |
| 3 | Business validation used by a process | modelled: column type / value list in the Schema, process input type, or a Closure/Rule — used from node bodies; no free value-object class | `jardissupport/validation` |
| 4 | Domain Services (external data, calculations; a foreign BC only through its doors) | `{BC}/Service/{Name}.php`, called from a node body via `handle()` | `generated-code-extend` §10 |
| 5 | Workflows / long-running flows | Process graph (`definition()` of `{Name}Handler`, run by the same door) + custom nodes | `jardissupport/workflow`, `generated-code-workflow-api` |
| 6 | Event transport | Event ◇ node announces the Domain event; `App/**` delivery code publishes it after the commit (the generated `<Agg>EventRouter` is hermetic) | `jardisadapter/messaging` |
| 7 | Non-functional (cache, logging) | Process node + DomainKernel wiring | `jardisadapter/cache`, `jardisadapter/logger` |

### 7. Constructing `DomainResponse`

The payload of a response is **flat**: the data is the business object itself and names its type in the field `@type` — no handler-name level, no root-key level around it (the package's `TransformResponse` takes the data of the one root context as `data`; two contexts carrying data throw a `LogicException`, nothing is merged silently). A Process node body does **not** construct the response: it returns a `NodeResult` and the door builds the `DomainResponse` (`generated-code-workflow-api` §4/§6):

```php
public function __invoke(MyContext $ctx): NodeResult
{
    $found = $this->read(GetCounterListHandler::class, new CounterListFilter(identifier: $ctx->input()->identifier, limit: 1))->first();

    return $found === null
        ? NodeResult::reject(ResponseStatus::NotFound, 'counter.notFound', ['identifier' => $ctx->input()->identifier])   // 4xx ends the run; the door forms the typed failure
        : NodeResult::ok($this->handle(CounterRef::class, counterIdentifier: $ctx->input()->identifier));           // object value the node gives (readonly)
}
```

The `$this->result()` helpers below belong to the generated handlers and the door, which build the response through them; a node body never calls them.

| Status | Use |
|---|---|
| `Success` (200) | Read with data; also **every successful write over the wire** — a Process answers 200 with the reference value (or the fields its `end` declares, `generated-code-recipes` Recipe 6; the reference value is the public key outside, `id` only without a key — inside the BC the command answer also carries the root `id`), never 201 (`ResponseStatus::Created` appears only inside the generated aggregate Create handler, which is family-internal and never a route) |
| `NoContent` (204) | Write succeeded, empty body |
| `ValidationError` (400) | DTO / invariant validation failed — thrown as a dedicated `ValidationException` in generated Validate classes (typed property `fields`: `list<array{field, reason, message, messageKey}>`; `field` = outer name per FieldMap as dot path with the list index at every door (`orderItem[0].qty`), `reason` = `missing` only for a validator marked `MissingValueValidatorInterface` (absent enum/date/number/boolean/value-list/child), else `invalid` — an empty string or `null` in a NOT-NULL string column is `invalid`; a body key the contract does not name answers `unknown` from the door, before the handler), caught **before** the catch-all `\Throwable`; precedes Rule evaluation (form before business). Payload `{"@type":"validation","fields":[…]}` |
| `RuleViolation` (422, Rules-Layer; requires `jardiscore/kernel` ≥ 1.1.0) | A bound Rule chain rejected the Command — built directly by the generated Guard-calling handler code, **never thrown**; at a Process door the 422 may also come from a dev body of the own BC (open `messageKey`, see the contract note above); `messageKey` is the catalog key when declared, not the body's `reject()` argument; payload `{"@type":"ruleViolation","rule","messageKey","context"}` (stable across Rule versions, `messageKey` = the catalog key when the entry declares one, i18n resolved by the surrounding app; an empty `context` is an empty object). A **technical** failure inside a Rule's existing-data check (DB down, timeout) is never a 422 — it falls through to 500. |
| `Unauthorized` / `Forbidden` (401/403) | Raised by middleware, not here |
| `NotFound` (404) | Target absent — payload `{"@type":"notFound","context"}` (`context` = the identifier(s) that failed to load, an object) |
| `Conflict` (409) | A write lost its read basis (stale write/delete) — payload `{"@type":"concurrencyConflict","reason","aggregate","context"}`; the handler catches the conflict exception before the catch-all `\Throwable`; a transient database conflict (typed `TransientConflictException`, contracts ≥ v2.6.0 / repository ≥ v1.6.0) is a 409 `stale_write`/`stale_delete` too, not a 500 — on MariaDB/MySQL inside a process it surfaces at the process bracket, not at the node (`generated-code-recipes` Recipe 11 Case B); repeat = client retry in a new transaction |
| `InternalError` (500) | Unexpected `Throwable`; the message is in `errors` |

**409 `violated` and `expected`.** A `duplicate` 409 carries `violated: list<string>` (outer field names of the hit unique columns, child fields as `<kindProperty>.<field>`; `[]` for unknown columns and for every `stale_write` / `stale_delete`) — see `generated-code-recipes` Recipe 12. The aggregate **write facade** `{Agg}` (family-internal, reached via the kernel seam / `$this->context(...)`) takes an optional `array $expected = []` after `string $version` on the root writers (`update{Root}`, `set{Root}Computed`): outer field name ⇒ the value the calling node read; the handler uses it as the CAS expectation instead of the freshly loaded state (`generated-code-recipes` Recipe 11, Case B). It is **not** part of the BC facade, the Rules door, routes, OpenAPI or any Command DTO; child writers and `Remove` do not carry it; an unknown key answers 500.

When you build a 400/404/409/422 by hand, use exactly these typed payloads — the same four forms every route, aggregate command and Process answers with. A dev body refuses with `NodeResult::reject(status, messageKey, context, errors)`; the package builds the form from the context keys it knows (400 `fields`, 409 `reason`/`aggregate`/`violated`, 422 `rule`; the rest is the form's `context`), a Process 4xx without a form (401/403/405) is passed through unchanged. The `messageKey` a node declares with `rejects(status, key)` (else the one named in `reject(...)`) is set on `data` after forming, for every 4xx status (also when it is not a field of the form, and also for a 4xx that has no form).

**Status staircase (Rules-Layer):** `400` (field validation) → Load → `404` (root not found) → `422` (business, Rule chain) → `404` (child / apply miss, after the Guard) → `409` (persist conflict) → `500` (technical) — a Command dispatch checks field validation first (cheap, local), then the Rule chain (potentially DB-bound), so a malformed request never pays for a existing-data check it can't pass anyway. A Process node calling a Command translates this deterministically through `MapResponse`: 2xx → `ok(data)`; a 4xx (including `422`) → a `reject` with the same status — the run ends, the engine routes no reject; `5xx → the exception path` (500, cause logged) — never a blanket `isSuccess() ? ok : route(OnFalse)` mapping, which cannot tell the two apart. Only a Dev decision inside a node may map a 4xx to a route.

**Process gate (before the first node).** A Process rejects a faulty command input before any node runs and before its transaction opens: the orchestrator `{Process}Handler::__invoke` starts with `ValidateInput` over the registry of `{Process}Validator` (emitted only when the input carries a rule), which checks every command input field (single value, list, union) with the validator of the command the aggregate's own handler runs (a Remove has no validator and is not checked). A field error answers 400 `validation` with `fields[]` in the gate's path form (`<property>.<field>`, `<property>[<i>].<field>`) at every door (route, `process()` facade, sub-process), so not even a 422 Rule node or a writing predecessor node comes first. The 400 lists one finding per field with `messageKey` `validation.missing` / `validation.invalid` (the package adds it from the finding's `reason`; a text the validator delivered stays untouched).

**TOCTOU boundary (v1, known limitation).** A Rule's existing-data check and the Command's subsequent Apply/persist are two separate steps — nothing in the generated Guard or CommandHandler locks between them. A concurrent write between the Rule's read and the Command's write can pass a Rule that would have rejected the now-changed state (time-of-check-to-time-of-use). This is a deliberate v1 boundary, not a bug: close it in the model — as a hard invariant in the DB schema (`UNIQUE`, `CHECK`, FK) that fails the write outright rather than relying on the Rule alone (`generated-code-recipes` Recipe 12), or as state with a conditional write (Recipe 11). A recheck of your own is not a Jardis artefact and has no place in a Rule body. A general recheck/optimistic-locking mechanism is not yet part of the generated code — that is a gap to build in Jardis or to report (§10), not to emulate.

`$this->result()` helpers:

- `setData(array)` — replace the flat payload (last writer wins; carry `@type`)
- `addData(string $key, mixed $value)` — additive field of the flat payload
- `addError(string $message)` — accumulates under the context name → on the wire `errors` is an object `{<context>: [message, …]}` (`{}` when empty, never `[]`) — use it for technical messages (a 500); the cause of a typed 4xx belongs in the form's `data` fields (`fields[]` / `context` / `rule`+`messageKey`), not in `errors`
- `addEvent(object $event, EventScope $scope = EventScope::Internal)` — accumulates only; the handler does **not** dispatch. Generated aggregate life-cycle events are `EventScope::Internal`. Publication is the **caller's** job after commit: a custom Process node that holds the command's response hands its events to a transport, or an Event ◇ node announces a Domain event with `NodeResult::route(Route::OnTrue)->withEvents(...)` — the events travel with the result, the handler collects those of a successful run as `EventScope::Domain` on the process response (see `generated-code-recipes` §1). `getEvents(?EventScope $scope = null)` filters by scope; with no argument it returns all scopes. `EventScope` is a `jardissupport/contracts` enum.

`DomainResponse`/`ContextResponse` (the Response pair), `TransformResponse` (the envelope mapper) and `BuildResponseData` (how a Process answer becomes the envelope's `data`) are **package classes of `jardissupport/response`** (`JardisSupport\Response\Data\Response\`, `JardisSupport\Response\Handler\Response\`; skill `support-response`) — the generated `{Domain}Context` only reaches them through `result()` / `transformer()`. The clock of a run is PSR-20: `{Domain}Context::clock()` resolves `SystemClock` through the kernel seam, and it stamps the envelope and the columns marked `auto: created|modified` in `Schema.json` (read ONCE per write); a test or a deployment substitutes it through the container key `CLOSURE_STAND_INS` (`{Domain}Context::CLOSURE_STAND_INS`, a map `className => stand-in`, e.g. a `FixedClock` under `SystemClock`). The `DomainResponseInterface`/`ContextResponseInterface` contracts, the `ResponseStatus` enum, and `EventScope` live in `jardissupport/contracts` (no dedicated dev-skill).

### 8. Toolbox — which skill covers which Jardis package

When a Process node needs a Jardis runtime tool (cache, mail, queue …), consult the package skill below before re-deriving the API. All packages are constructor-injected via `handle(<Service>::class, …)` from inside the node — never `new` (V2 / V3). Three rows — DB connection, DB query builder, Repository — name packages that the generated code and the packages use; hand code never uses them: reading is a declared Query, writing is an aggregate Command or a `process()` call (§10).

| Need | Skill |
|---|---|
| Cache (PSR-16, multi-layer, write-through) | `adapter-cache` |
| Logger (PSR-3, fluent builder, enrichers) | `adapter-logger` |
| Mail (SMTP, STARTTLS, attachments, retry) | `adapter-mailer` |
| HTTP client (PSR-18, cURL transport, retry) | `adapter-http` |
| Filesystem (local + S3, streams) | `adapter-filesystem` |
| Messaging (Kafka / Redis / RabbitMQ / DB / InMemory) | `adapter-messaging` |
| Event dispatcher (PSR-14, priorities, type-hierarchy) | `adapter-eventdispatcher` |
| DB connection (pool, read/write split, MySQL/Postgres/SQLite) — generated code and packages only, never hand code | `adapter-dbconnection` |
| DB query builder (fluent SELECT/INSERT/UPDATE/DELETE, CTE, window) — generated code and packages only, never hand code | `support-dbquery` |
| Repository (generic CRUD on top of dbquery) — generated code and packages only, never hand code | `support-repository` |
| Validation (composite, fluent, object graph) | `support-validation` |
| Workflow engine (multi-step orchestration) | `support-workflow` (and `generated-code-workflow-api` for the Process Designer-Orchestrator API) |
| Auth (session, password hash, RBAC) | `support-auth` |
| Scheduling / cron parsing | `support-scheduling` |
| Secret resolution (encrypted `.env` values) | `support-secret` |
| Dotenv loader (cascade, type casting) | `support-dotenv` |
| Hydration, identity, FieldMapper, UUID/NanoID | `support-data` |
| ClassVersion loader / namespace injection | `support-classversion` (Designer-side: see `generated-code-versioning`) |
| Factory / container | `support-factory` |
| `DomainKernelInterface` (the DomainKernel) + Bootstrap-Packer `BuildDomainKernelFromEnv` (ENV-driven DomainKernel assembly) | `core-kernel` — `DomainApp`/`BoundedContext`/`ServiceRegistry` do not exist; their role is carried by the generated Domain facade + `<Domain>Context` (this skill, §1) |
| `DomainResponse`/`ContextResponse`/`TransformResponse`/`BuildResponseData` | `support-response` (package `jardissupport/response`, §7 above) |
| One-time ENV-driven App entry point (`App/bootstrap.php` + `App/.env`, wires the DomainKernel + every workspace domain) | `core-kernel` (Bootstrap-Packer) — `jardiscore/foundation` does not exist; never reach for it |

### 9. Anchors

- `foundation-architecture` (pillar 4 Data-Behavior-Separation drives the service-layer note in `generated-code-versioning` §2), `foundation-patterns`, `foundation-testing`
- `generated-code-versioning` (ClassVersion resolution + versioning model — `LoadClassFromSubDirectory`, `v{N}/` per-class)
- `generated-code-workflow-api` (Workflow-Engine API used by Process Designer orchestrators)
- `generated-code-recipes` (Phase-3 recipes, event transport, troubleshooting)
- `DomainResponse`, `ContextResponse`, `TransformResponse`, `BuildResponseData`: package classes of `jardissupport/response` (§7 above); `ResponseStatus` + the response/context interfaces: `jardissupport/contracts`
- `MessagingService`, `MessagePublisher`: `adapter-messaging`
- `EventListenerRegistryInterface`, `EventDispatcher`, `EventCollector`: `adapter-eventdispatcher`
- `HttpClientInterface`, `HttpClient`: `adapter-http`

### 10. Where hand code may stand — the one table

Hand code is written only into the stubs a build produces and into `App/**` delivery code; every other file is generated and hermetic. This is the single place-table of the skill set — every other skill points here and carries no list of its own. A PRD or plan that names a piece of work sorts it by this table; a place that is not in it does not exist. Hand code uses only Jardis artefacts (kernel seam, packages, Queries, Commands and Processes, value lists); no own SQL, no helper or base classes, no foreign-BC access except through its doors.

<!-- generat:begin ablage-tabelle (internal/docgen/ablage.go from genpath + stubmanifest; regenerate: make ablage-tabelle; do not edit by hand) -->
| # | Ort | schreibt | Modus | erlaubte Abhängigkeiten | Manifest-Art |
|---|---|---|---|---|---|
| 1 | `<Domain>.php`, `<Domain>Context.php` (domain root) | Generator | hermetic (ForceOverwrite) | `contracts`, support packages; never `JardisCore` (Kernel-Entkopplung stage 2) | — |
| 2 | `{BC}/{BC}.php`, `{BC}/FieldMap.php`, `{BC}/Entity/**`, `{BC}/Data/{Name}.php` (one enum per value list), `{BC}/Process/{BC}Process.php` | Generator | hermetic (ForceOverwrite) | own BC, packages | — |
| 3 | `{BC}/Model/{Agg}/**` — includes the classes of every query declared in the BC-wide `Queries.json` (`Query/`, `Query/Handler/`, `Repository/` of the aggregate that owns the query root) | Generator | hermetic (ForceOverwrite), no `v{N}/` | own BC, packages | — |
| 4 | `{BC}/Process/{P}/Command/{P}.php` (+ item classes), `Command/Handler/{P}Handler.php` (the one door class, the graph is its `definition()`), `{P}Context.php` (one typed getter per node), `{P}Validator.php` (dropped when it would be empty), `{P}{Name}.php` (object value of a `gives` with fields), `Event/**`, generated Rule nodes in `Action/` | Generator | hermetic (ForceOverwrite) | kernel seam, packages | — |
| 5 | `{BC}/Process/{P}/Command/Handler/Action/{Node}.php` | frame Generator, **body hand** | stub: `CreateIfNotExists` + `@node-id` body-preserve; a `v{N}/` variant is allowed | kernel seam (`handle` / `context` / `read`), Queries and Commands of the own BC, value lists, packages, sub-process and Cross-BC-Call nodes, Domain Services of `{BC}/Service/` via `handle()` | `node` |
| 6 | `{BC}/Closure/{Name}.php` | frame Generator, **body hand** | stub: `RuleClass` header-splice; a `v{N}/` variant is allowed | the own BC read-only (M9, V13), value lists, packages | `closure` |
| 7 | `{BC}/Closure/Guard/*` | Generator | hermetic (ForceOverwrite) | `response` (`RuleResult`) | — |
| 8 | `{BC}/Service/{Name}.php` | frame Generator from a model artefact — a Closures.json catalog entry of kind `service` (the fourth closure kind, MCP `save_closures`) **or** a stub derived from a Cross-BC-Call/External-Call node — **body hand** | stub: header-aware merge (`RuleClass` for a catalog Service, `ServiceClass` for a derived stub); a name used by both is the blocker `V-SVC-NAME`; a `v{N}/` variant is allowed | the own BC read-only (M9) — a write in the own BC is a node's business, not a Service's; a foreign BC only through its doors: `process()` or a command door at `domain` or wider writes, `{Agg}Read` reads — a foreign aggregate, repository or SQL is prohibited; called from node bodies via `handle()` | `service` |
| 9 | `Api/{Domain}/openapi.yaml`, `routes.php`, `versions.php`, `rights.json` (the rights catalog, MCP resource, only for an app with a login), `system.php` (the system run over `SignedInRun`, only for an app with a login); `App/middleware.php` | Generator | hermetic (delivery) | `jardiscore/app` (`RequireRight`, `SignedInRun`), `response` | — |
| 10 | `App/**` — `bootstrap.php`, project middleware per FQCN, event listeners, transport (all except the generated `middleware.php`) | Generator (first creation) and **hand** (the AI, on the human's request) | the place of the AI's own implementation; no business logic here, the rules of hand code apply | the doors `{agg}()` / `process()`, `jardiscore/app`, packages; never `Model/**`, a repository or `DbQuery` | `delivery` (reserved: the Builder writes no stub here today) |
| 11 | `v{N}/` subdir next to a stub class (rows 5, 6, 8) | frame: versioned build (fresh `final` stub frame, no `extends`), **body hand** | ClassVersion-resolved variant of a stub; body-preserve on every rebuild; never beside a hermetic class | those of the stub it varies | the kind of the stub it varies, `v{N}` set |
| 12 | `tests/**` | **hand** (the tests); Generator (scaffold: `tests/Support/{Domain}/`, `tests/Unit/Closure/{Domain}/ClosureTestCase.php`) | no production-code place — Jardis generates the scaffold, never the test | — | — |

**The Stub-Manifest.** Every build writes `.jardis/stubs.json` at the project root (committed, one file per project): one entry per stub of the stub rows — `path`, `kind` (`node`, `closure`, `service`, `delivery`), `marker` (the open-body text of the fresh frame) and `v{N}` (empty = baseline; set for the `v{N}/` frames). It is the list of the places the AI writes into: a partial build merges, an entry never outlives its file, a filled stub stays listed. A hand file of the AI that is neither in the manifest nor at a place of this table does not exist.
<!-- generat:end ablage-tabelle -->

**The context head of a stub (PHPDoc chain).** Business meaning is description (Domain → BC → Process → Node) and lands as PHPDoc, the context the AI reads where it implements. The Domain description stands at `<Domain>.php`, the BC description at `{BC}/{BC}.php`, the process description at `{P}Handler` (rows 1, 2, 4); every stub of rows 5, 6 and 8 carries the chain as the last block of its class docblock — generated frame, so every build refreshes it and the hand-written body is never touched:

```
 * Kontext-Kette (von oben nach unten):
 * Domain: <Name>
 *     <description of the Domain, indented, may span lines>
 * BC: <Name>
 *     <description of the BC>
 * Prozess: <Name>            (node stubs only)
 *     <description of the process>
 * Knoten: <Class>            (Closure: / Service: for a stub without a process)
```

A level line is `<Level>: <Name>` at the start of a docblock line and carries the name only; description lines are indented, an empty description leaves the level line without text (the registry warns `V-DESC-EMPTY` for an empty Domain, BC or process description). The node, Closure or Service's own description is the prose above the chain. The Gesetz-Gate (invariant D, `StubChainRule`) checks the level lines — a node stub needs Domain, BC, Prozess, Knoten; a Closure stub Domain, BC, Closure; a Domain Service stub Domain, BC, Service — never the text.

**If the model cannot express it,** no hand-code place opens up: a missing capability is a gap in Jardis. When you work on Jardis itself, build it in the package or the generator first and use it afterwards; when you work on an app, report it as an issue and wait. A substitute in the project — an interim, a workaround, a class in a place that is not in the table — is a violation, not progress.

**Removed hand-code places** (a file there is a violation; the generator emits no new ones):

1. `{Domain}/Service/*` (the Cross-BC-Call stub always, External-Call `level: domain`) → `{BC}/Service/` of the calling BC.
2. `{BC}/Process/{P}/Service/` (External-Call `level: process`, formerly the default) → `{BC}/Service/`; the External-Call field `level` is retired.
3. `{BC}/Process/{P}/Query/` and `Repository/` → none: reading is a declared Query, writing is an aggregate Command or a `process()` call.
4. `{BC}/Process/{P}/ValueObject/` and free projector or helper classes → none.
5. `v{N}/` variants of hermetic classes → none; `v{N}/` exists only for rows 5, 6 and 8. `App/**` is the delivery place of row 10; a process trigger by event or time is a project of its own, not a hand-code place.

The generator emits none of these places any more; a developer body left there moves to `{BC}/Service/` with the next build (Hinweis per move).

### 11. The way

The order of work for a change to a Jardis domain. Each step has one skill; do not skip ahead.

1. **Model.** Express the change in the model first (schema, aggregate, process, queries, closures), through the authoring doors. → `design-headless-mcp`, `design-draft-schema`.
2. **Check.** Validate the model before building; fix findings in the model, never in the output. → `design-headless-mcp` (the `validate_*` tools).
3. **Build.** Generate the code; the hermetic tree is overwritten on every build (§2). → `design-headless-mcp`.
4. **Hand code.** Fill the stubs the build produced — node bodies, Closure/Rule bodies, `{BC}/Service/` — at the places of §10, using only Jardis artefacts. What the model cannot express is a missing Jardis capability: build it in the package or generator (when working on Jardis itself) or report it as an issue (when working on an app), then wait — no substitute in the project. → this skill, §5 and §6; recipes in `generated-code-recipes`.
5. **Tests.** Test behaviour through the doors, with the generated scaffold. → `foundation-testing` §6.
6. **QA.** Run the gates of the project profile, model checks first, then the gates. → `process-run-stage` §7.
