# Specifying with .specly

The complete language reference for writing SpecVerse specifications. Describe WHAT your system does; let the engines + manifests determine HOW it's built.

**See also:**
- [SPECVERSE-INTRO.md](SPECVERSE-INTRO.md) — philosophy and ecosystem overview
- [SPECVERSE-TOOLING.md](SPECVERSE-TOOLING.md) — CLI commands (validate / infer / realize / init / gen / dev / cache / ai / session / smoke)
- [SPECVERSE-REALIZING.md](SPECVERSE-REALIZING.md) — code generation, manifests, instance factories
- [SPECVERSE-EXTENDING.md](SPECVERSE-EXTENDING.md) — adding entity types, engines, factories, LLM providers

---

## Contents

### Part 1: Core Concepts
- [Your First Spec](#your-first-spec)
- [The Three Sections](#the-three-sections)
- [Convention Shorthand](#convention-shorthand)

### Part 2: Components
- [Models](#models) — attributes, metadata, profiles, relationships, lifecycles, constraints, behaviors
- [Controllers](#controllers) — CURVED operations and custom actions
- [Services](#services) — cross-model business logic
- [Events](#events) — domain events with typed payloads
- [The Event System](#the-event-system) — publish / subscribe
- [Views](#views) — UI specifications
- [Primitives](#primitives) — custom type definitions
- [Imports and Exports](#imports-and-exports)

### Part 3: Extension Entities
- [Commands](#commands) — CLI generation
- [Measures](#measures) — aggregation metrics
- [Conventions](#conventions) — shorthand definitions
- [Promotions](#promotions) — domain extension example
- [Distributions](#distributions) — packaging (IDE / CLI / npm / Docker)

### Part 4: Deployments
- [Deployment Structure](#deployment-structure)
- [Instance Types Reference](#instance-types-reference)
- [Operation Policies](#operation-policies)
- [Autoscaling and Resources](#autoscaling-and-resources)
- [Namespaces](#namespaces)

### Part 5: Examples
- [Personal Blog (simple)](#personal-blog)
- [E-Commerce (business scale)](#e-commerce)
- [Enterprise SaaS (enterprise scale)](#enterprise-saas)

### Part 6: Reference
- [Type Reference](#type-reference)
- [Convention Cheat Sheet](#convention-cheat-sheet)

---

## Your First Spec

A `.specly` file defines WHAT your system does, not HOW it's built:

```yaml
components:
  TaskManager:
    version: "1.0.0"
    description: "A simple task management system"

    models:
      Task:
        attributes:
          id: UUID required unique
          title: String required
          description: String
          priority: String values=[low,medium,high] default=medium
          dueDate: DateTime
        lifecycles:
          status:
            flow: todo -> in_progress -> done
```

That's a complete, valid spec. The inference engine generates the rest — controllers, services, events, views — from this minimal definition. See [SPECVERSE-REALIZING.md](SPECVERSE-REALIZING.md) for what inference expands this into.

## The Three Sections

Every `.specly` file has three top-level sections:

```yaml
components:        # WHAT the system does
  MyApp:
    models: ...
    controllers: ...
    services: ...
    events: ...
    views: ...

deployments:       # WHERE it runs
  development:
    instances: ...

manifests:         # HOW it's built (technology choices)
  implementation:
    capabilityMappings: ...
```

**components** define the logical architecture. **deployments** define runtime topology. **manifests** (or a separate manifest YAML file) map abstract capabilities to concrete technology implementations.

## Convention Shorthand

SpecVerse reduces boilerplate by 90% with shorthand conventions:

```yaml
# Attribute pattern: name: Type modifiers
email: Email required unique verified
name: String required
age: Integer min=0 max=150
price: Money min=0 default=9.99
status: String values=[active,inactive,suspended] default=active
tags: String[] optional                      # Array of strings
createdAt: DateTime auto=now
id: UUID auto=uuid4

# Relationship pattern: name: type Target modifiers
author: belongsTo User
posts: hasMany Post cascade
tags: manyToMany Tag through=PostTag
profile: hasOne Profile dependent

# Lifecycle shorthand
status:
  flow: draft -> published -> archived
```

Full [Convention Cheat Sheet](#convention-cheat-sheet) at the end.

---

## Models

Models are the foundation. Everything else is derived from them.

### Attributes

Attributes use convention shorthand — type followed by modifiers:

```yaml
models:
  Product:
    attributes:
      id: UUID required unique          # Type + boolean modifiers
      email: Email required unique       # Built-in Email type
      price: Money min=0 default=9.99   # Key=value modifiers
      status: String values=[a,b,c]     # Enum values
      tags: String[]                     # Array type
```

**Available types:** `String`, `Integer`, `Number`, `Boolean`, `UUID`, `DateTime`, `Date`, `Email`, `URL`, `Money`, `JSON`, plus custom primitives.

**Available modifiers:** `required`, `optional`, `unique`, `searchable`, `verified`, `default=`, `min=`, `max=`, `values=[]`, `auto=` (e.g. `auto=now`, `auto=uuid4`, `auto=autoincrement`, `auto=sequence`).

### Identity: who generates the value

`auto=` declares **who owns a field's value**:

| `auto=` | Meaning | Realize | Interpreter |
|---|---|---|---|
| `uuid4` | SpecVerse generates a UUID | `@default(uuid())` | generated |
| `autoincrement` | DB-assigned integer key | `@default(autoincrement())` | generated |
| `now` | timestamp at write | `@default(now())` | generated |
| *(none, on a field named `id`)* | primary-key convention → generated | `@default(uuid())` | generated |
| **`none` / `provided` / `external`** | **issued OUTSIDE SpecVerse — require it as input, never generate** | `@id` with **no default**, required | required, never synthesised |

`auto=none` (aka `provided` / `external`) is the **externally-owned / opaque key**: a primary key or id supplied by another system (e.g. a Firebase/Replit-Auth uid). It **overrides** the "a field named `id` is auto-generated" convention, so both the realized backend and the runtime interpreter require it as input rather than inventing a value. `spv ai analyse` infers it automatically when a Prisma/Drizzle source declares a primary key with no generator.

```yaml
User:
  attributes:
    id: String required auto=none   # the auth provider issues this id; we don't
    email: Email required unique
```

For complex cases use the explicit form:

```yaml
attributes:
  status:
    type: String
    required: true
    default: "active"
    values: ["active", "inactive", "suspended"]
```

### Metadata

Metadata generates synthetic fields automatically. Don't manually define fields that metadata generates.

```yaml
models:
  Post:
    metadata:
      id: uuid                     # Generates id field (uuid|integer|auto|composite|manual)
      audit:
        timestamps: true            # Adds createdAt, updatedAt
        users: true                 # Adds createdBy, updatedBy
      softDelete: true              # Adds deletedAt, isDeleted
      status: publishing            # Status from lifecycle name
      version: true                 # Optimistic locking counter
      label: [title]                # Display label field(s)
```

**Metadata options:**

| Option | Description | Generated fields |
|--------|-------------|------------------|
| `id` | ID generation strategy | `id` field with specified type |
| `label` | Display label field(s) | Used by UIs for entity display |
| `audit.timestamps` | Timestamp tracking | `createdAt`, `updatedAt` |
| `audit.users` | User tracking | `createdBy`, `updatedBy` |
| `softDelete` | Soft delete support | `deletedAt`, `isDeleted` |
| `status` | Status field | From lifecycle or explicit values |
| `version` | Optimistic locking | Version counter |

### Attribute Categories

Every attribute is auto-classified into one of three categories at parse time so consumers don't need name-based heuristics:

- **`business`** — user-facing domain data (title, price, status)
- **`metadata`** — synthetic / system fields (id, createdAt, updatedAt, version)
- **`relationship`** — foreign-key scalars for belongsTo relations

Forms hide `metadata` by default. List views colour-code `relationship` columns blue and `lifecycle` columns purple.

### Profiles

Profiles attach reusable configuration to models. Define a model as a profile with `profile-attachment`:

```yaml
models:
  Product:
    attributes:
      id: UUID required unique
      name: String required
      price: Money required

  DigitalProductProfile:
    description: "Profile for digital products"
    attributes:
      downloadUrl: String required
      fileSize: String
      license: String required
    profile-attachment:
      profiles: [Product]            # This profile attaches to Product

  PhysicalProductProfile:
    description: "Profile for physical products"
    attributes:
      weight: Number required
      dimensions: String
    profile-attachment:
      profiles: [Product]
```

A `Product` can have both a `DigitalProductProfile` and a `PhysicalProductProfile` attached simultaneously — composition without inheritance.

### Relationships

```yaml
relationships:
  orders: hasMany Order cascade         # One-to-many, cascade delete
  customer: belongsTo Customer          # Many-to-one (FK on this model)
  profile: hasOne Profile               # One-to-one
  tags: manyToMany Tag through=PostTag  # Many-to-many via junction
```

**Four relationship types:**

| Type | Meaning | Inverse | Example |
|------|---------|---------|---------|
| `hasMany` | One-to-many | `belongsTo` | `posts: hasMany Post` |
| `hasOne` | One-to-one | `belongsTo` | `profile: hasOne Profile` |
| `belongsTo` | Many-to-one | `hasMany`/`hasOne` | `author: belongsTo User` |
| `manyToMany` | Many-to-many | `manyToMany` | `tags: manyToMany Tag` |

**Modifiers:** `cascade`, `dependent`, `eager`, `lazy`, `optional`, `through=ModelName`.

### Lifecycles

State machines on models. The shorthand syntax:

```yaml
lifecycles:
  order:
    flow: draft -> submitted -> confirmed -> shipped -> delivered
```

Or detailed form:

```yaml
lifecycles:
  order:
    states: [pending, paid, processing, shipped, delivered, cancelled]
    transitions:
      pay: pending -> paid
      process: paid -> processing
      ship: processing -> shipped
      deliver: shipped -> delivered
      cancel: "* -> cancelled"       # from any state
```

Multiple independent lifecycles per model are supported:

```yaml
models:
  Article:
    lifecycles:
      editorial:
        flow: draft -> in_review -> approved
      publication:
        flow: unpublished -> scheduled -> published -> archived
      seo:
        flow: unoptimized -> optimized -> needs_review
```

Each lifecycle generates its own `evolve` operation, events (`{Model}Evolved`), and state-dependent UI (dropdowns, badges).

### Constraints

Runtime business rules that gate when an entity can be created, updated, deleted, or evolved. Constraints live on the model itself and fire across **every** CURVED operation that mutates it — not on a specific behavior.

```yaml
models:
  Vote:
    attributes:
      choice: String required
    relationships:
      voter: belongsTo User
      poll:  belongsTo Poll
    constraints:
      - on: [create]
        requires: "Poll is open"
      - on: [create, update]
        requires: "Vote's choice is set"
```

(For "one vote per (voter, poll)" use a compound `unique` — not a constraint; see [Compound uniqueness](#compound-uniqueness--unique-a-b) below.)

Each entry is `{on: [...ops], requires: '<predicate>'}`. The operation list accepts `create` / `update` / `delete` / `evolve` (or `evolve.<actionName>` for a specific transition). `requires:` is a single predicate string written in either **schema-layer form** or **natural-language sugar**.

**How constraints are enforced** — five surfaces, all driven from the same predicate:

| Mode | Surface | What it does |
|---|---|---|
| α | FK dropdown | Disables options that would violate a constraint — both single-FK rules (closed Polls disabled in Vote's poll picker) and **cross-relation FK-equality** (once a Poll is chosen, options from other Polls disable, scoping the dropdown to the selection) |
| γ | Preflight | `POST /api/<plural>/validate` fires before any mutation; failure surfaces in the form before the real request goes out |
| δ | Button disable | `+ Add <Related>`, Edit, Delete, Evolve buttons disable with the constraint's text as tooltip when the parent state would fail |
| ε | Error display | `FormViolationsPanel` (form-top) + inline `FieldError` (per-field) + `ValidationToast` (auto-dismiss) for server-returned violations |
| ζ | Pending checks | Informational blue panel listing constraints that need server-side context (subqueries, actor checks) — "Checked at submit time" |

#### Predicate vocabulary

The predicate is parsed top-down through nine **sugar conventions**. The first one whose pattern matches wins; if no sugar applies, you fall back to the **schema-layer form** (raw path-op-value).

| Convention | Pattern | Example input | Desugars to |
|---|---|---|---|
| `model_is_state` | `{Model} is {state}` | `"Poll is open"` | `self.poll.votingStatus == "open"` (auto-traverses belongsTo) |
| `actor_has_role` | `{Actor} has role:{role}` | `"User has role:admin"` | `actor.role == "admin"` |
| `actor_has_not_verb_on_target` | `{Actor} has not {verb} on {Target}` | `"User has not voted on Poll"` | `not Vote.exists(__v => __v.voter == actor and __v.poll == self.poll)` — **actor-based** (needs auth; compares to the logged-in user). For *data-level* "one row per combination" (e.g. one vote per voter+poll where `voter` is a field, not the actor), use a **compound `unique`** instead, not this sugar — see below. |
| `actor_is_self_relation` | `{Actor} is {Model}.{relation}` | `"User is Vote.voter"` | `self.voter == actor` (ownership) |
| `attr_is_set` | `{Model}'s {field} is set` | `"Vote's choice is set"` | `self.choice != null` |
| `path_op_value` | `{lhs} {op} {rhs}` | `"self.endDate > self.startDate"` | `self.endDate > self.startDate` (paths on both sides; either path may end in a **derived FK** like `self.option.pollId` — see cross-relation FK-equality below) |
| `lhs_in_list` | `{lhs} in {list}` | `'self.choice in ["yes", "no"]'` | `Set("yes", "no").contains(self.choice)` |
| `path_op_qstring` | `{path} {op} {"qstring"}` | `'self.poll.votingStatus == "open"'` | (identity — already schema-layer form) |
| `path_op_number` | `{path} {op} {number}` | `"self.viewCount > 100"` | (identity) |

Two special variables are always in scope:
- **`self`** — the entity being mutated (with relations traversed via dots: `self.poll.votingStatus`)
- **`actor`** — the authenticated user from `request.user`. Null when no auth middleware is wired; constraints that reference `actor.*` paths fail-OPEN (logged, treated as pass) until auth lands

#### Compound predicates

`and` / `or` / `not` compose the sugars or the schema-layer form:

```yaml
constraints:
  # Authors can only edit their own draft posts.
  - on: [update]
    requires: "User is Post.author and Post is draft"

  # Bypass for moderators.
  - on: [update, delete]
    requires: '(User is Post.author and Post is draft) or User has role:moderator'

  # Date sanity check.
  - on: [create, update]
    requires: "self.endDate > self.startDate"
```

#### Worked example: the Poll / Option / Vote pattern

```yaml
models:
  Poll:
    attributes:
      question: String required
    lifecycles:
      votingStatus:
        flow: "draft -> open -> closed"
    relationships:
      options: hasMany Option

  Option:
    attributes:
      label: String required
    relationships:
      poll: belongsTo Poll

  Vote:
    # One vote per (voter, poll) — a COMPOUND UNIQUE on the FK columns. This is
    # DATA-level uniqueness (a DB unique index), NOT a validate-centric guard:
    # a "has this voter already voted?" pre-check is a TOCTOU race in a
    # multi-user world. See "Compound uniqueness" below.
    unique: [[voterId, pollId]]
    relationships:
      voter:  belongsTo User
      poll:   belongsTo Poll
      option: belongsTo Option
    constraints:
      - on: [create]
        requires: "Poll is open"                        # mode α/δ/γ/ε
      - on: [create]
        requires: "self.option.pollId == self.pollId"   # the chosen option must belong to the selected poll
```

At runtime:
- Vote's poll picker only shows Polls in `open` state (α)
- Once a Poll is chosen, the **option** picker disables options that belong to a *different* poll — `self.option.pollId == self.pollId` scopes the FK dropdown (α). Before a poll is picked, all options are selectable.
- "+ Add Vote" on a closed Poll is disabled (δ)
- A duplicate `(voter, poll)` is rejected **atomically by the DB unique index** (`@@unique([voterId, pollId])`); the poll-open + chosen-option guards fire at write time (γ/ε)

> **`self.option.pollId`** is a *derived FK* — `belongsTo poll` exposes a `pollId` column even though it isn't a declared attribute. Constraint paths may reference these (`self.<rel>.<fk>Id`) as a terminal segment, which is how cross-relation FK-equality (one record's parent must match another's) is expressed. Enforced server-side in both the interpreter and the realized backend.

#### Constraints vs behavioral `requires:` — they are different

`model.constraints[].requires:` (this section) gates **CURVED operations on the model itself**. It runs automatically across create/update/delete/evolve.

`behaviors.<name>.requires:` (next section) is a **precondition on one specific behavior** — declarative documentation that doesn't yet auto-enforce. The two share the word `requires` but live at different levels: behaviors are operation contracts; constraints are model invariants.

Rule of thumb: if the rule should hold across **every** way the entity could change, write it as a model constraint. If it only matters for a specific operation, write it as a behavior's `requires:`.

#### Author errors fail loud — broken constraints block spec load

If a constraint references something that doesn't resolve — typo'd field, missing lifecycle, unknown sugar shape — the parser produces a hard error and the spec refuses to load. For example, this spec:

```yaml
Vote:
  constraints:
    - on: [create]
      requires: "self.poll.deletedAt == null"   # Poll has no deletedAt
```

…fails immediately with:

```
Component 'PollSystem', model 'Vote': could not expand constraint
requires="self.poll.deletedAt == null" — no matching convention or invalid
path (check that all referenced models/attributes/relationships exist).
```

This is intentional. Silently shipping a constraint that does nothing is the worst failure mode — author thinks the rule is enforced when it isn't. Fix the typo, add the missing field, or remove the constraint before the spec loads.

### Compound uniqueness — `unique: [[a, b]]`

Some rules are about **uniqueness across rows**, not one record's state — e.g. *"one vote per (voter, poll)"*. These are **not** validate-centric constraints. A "does a row with this combination already exist?" pre-check followed by an insert is a **TOCTOU race** in a multi-user world: two concurrent requests both pass the check, then both write — exactly the duplicate the rule meant to prevent. The only correct enforcement is a **database unique index**, which makes the check-and-write atomic.

Declare it at the model level:

```yaml
Vote:
  unique: [[voterId, pollId]]      # at most one row per (voterId, pollId)
  relationships:
    voter: belongsTo User
    poll:  belongsTo Poll
```

Each inner array is a tuple of column names that must be unique *together*. Single-column uniqueness stays at the attribute level (`email: Email required unique`). Reference a belongsTo FK by its derived `<rel>Id` column name (`voterId`, `pollId`).

- **Realized backend:** a real DB unique index — Prisma `@@unique([voterId, pollId])` / SQL `UNIQUE (voterId, pollId)` — atomic, race-safe.
- **app-demo interpreter:** mirrors the invariant with an in-memory check, so the demo rejects the same duplicates (Define-Once).

> Note the distinction from `voter` being a *field* vs the *actor*: "one vote per voter+poll" is data-level (`unique` over the `voterId`/`pollId` columns), independent of who is logged in. The `{Actor} has not {verb} on {Target}` sugar is for genuinely *actor*-scoped rules (the authenticated user), which need auth wired.
>
> **Rule of thumb:** per-row / contextual rules → a `constraint`; cross-row invariants (uniqueness, "at most N of X") → DB-atomic (`unique`), never a read-then-write guard.

#### Lifecycles imply attributes (you don't declare both)

Declaring a `lifecycles:` block on a model auto-synthesizes a corresponding attribute with the lifecycle name + a `<select>` of valid states:

```yaml
Poll:
  attributes:
    question: String required
  lifecycles:
    votingStatus:
      flow: "draft -> open -> closed"
  # ↑ No need to declare `votingStatus: String values=[draft,open,closed]`
  # in attributes — the parser synthesizes it from the lifecycle.
```

The synthesized attribute is `category: 'lifecycle'`, which the runtime form-fields renderer uses to:
- **Hide the input on Create** (each lifecycle always starts at its declared `initialState`; user doesn't choose)
- **Show the input on Update** for error-correction (any state → any state, bypassing the transition map)
- **Show the dropdown on the Evolve tab** with only valid next states (transition-map-validated path)

Constraint guards reading `self.<lifecycleName>` see the current state value normally.

### Behaviors

Declarative business logic with contracts:

```yaml
behaviors:
  calculateTotal:
    description: "Calculate order total from line items"
    parameters:
      discountRate: Number min=0 max=1
    returns: Money
    requires: ["Order has at least one item", "discountRate is valid"]
    ensures: ["Total reflects all items minus discount"]
    publishes: [OrderTotalCalculated]

  processPayment:
    parameters:
      paymentMethod: String required values=[card,bank,crypto]
      amount: Money required
    requires: ["Amount matches order total", "Payment method is valid"]
    ensures: ["Payment recorded", "Order status updated"]
    publishes: [PaymentProcessed]
    steps:
      - "Validate payment details"
      - "Charge payment provider"
      - "Record transaction"
      - "Update order status"
```

- `requires` — preconditions (must be true before execution)
- `ensures` — postconditions (guaranteed after execution)
- `publishes` — domain events emitted on success
- `steps` — ordered execution steps for complex workflows

How behaviors become TypeScript at realize time is covered in [SPECVERSE-REALIZING.md](SPECVERSE-REALIZING.md) (the L1/L2/L3 walkthrough).

---

## Controllers

Define API endpoints. Usually inferred from models, but can be explicit:

```yaml
controllers:
  ProductController:
    model: Product
    path: "/api/products"
    description: "Product management API"

    cured:
      create:
        description: "Create new product"
        publishes: ProductCreated
      retrieve:
        description: "Get single product"
      retrieve_many:
        description: "List products"
        parameters:
          limit: Integer default=10 max=100
          offset: Integer default=0
      update:
        description: "Update product"
        publishes: ProductUpdated
      evolve:
        description: "Change product state"
        publishes: ProductStateChanged
      delete:
        description: "Delete product"
        publishes: ProductDeleted

    actions:
      restock:
        description: "Restock product"
        parameters:
          quantity: Integer required min=1
        returns: Product
        requires: ["Product exists"]
        ensures: ["Stock increased by quantity"]
        publishes: [ProductRestocked]
```

### CURVED Operations

The standard six operations on a model — **CURVED**, not CRUD:

| Operation | HTTP | Purpose |
|-----------|------|---------|
| **C**reate | `POST /resource` | Instantiate new entity |
| **U**pdate | `PUT /resource/:id` | Full replace with validation |
| **R**etrieve | `GET /resource/:id` + `GET /resource` | Fetch one or many |
| **V**alidate | `POST /resource/validate` | Check business rules without persisting |
| **E**volve | `PATCH /resource/:id/evolve` | Lifecycle state transitions |
| **D**elete | `DELETE /resource/:id` | Remove entity |

Validate is a dry-run endpoint. Evolve enforces lifecycle state machines from the spec. This is what distinguishes SpecVerse from an ORM scaffolder.

**Custom actions** sit alongside CURVED at `POST /resource/:id/<actionName>`. Generated controllers publish typed events (`${Model}Created` / `Updated` / `Deleted` / `Evolved`) for every CURVED operation that mutates state.

---

## Services

Business logic that spans multiple models:

```yaml
services:
  OrderFulfillmentService:
    description: "Coordinates order processing across models"
    subscribes_to:
      InventoryReservationRequested: reserveStock
      OrderCancelled: releaseStock

    operations:
      fulfillOrder:
        parameters:
          orderId: UUID required
        returns: Boolean
        requires: ["Order exists and is confirmed"]
        ensures: ["Inventory reserved", "Shipping label created"]
        publishes: [OrderFulfilled]

      reserveStock:
        parameters:
          orderId: UUID required
          items: Array required
        ensures: ["Stock reserved for all items"]
        publishes: [StockReserved]
```

Services **subscribe** to events via `subscribes_to` (event → handler method) and **publish** new events from their operations.

---

## Events

Domain events with typed payloads:

```yaml
events:
  OrderPlaced:
    description: "Customer placed a new order"
    attributes:
      orderId: UUID required
      customerId: UUID required
      total: Money required
      itemCount: Integer required
      timestamp: DateTime auto=now
```

Events are usually inferred from model behaviors and controller actions. Define them explicitly when you need custom payload shapes.

## The Event System

Events connect components of your system. The pattern is **publish / subscribe** — behaviors and controllers publish events, services subscribe to handle them.

**Publishing** — declare what events an operation emits:

```yaml
# In a controller action or service operation:
behaviors:
  confirmOrder:
    requires: ["Order is pending", "Payment verified"]
    ensures: ["Order confirmed"]
    publishes: [OrderConfirmed, InventoryReservationRequested]
```

**Subscribing** — services declare which events they handle:

```yaml
services:
  InventoryService:
    subscribes_to:
      InventoryReservationRequested: reserveStock
      OrderCancelled: releaseStock
```

**Event chains** — events triggering further events create reactive workflows:

```
Customer places order
  → OrderPlaced event
    → PaymentService.processPayment subscribes
      → PaymentProcessed event
        → OrderService.confirmOrder subscribes
          → OrderConfirmed event
            → InventoryService.reserveStock subscribes
              → StockReserved event
                → NotificationService.sendConfirmation subscribes
```

The inference engine wires this automatically from relationships and behaviors. The realize engine produces an EventEmitter bus (development) or targets RabbitMQ / Kafka / NATS (production) via different instance factories.

**What gets generated** for the event system:
- Event type definitions with typed payloads
- Publisher methods on controllers and services
- Subscriber registration on service initialization
- Event bus infrastructure (EventEmitter or message queue)

---

## Views

UI specifications:

```yaml
views:
  ProductCatalog:
    description: "Product browsing and search"
    model: Product
    type: list

  ProductDetail:
    description: "Single product view"
    model: Product
    type: detail

  ProductForm:
    description: "Create/edit product"
    model: Product
    type: form

  OrderDashboard:
    description: "Order management dashboard"
    type: dashboard
    models: [Order, OrderItem]   # Multi-model views use `models:` array

  OrderPipeline:
    description: "Kanban board for order fulfillment"
    type: board
    model: Order
    groupBy: status
```

**View types:** `list`, `detail`, `form`, `dashboard`, `board`, `timeline`, `calendar`, `workflow`, `wizard`, `comparison`, `settings`, `map`, `feed`, `profile`, `custom`.

The realize engine generates React components from view specs. List views render sortable tables with FK links; detail views render attribute grids with lifecycle dropdowns; form views render typed inputs with FK dropdowns.

Views can declare UI structure via `uiComponents`:

```yaml
views:
  ProductListView:
    model: Product
    type: list
    uiComponents:                # UI composition
      searchBar: { type: search, target: [name, sku] }
      filterPanel: { type: filter, fields: [category, status] }
      productTable: { type: table, columns: [name, sku, price, status] }
      actionButtons: { type: actions, items: [create, export] }
```

---

## Primitives

Define custom reusable data types:

```yaml
primitives:
  # Shorthand
  PhoneNumber: String pattern="^\\+[1-9]\\d{1,14}$"

  # Explicit
  Money:
    baseType: Number
    validation:
      min: 0
      max: 1000000
    description: "Monetary amount"

  Status:
    baseType: String
    validation:
      values: ["pending", "active", "suspended", "deleted"]
```

SpecVerse ships with built-in primitives: `Money`, `Email`, `URL`, `PhoneNumber`, `Address`. Import them explicitly:

```yaml
import:
  - from: "@specverse/primitives"
    select: [Money, Email]
```

## Imports and Exports

Share types and models between specs:

```yaml
components:
  MyApp:
    import:
      - from: "@specverse/primitives"
        select: [Money, Email]
      - from: "./shared-models"
        select: [Address, PhoneNumber]

    export:
      models: [Customer, Order]
      events: [OrderPlaced]
      primitives: [CustomType]
```

Imports can also pull from the community registry (`@specverse/auth`, `@specverse/commerce`, `@specverse/rest-api`, etc.) — see specverse-lang-registry.

---

## Extension Entities

Beyond the 6 core entities (models, controllers, services, events, views, deployments), SpecVerse ships 5 extension entities:

### Commands

Define CLI commands directly in your spec. Realize generates a full Commander.js CLI:

```yaml
components:
  CLI:
    commands:
      mytool:
        description: "My application CLI"
        subcommands:
          deploy:
            description: "Deploy the application"
            arguments:
              environment:
                type: String
                required: true
                positional: true
            flags:
              --dry-run:
                type: Boolean
                default: false
              --region:
                type: String
                alias: "-r"
                default: "us-east-1"
            returns: DeploymentResult
            exitCodes:
              0: Success
              1: Deployment failed
```

Generates: CLI entry point with Commander.js, typed argument/flag parsing, subcommand registration, help text from descriptions.

### Measures

Define analytics and aggregation metrics:

```yaml
measures:
  totalRevenue:
    source: Order
    aggregation: sum
    field: total
    filter: "status = 'completed'"
    dimensions: [region, productCategory]
    format: currency

  activeUsers:
    source: User
    aggregation: count
    filter: "lastLogin > now() - 30d"
    dimensions: [plan, country]

  averageOrderValue:
    source: Order
    aggregation: avg
    field: total
    dimensions: [month]
```

**Aggregation types:** `sum`, `count`, `avg`, `min`, `max`, `custom`.

### Conventions

Define how shorthand syntax expands. This is the meta-circular entity — conventions defining how conventions work:

```yaml
conventions:
  attributeShorthand:
    pattern: "{name}: {type} {modifiers}"
    expansion:
      name: "{name}"
      type: "{type}"
      required: "contains(modifiers, 'required')"
      unique: "contains(modifiers, 'unique')"

  relationshipShorthand:
    pattern: "{name}: {relType} {target} {modifiers}"
    expansion:
      name: "{name}"
      type: "{relType}"
      target: "{target}"
      cascade: "contains(modifiers, 'cascade')"
```

### Promotions

Domain-specific extension example (e-commerce promotions):

```yaml
promotions:
  summerSale:
    description: "20% off all electronics"
    discountType: percentage
    discountValue: 20
    conditions:
      category: electronics
      minOrderValue: 50
    validFrom: "2026-06-01"
    validTo: "2026-08-31"
```

**Discount types:** `percentage`, `fixed`, `buyXgetY`, `freeShipping`, `bundle`.

### Distributions

Describe WHAT to distribute — IDE extensions, CLI tools, npm packages, containers:

```yaml
distributions:
  MyAppIDE:
    description: "Language support for editors"
    type: ide
    displayName: "MyApp"
    publisher: myorg
    languages:
      - id: myapp
        extensions: [.myapp]
        grammar: source.myapp
    commands:
      - from: CLI.validate
      - from: CLI.build
    themes:
      - name: "MyApp Dark"
        type: dark
```

**Distribution types:** `ide`, `cli`, `npm`, `docker`, `homebrew`.

The VSCode factory reads the distribution spec to generate a `.vsix` extension with commands, themes, and language grammar. The MCP factory reads service operations to generate tool definitions.

---

## Deployments

Deployments bridge the logical specification to the physical infrastructure.

### Deployment Structure

```yaml
deployments:
  production:
    version: "1.0.0"
    environment: production
    instances:
      # Eight instance categories:
      controllers: {...}      # API servers
      services: {...}         # Background workers
      views: {...}            # Frontend apps
      communications: {...}   # Event buses, message queues
      storage: {...}          # Databases, caches
      security: {...}         # Auth, encryption
      infrastructure: {...}   # Load balancers, CDN, DNS
      monitoring: {...}       # Metrics, logging, tracing
```

### Instance Types Reference

#### Controllers / Services / Views

```yaml
controllers:
  api-server:
    component: "ComponentName"
    namespace: "api"
    advertises: "*"                   # or ["capability1", "capability2"]
    uses: ["database.*", "cache.*"]
    scale: 3
    config:
      port: 8080
      timeout: 30
```

#### Storage

```yaml
storage:
  main-db:
    component: "ComponentName"
    type: "relational"                # relational | document | keyvalue | cache | file | blob | queue | search
    provider: "postgresql"
    persistence: "durable"            # durable | session | cache | temporary
    consistency: "strong"             # strong | eventual | weak
    scale: 2
    replication: 2
    backup: true
    encryption: true
```

#### Security

```yaml
security:
  auth-system:
    component: "ComponentName"
    type: "authentication"            # authentication | authorization | encryption | audit | firewall | scanning | secrets | identity
    provider: "oauth"                 # oauth | saml | jwt | ldap | local | external | cloud | enterprise
    scope: "global"                   # global | component | namespace | instance | user | role
    policies: ["mfa", "session-timeout"]
    protocols: ["oauth2", "openid"]
    encryption: "strong"              # none | basic | strong | enterprise
    auditLevel: "detailed"            # none | basic | detailed | comprehensive
```

#### Infrastructure

```yaml
infrastructure:
  load-balancer:
    component: "ComponentName"
    type: "loadbalancer"              # gateway | loadbalancer | proxy | cdn | dns | registry | mesh | ingress
    provider: "nginx"                 # aws | gcp | azure | cloudflare | vercel | netlify | kubernetes | istio | envoy | nginx | traefik | consul | local
    tier: "regional"                  # edge | regional | global | local
    redundancy: "high"                # none | basic | high | enterprise
    protocols: ["http", "https", "websocket"]
    endpoints: ["api.example.com", "ws.example.com"]
    healthChecks: true
    autoScaling: true
```

#### Monitoring

```yaml
monitoring:
  metrics-system:
    component: "ComponentName"
    type: "metrics"                   # metrics | logging | tracing | alerting | analytics | profiling | uptime | synthetic
    provider: "prometheus"            # prometheus | grafana | datadog | newrelic | splunk | elasticsearch | jaeger | zipkin | sentry | rollbar | cloudwatch | stackdriver | azure-monitor | local
    scope: "component"                # global | component | namespace | instance | service | request
    retention: "medium"               # short | medium | long | permanent
    resolution: "high"                # high | medium | low
    sampling: 1.0                     # 0.0 to 1.0
    dashboards: ["overview", "performance", "errors"]
    alerts: ["high-cpu", "low-memory", "error-rate"]
    aggregation: true
    realtime: false
```

#### Communications

```yaml
communications:
  event-bus:
    namespace: "global"
    capabilities: ["*"]
    type: "pubsub"                    # pubsub | streaming | rpc | queue
    config:
      broker: "redis"
      retention: "24h"
```

### Operation Policies

Apply resilience patterns to specific operations:

```yaml
deployments:
  production:
    instances:
      controllers:
        api:
          component: MyApp
          operationPolicies:
            processPayment:
              retry:
                enabled: true
                maxAttempts: 3
                backoffMs: 100
                retryableErrors: ["TIMEOUT", "UNAVAILABLE"]
              circuitBreaker:
                enabled: true
                failureThreshold: 5
                halfOpenAfterMs: 60000
            listProducts:
              rateLimit:
                enabled: true
                requestsPerMinute: 1000
                burstSize: 100
                byIP: true
              cache:
                enabled: true
                ttlSeconds: 300
                keyFields: ["category", "limit", "offset"]
```

**Available policies:** `retry`, `circuitBreaker`, `rateLimit`, `cache`, `transactional`, `idempotency`.

### Autoscaling and Resources

```yaml
deployments:
  production:
    instances:
      controllers:
        api:
          component: MyApp
          scale: 3                    # initial replica count
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "512Mi"
          autoscaling:
            enabled: true
            minReplicas: 2
            maxReplicas: 10
            targetCPU: 80
```

### Namespaces

Domain isolation:

```yaml
deployments:
  production:
    namespacing:
      strategy: "domain-based"        # single | domain-based | environment-based | tenant-based
      namespaces:
        products:
          isolation: "strict"
          resourceLimits:
            cpu: "1000m"
            memory: "2Gi"
        orders:
          isolation: "permissive"
      crossNamespacePolicy: "explicit"
```

---

## Examples

Three end-to-end examples at different scales, each showing `components:` + `deployments:` together.

### Personal Blog

Simple scale — SQLite, single instance:

```yaml
components:
  PersonalBlog:
    version: "1.0.0"
    description: "Personal blogging platform"

    models:
      Post:
        attributes:
          title: String required
          slug: String required unique
          content: String required
          publishedAt: DateTime optional
          tags: String[] optional
        lifecycles:
          status:
            flow: draft -> published -> archived

deployments:
  personal:
    version: "1.0.0"
    environment: development
    instances:
      controllers:
        blog-api:
          component: "PersonalBlog"
          scale: 1
      storage:
        sqlite-db:
          component: "PersonalBlog"
          type: "relational"
          provider: "sqlite"
          persistence: "durable"
```

### E-Commerce

Business scale — PostgreSQL, Redis cache, JWT auth:

```yaml
components:
  ECommerce:
    version: "1.0.0"
    description: "Online shopping platform"

    models:
      Product:
        attributes:
          name: String required searchable
          sku: String required unique
          price: Money required currency=USD
          inventory: Integer required min=0
          category: String required
        relationships:
          reviews: hasMany Review
          orders: manyToMany Order through=OrderItem
        lifecycles:
          availability:
            flow: draft -> available -> outOfStock -> discontinued

      Order:
        attributes:
          orderNumber: String required unique auto=sequence
          total: Money required currency=USD
          status: String required
        relationships:
          customer: belongsTo Customer
          items: hasMany OrderItem cascade
        lifecycles:
          fulfillment:
            states: [pending, paid, processing, shipped, delivered, cancelled]
            transitions:
              pay: pending -> paid
              process: paid -> processing
              ship: processing -> shipped
              deliver: shipped -> delivered
              cancel: "* -> cancelled"

deployments:
  production:
    version: "1.0.0"
    environment: production
    instances:
      controllers:
        api-gateway:
          component: "ECommerce"
          scale: 3
          advertises: "api.*"
      services:
        order-processor:
          component: "ECommerce"
          scale: 2
          advertises: "orders.*"
      storage:
        postgres-db:
          component: "ECommerce"
          type: "relational"
          provider: "postgresql"
          persistence: "durable"
          consistency: "strong"
          scale: 2
          backup: true
        redis-cache:
          component: "ECommerce"
          type: "keyvalue"
          provider: "redis"
          persistence: "cache"
      security:
        auth-service:
          component: "ECommerce"
          type: "authentication"
          provider: "jwt"
          scope: "global"
```

### Enterprise SaaS

Enterprise scale — multi-tenant, SSO, CDN, comprehensive monitoring:

```yaml
components:
  EnterpriseSaaS:
    version: "1.0.0"
    description: "Multi-tenant SaaS platform"

    import:
      - from: "@specverse/primitives"
        select: [Money, Address, PhoneNumber]

    models:
      Tenant:
        attributes:
          name: String required
          subdomain: String required unique
          plan: String required values=["starter","professional","enterprise"]
          seats: Integer required min=1
          billingEmail: Email required
        relationships:
          users: hasMany User cascade
          subscription: hasOne Subscription
        lifecycles:
          account:
            states: [trial, active, suspended, cancelled]
            transitions:
              activate: trial -> active
              suspend: active -> suspended
              reactivate: suspended -> active
              cancel: "* -> cancelled"

      User:
        profiles: [Auditable, Taggable]
        attributes:
          email: Email required unique
          name: String required
          role: String required values=["admin","manager","member","readonly"]
        relationships:
          tenant: belongsTo Tenant
          permissions: manyToMany Permission through=UserPermission
        behaviors:
          hasPermission:
            parameters:
              permission: String required
            returns: Boolean
            steps:
              - "Check user role permissions"
              - "Check explicit permissions"
              - "Apply tenant-level overrides"

deployments:
  enterprise:
    version: "1.0.0"
    environment: production
    instances:
      controllers:
        api-gateway:
          component: "EnterpriseSaaS"
          scale: 10
          advertises: "api.*"
      services:
        tenant-manager:
          component: "EnterpriseSaaS"
          scale: 5
          advertises: "tenants.*"
        billing-service:
          component: "EnterpriseSaaS"
          scale: 3
          advertises: "billing.*"
      storage:
        primary-db:
          component: "EnterpriseSaaS"
          type: "relational"
          provider: "postgresql"
          persistence: "durable"
          consistency: "strong"
          scale: 5
          replication: 2
          backup: true
          encryption: true
        cache-cluster:
          component: "EnterpriseSaaS"
          type: "keyvalue"
          provider: "redis"
          persistence: "cache"
          scale: 3
      security:
        sso-auth:
          component: "EnterpriseSaaS"
          type: "authentication"
          provider: "enterprise"
          scope: "global"
          policies: ["sso", "mfa", "compliance"]
          protocols: ["saml", "oauth2", "openid"]
          encryption: "enterprise"
          auditLevel: "comprehensive"
      monitoring:
        apm:
          component: "EnterpriseSaaS"
          type: "metrics"
          provider: "datadog"
          scope: "global"
          retention: "long"
          resolution: "high"
          dashboards: ["executive", "operations", "technical"]
          alerts: ["sla-breach", "high-error-rate", "security-event"]
      infrastructure:
        cdn:
          component: "EnterpriseSaaS"
          type: "cdn"
          provider: "cloudflare"
          tier: "global"
          redundancy: "enterprise"
```

---

## Type Reference

### Built-in Primitive Types

| Type | Description | Example |
|------|-------------|---------|
| `String` | Text data | `name: String required` |
| `Integer` | Whole numbers | `age: Integer min=0 max=150` |
| `Number` | Decimal numbers | `price: Number min=0.01` |
| `Boolean` | True/false | `active: Boolean default=true` |
| `UUID` | Unique identifier | `id: UUID auto=uuid4` |
| `Email` | Email address | `email: Email required unique` |
| `URL` | Web address | `website: URL optional` |
| `DateTime` | Date and time | `createdAt: DateTime auto=now` |
| `Date` | Date only | `birthDate: Date required` |
| `Money` | Monetary amount | `price: Money currency=USD` |
| `JSON` | Arbitrary JSON | `metadata: JSON optional` |

### Array Types

Any type can be made into an array by adding `[]`:

| Array type | Example |
|------------|---------|
| `String[]` | `tags: String[] optional` |
| `Integer[]` | `scores: Integer[] required` |
| `UUID[]` | `relatedIds: UUID[] optional` |

### Common Import Types

From `@specverse/primitives`: `Money`, `Address`, `PhoneNumber`, `PersonName`, `ContactInfo`, `AuditFields`.

Community libraries from `specverse-lang-registry`: `@specverse/auth` (User, AuthController), `@specverse/commerce` (Product, Order, OrderItem), `@specverse/rest-api`, `@specverse/event-driven`, domain packs.

---

## Convention Cheat Sheet

```yaml
# Attributes
fieldName: Type                        # Basic
fieldName: Type required               # Required
fieldName: Type required unique        # Required + unique
fieldName: Type default=value          # With default
fieldName: Type values=[a,b,c]         # Enum
fieldName: Type min=0 max=100          # Bounded
fieldName: Type[]                      # Array
fieldName: Type auto=now               # Auto-generated
fieldName: Type pattern="regex"        # Pattern validation

# Relationships
name: hasMany Target                   # One-to-many
name: hasMany Target cascade           # With cascade delete
name: belongsTo Target                 # Many-to-one
name: hasOne Target dependent          # One-to-one
name: manyToMany Target through=Join   # Many-to-many

# Lifecycles
flow: state1 -> state2 -> state3       # Linear flow

states: [a, b, c]                      # Explicit states
transitions:
  verbName: a -> b                     # Named transition
  cancel: "* -> cancelled"             # From any state

# Metadata
metadata:
  id: uuid                             # ID strategy
  label: [fieldName]                   # Display field
  audit: { timestamps: true }          # Auto-timestamps
  softDelete: true                     # Soft delete support
  version: true                        # Optimistic locking
```

### CURVED Operations Summary

| Operation | HTTP | Purpose |
|-----------|------|---------|
| Create | `POST /resource` | Instantiate new entity |
| Update | `PUT /resource/:id` | Full replace with validation |
| Retrieve | `GET /resource/:id` + `GET /resource` | Fetch one or many |
| Validate | `POST /resource/validate` | Dry-run business rule check |
| Evolve | `PATCH /resource/:id/evolve` | Lifecycle state transition |
| Delete | `DELETE /resource/:id` | Remove entity |

### File Structure Template

```yaml
components:
  ComponentName:
    version: "1.0.0"
    description: "..."
    import: [...]
    export: [...]
    primitives: { ... }
    models: { ... }
    controllers: { ... }
    services: { ... }
    events: { ... }
    views: { ... }
    commands: { ... }           # extension
    measures: { ... }           # extension
    conventions: { ... }        # extension

deployments:
  name:
    environment: development
    instances: { ... }

manifests:                      # or in separate manifest YAML
  name:
    deployment: { ... }
    instanceFactories: [...]
    capabilityMappings: [...]
```

---

## Related

- [SPECVERSE-TOOLING.md](SPECVERSE-TOOLING.md) — validate / infer / realize the specs you write
- [SPECVERSE-REALIZING.md](SPECVERSE-REALIZING.md) — how your `.specly` becomes working code (manifests + instance factories + L1/L2/L3)
- [SPECVERSE-EXTENDING.md](SPECVERSE-EXTENDING.md) — add a new entity type when core + extension entities don't cover your domain
- [SPECVERSE-ARCHITECTURE.md](SPECVERSE-ARCHITECTURE.md) — internal system architecture (parse → infer → realize pipeline)
