# Extending SpecVerse

How to extend SpecVerse with a new entity type, a new engine, a new instance factory, or a new LLM provider. Plus Quint formal verification and behavioural conventions.

**See also:**
- [SPECVERSE-INTRO.md](SPECVERSE-INTRO.md) — philosophy and ecosystem overview
- [SPECVERSE-SPECIFYING.md](SPECVERSE-SPECIFYING.md) — current entity types available in `.specly`
- [SPECVERSE-REALIZING.md](SPECVERSE-REALIZING.md) — instance factory system and generation levels
- [SPECVERSE-ARCHITECTURE.md](SPECVERSE-ARCHITECTURE.md) — internal system architecture

**Worked examples — extension entity types in `.specly`.** The **Domain Extensions** examples show the
per-spec *payoff* of adding a custom entity type — what your users write once your extension ships:

- *Basic CLI* (`13-01`) — a `commands:` (CLI) spec
- *Basic Conventions* (`13-02`) — custom `conventions:` that extend the grammar
- *Basic Measures* (`13-03`) — `measures:` (aggregations) over a model
- *Promotions* (`13-04`) — a custom domain entity type
- *IDE Distribution* (`13-05`) — a `distributions:` (IDE/VSCode packaging) spec

Find them under the **Domain Extensions** category in the examples. (These show extensions *in use*;
authoring the extension itself is the step-by-step below.)

---

## Contents

- [When to Use Which Extension Path](#when-to-use-which-extension-path)
- [Adding a New Entity Type](#adding-a-new-entity-type)
- [Adding a New Engine](#adding-a-new-engine)
- [Adding a New Composition Pipeline](#adding-a-new-composition-pipeline)
- [Adding a New Instance Factory](#adding-a-new-instance-factory)
- [Adding a New LLM Provider](#adding-a-new-llm-provider)
- [Formal Verification with Quint](#formal-verification-with-quint)
- [Behavioural Conventions](#behavioural-conventions)

---

## When to Use Which Extension Path

Five orthogonal extension points. Pick based on what you're adding:

```mermaid
flowchart TD
    Q{What do you want to add?}

    Q -->|New concept expressible in `.specly`| ET[Add Entity Type]
    Q -->|New technology target| IF[Add Instance Factory]
    Q -->|New pipeline stage| EN[Add Engine]
    Q -->|New tree-to-artifact transformation| CP[Add Composition Pipeline]
    Q -->|New AI model integration| LP[Add LLM Provider]

    ET --> ET1[Create module in entities/src/]
    ET1 --> ET2[Implement 9 facets]
    ET2 --> ET3[Register in _bootstrap.ts]

    IF --> IF1[Create YAML definition in engines/libs/instance-factories/]
    IF1 --> IF2[Write generator templates]
    IF2 --> IF3[Map capabilities in manifest]

    EN --> EN1[Pick Path A: subpath in @specverse/engines]
    EN --> EN2[Or Path B: sibling workspace package]
    EN1 --> EN3[Explicit registration via EngineRegistry.register]
    EN2 --> EN3

    CP --> CP1[Write walker reading facet files]
    CP1 --> CP2[Deterministic composer → tracked artifact]
    CP2 --> CP3[Document in architecture guide]

    LP --> LP1[Implement LanguageModelV3 from @ai-sdk/provider]
    LP1 --> LP2[Add doGenerate and doStream]
    LP2 --> LP3[Wire into model-resolver switch]
```

| Path | When | Example |
|------|------|---------|
| **Entity Type** | New kind of thing in `.specly` files | `workflows`, `policies`, `metrics` |
| **Instance Factory** | New technology target | Express, MongoDB, Vue |
| **Engine** | New pipeline stage | Test runner, migration engine, linter |
| **Composition Pipeline** | New tree → composed-artifact transformation | OpenAPI aggregate, dependency graph, coverage report |
| **LLM Provider** | New AI model integration | Gemini, Ollama, local model |

### Engines vs Entity Modules vs Instance Factories (clarification)

These three concepts are often confused:

| Concept | What it is | Example | When to add one |
|---|---|---|---|
| **Engine** | A pipeline stage (currently: parser, inference, realize, generators, ai, registry) | `@specverse/engines/parser` | You need a fundamentally new stage (a test runner engine, a migration engine) |
| **Entity Module** | A self-contained definition of a spec element type | `models`, `views`, `promotions` | You need a new kind of thing in `.specly` files |
| **Instance Factory** | A code generator template for a specific technology | `fastify`, `prisma`, `react-app-runtime` | You need to target a new technology |

**Engines** are pipeline stages — each does one job:

| Engine | Role | Input | Output |
|---|---|---|---|
| `@specverse/entities` | Define what entity types exist and how they behave | Entity module registrations | Convention processors, schema fragments, inference rules |
| `@specverse/engines/parser` | Read `.specly` files | Raw text | `SpecVerseAST` (validated, conventions expanded) |
| `@specverse/engines/inference` | Generate architecture from minimal specs | AST with models | AST with controllers, services, events, views, deployments |
| `@specverse/engines/realize` | Generate production code | AST + manifest | Source files (backend, frontend, CLI, tools) |
| `@specverse/engines/generators` | Generate diagrams and docs | AST | Mermaid diagrams, markdown docs, UML |
| `@specverse/engines/ai` | Build prompts, execute LLMs, orchestrate workflows | AST or requirements | Prompts, suggestions, generated specs |
| `@specverse/engines/registry` | Explicit engine discovery | Registered engines | Capability → engine lookup |

---

## Adding a New Entity Type

When your domain needs a kind of thing that isn't a model, controller, service, event, view, deployment, command, measure, convention, promotion, or distribution — add a new entity type.

**Example:** adding a `workflows` entity type.

### Step 1 — Create the entity module directory

```
entities/src/extensions/workflows/
├── module.yaml
├── index.ts
├── schema/
│   └── workflows.schema.json
├── conventions/
│   └── workflow-processor.ts
├── inference/
│   ├── index.ts
│   └── workflow-rules.json
├── generators/
│   └── index.ts
├── behaviour/
│   ├── invariants.qnt
│   ├── rules.qnt
│   └── conventions/
│       └── grammar.yaml
├── docs/
│   └── index.ts
├── tests/
│   └── index.ts
└── examples/
    └── example-workflow.specly
```

### Step 2 — Define the module manifest

```yaml
# module.yaml
name: workflows
type: extension                         # 'core' or 'extension'
version: 0.1.0
depends_on: [models, services]          # entity types this depends on

facets:
  schema: schema/workflows.schema.json
  conventions:
    structural: conventions/workflow-processor.ts
    behavioural: behaviour/conventions/grammar.yaml
  behaviour:
    rules: behaviour/rules.qnt
    invariants: behaviour/invariants.qnt
  inference:
    entry: inference/index.ts
  generators: generators/index.ts
  docs: docs/index.ts
  tests: tests/index.ts

diagrams:
  - type: workflow                      # registers a 'workflow' diagram type

delivery:
  parser: true                          # convention processor is available
  inference: false                      # no inference rules yet
  realize: false                        # no code generators yet
  cli: false                            # not a CLI entity
```

### Step 3 — Define the JSON Schema

Defines what `workflows:` looks like in a `.specly` file.

```json
{
  "$id": "specverse://entities/workflows",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "description": "Workflow definitions for orchestrating multi-step processes",
  "$defs": {
    "WorkflowsSection": {
      "type": "object",
      "patternProperties": {
        "^[a-z][a-zA-Z0-9_]*$": {
          "type": "object",
          "properties": {
            "description": { "type": "string" },
            "trigger": { "type": "string" },
            "steps": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "name": { "type": "string" },
                  "service": { "type": "string" },
                  "operation": { "type": "string" },
                  "onFailure": { "type": "string" }
                }
              }
            },
            "compensations": {
              "type": "object",
              "additionalProperties": { "type": "string" }
            }
          },
          "additionalProperties": false
        }
      },
      "additionalProperties": false
    }
  }
}
```

### Step 4 — Create the convention processor

Transforms raw YAML into a structured array.

```typescript
// conventions/workflow-processor.ts
import { AbstractProcessor, ProcessorContext } from '@specverse/types';

export interface WorkflowSpec {
  name: string;
  description?: string;
  trigger?: string;
  steps: WorkflowStep[];
  compensations?: Record<string, string>;
}

export interface WorkflowStep {
  name: string;
  service?: string;
  operation?: string;
  onFailure?: string;
}

export class WorkflowProcessor extends AbstractProcessor<any, WorkflowSpec[]> {
  process(workflowsData: any): WorkflowSpec[] {
    return Object.entries(workflowsData).map(([name, def]: [string, any]) => ({
      name,
      description: def.description,
      trigger: def.trigger,
      steps: (def.steps || []).map((step: any) => ({
        name: step.name,
        service: step.service,
        operation: step.operation,
        onFailure: step.onFailure,
      })),
      compensations: def.compensations,
    }));
  }
}
```

### Step 5 — Create the module entry point

```typescript
// index.ts
import type { EntityModule, EntityConventionProcessor } from '../../_shared/types.js';
import { WorkflowProcessor } from './conventions/workflow-processor.js';
import type { WorkflowSpec } from './conventions/workflow-processor.js';
import type { ProcessorContext } from '@specverse/types';

export { WorkflowProcessor, type WorkflowSpec };

function createWorkflowConventionProcessor(): EntityConventionProcessor<any, WorkflowSpec[]> {
  return {
    process(input: any, context: ProcessorContext): WorkflowSpec[] {
      const processor = new WorkflowProcessor(context);
      return processor.process(input);
    }
  };
}

export const workflowsModule: EntityModule = {
  name: 'workflows',
  type: 'extension',
  version: '0.1.0',
  dependsOn: ['models', 'services'],

  conventionProcessor: createWorkflowConventionProcessor(),

  inferenceRules: [],     // Add later when you have rules
  generators: [],         // Add later for code generation
  diagramPlugins: [
    { type: 'workflow' },
  ],
  docs: [],
  tests: [],
};

export default workflowsModule;
```

### Step 6 — Register the module

In `entities/src/_bootstrap.ts`:

```typescript
import { workflowsModule } from './extensions/workflows/index.js';

// In the bootstrap function:
registry.register(workflowsModule);
```

The entity module also needs to be listed in `entities/src/extensions/` and exported from `entities/src/index.ts`.

### Step 7 — Add to the component schema

In `entities/src/_shared/schema/root.schema.json`, add `workflows` to the component properties:

```json
"workflows": {
  "$ref": "#/$defs/WorkflowsSection"
}
```

The schema composition script (`entities/scripts/compose-schema.cjs`) loads extension schemas automatically.

### Step 8 — Add to the component entity filter

In `engines/src/parser/convention-processor.ts`, add `workflows` to the filter:

```typescript
const COMPONENT_ENTITY_TYPES = new Set([
  'models', 'controllers', 'services', 'views', 'events', 'commands', 'workflows'
]);
```

Also add `workflows?: any[]` to `ComponentSpec` in the AST types.

### Step 9 — Handle in astToExpandedYaml

In `engines/src/parser/unified-parser.ts`, add `workflows` to `knownProps` and add a conversion block (if the processed format differs from the raw format):

```typescript
const knownProps = new Set([..., 'workflows']);

// Convert workflows back to object format for post-validation
if (component.workflows && component.workflows.length > 0) {
  result.components[component.name].workflows = {};
  for (const wf of component.workflows as any[]) {
    const { name, ...def } = wf;
    result.components[component.name].workflows[name] = def;
  }
}
```

### Step 10 — Add behavioural specifications

Create Quint invariants and rules:

```quint
// behaviour/invariants.qnt
module workflowInvariants {
  import specverseTypes.* from "../../../_shared/behaviour/types"

  var workflows: Set[Workflow]

  // Every workflow must have at least one step
  val workflowsHaveSteps: bool =
    workflows.forall(w => w.steps.size() > 0)

  // Workflow step service references must exist
  val stepServicesExist: bool =
    workflows.forall(w =>
      w.steps.forall(s => s.serviceRef != ""))
}
```

### Step 11 — Add tests

Use the `testEntityModule()` helper for baseline coverage, then add entity-specific tests:

```typescript
// __tests__/workflows-entity.test.ts
import { describe, it, expect } from 'vitest';
import { testEntityModule, createTestContext } from '../../test-helpers.js';
import { workflowsModule } from '../extensions/workflows/index.js';
import { WorkflowProcessor } from '../extensions/workflows/conventions/workflow-processor.js';

// Standard entity module tests (metadata, schema, processor, diagrams)
testEntityModule(workflowsModule);

// Entity-specific tests
describe('Workflows convention processing', () => {
  it('should expand workflow definition', () => {
    const ctx = createTestContext();
    const processor = new WorkflowProcessor(ctx);
    const result = processor.process({
      checkout: {
        description: 'Checkout process',
        steps: [{ name: 'validate', service: 'CartService' }]
      }
    });
    expect(result).toHaveLength(1);
    expect(result[0].name).toBe('checkout');
    expect(result[0].steps).toHaveLength(1);
  });
});
```

If your tests need files from other workspace packages:

```typescript
import { resolvePackage } from '../../test-helpers.js';
const schemaPath = resolvePackage('entities', 'schema/SPECVERSE-SCHEMA.json');
```

Valid workspace package names: `types`, `entities`, `engines`, `runtime`. See [`docs/strategy/TEST-STRATEGY.md`](../strategy/TEST-STRATEGY.md) for the full test strategy.

### Step 12 — Build and verify

```bash
npm run build          # Rebuilds schema, compiles TypeScript
npm test               # All tests should pass
spv validate examples/my-workflow-spec.specly  # Test your entity
```

### What Happens Automatically

Once registered, your entity type is automatically:

1. **Parsed** — the convention processor runs when `workflows:` appears in a component
2. **Schema-validated** — both pre and post convention processing
3. **Discoverable** — `getEntityRegistry().getModule('workflows')` returns your module
4. **Available to inference** — if you add inference rules, they're loaded automatically
5. **Available to realize** — if you add generators, they're discovered by the realize engine
6. **Available to diagrams** — your `diagramPlugins` declaration registers diagram types

### What You Need to Do Manually

1. **Add inference rules** — JSON rule files that pattern-match models and generate architecture
2. **Add code generators** — instance factory YAMLs + template generators
3. **Add diagram plugins** — implement a diagram plugin class and register it
4. **Add Quint types** — extend `entities/src/_shared/behaviour/types.qnt` with your type definition

### Common Patterns

**Convention Syntax:** follow the existing pattern — `name: Type modifiers` for simple values, structured YAML for complex ones. The convention processor expands shorthand into full objects.

**Cross-Entity References:** if your entity references models (e.g., `workflows.steps[].service` references a service), add semantic validation in `unified-parser.ts` to the `SEMANTIC_RULES` array.

**Extension vs Core:**
- **Core**: Ships with SpecVerse, always available, deeply integrated. Examples: models, controllers, services.
- **Extension**: Installed separately, optional, can be distributed via npm. Examples: commands, measures, workflows.

Code structure is identical. The only difference is packaging (core is in `entities/src/core/`, extensions in `entities/src/extensions/`).

---

## Adding a New Engine

An engine is a pipeline-stage module. Current engines live as subpath exports of `@specverse/engines`: parser, inference, realize, generators, ai, registry.

**Two paths for adding a new engine** — pick based on dependency profile:

### Path A — Subpath of `@specverse/engines` (preferred)

Use when the new engine has the same dependency surface as existing engines (TypeScript, no framework deps, node-only).

**Examples:** a test-generation engine, a migration engine, a linter engine, a documentation engine.

#### Step 1 — Create the engine directory

```
engines/src/<name>/
├── index.ts             # engine adapter + exports
├── <name>-core.ts       # core logic
└── __tests__/
    └── <name>.test.ts
```

#### Step 2 — Implement `SpecVerseEngine`

```typescript
// engines/src/mytool/index.ts
import type { SpecVerseEngine, EngineInfo } from '@specverse/types';

export interface MyToolEngine extends SpecVerseEngine {
  doThing(input: any): Promise<Result>;
}

class SpecVerseMyToolEngine implements MyToolEngine {
  name = 'mytool';
  version = '6.3.1';                     // align with engines@ package version
  capabilities = ['mytool', 'mytool.doThing'];

  private state: any = null;

  async initialize(config?: any): Promise<void> {
    // Idempotent setup — safe to call multiple times
    this.state = new MyToolState(config);
  }

  getInfo(): EngineInfo {
    return { name: this.name, version: this.version, capabilities: this.capabilities };
  }

  async doThing(input: any): Promise<Result> {
    if (!this.state) throw new Error('mytool engine not initialized');
    return this.state.execute(input);
  }
}

// Singleton — this is what consumers register
export const engine = new SpecVerseMyToolEngine();
export default engine;
export { SpecVerseMyToolEngine };
```

#### Step 3 — Add the subpath export

In `engines/package.json`:

```json
{
  "exports": {
    "./parser":     { "types": "./dist/parser/index.d.ts",     "import": "./dist/parser/index.js" },
    "./inference":  { "types": "./dist/inference/index.d.ts",  "import": "./dist/inference/index.js" },
    "./realize":    { "types": "./dist/realize/index.d.ts",    "import": "./dist/realize/index.js" },
    "./generators": { "types": "./dist/generators/index.d.ts", "import": "./dist/generators/index.js" },
    "./ai":         { "types": "./dist/ai/index.d.ts",         "import": "./dist/ai/index.js" },
    "./registry":   { "types": "./dist/registry/index.d.ts",   "import": "./dist/registry/index.js" },
    "./mytool":     { "types": "./dist/mytool/index.d.ts",     "import": "./dist/mytool/index.js" },
    "./package.json": "./package.json"
  }
}
```

#### Step 4 — Explicit registration in the consumer (R36)

Engines don't autodiscover in published packages (R36 — see [SPECVERSE-SELF-HOSTING.md](SPECVERSE-SELF-HOSTING.md)). Each consumer imports explicitly and registers:

```typescript
import { EngineRegistry } from '@specverse/entities';
import { engine as parserEngine }    from '@specverse/engines/parser';
import { engine as inferenceEngine } from '@specverse/engines/inference';
import { engine as realizeEngine }   from '@specverse/engines/realize';
import { engine as mytoolEngine }    from '@specverse/engines/mytool';   // new

const registry = new EngineRegistry({ disableAutoDiscovery: true });
registry.register(parserEngine);
registry.register(inferenceEngine);
registry.register(realizeEngine);
registry.register(mytoolEngine);
```

Explicit imports mean Node resolves the engines from the consumer's `node_modules/`, which always works regardless of how transitively deps get hoisted.

#### Step 5 — Use by capability

```typescript
const mytool = registry.getEngineForCapability('mytool.doThing');
if (!mytool) throw new Error('mytool engine not available');
await mytool.initialize();
const result = await (mytool as MyToolEngine).doThing({ /* ... */ });
```

### Path B — New sibling workspace package

Use when the engine has fundamentally different dependencies or a different lifecycle. `@specverse/runtime` is the canonical example (it has React peer dependencies and a browser-runtime lifecycle).

#### Step 1 — Create the workspace directory

```
specverse-engines/
├── types/                  # existing
├── entities/               # existing
├── engines/                # existing
├── runtime/                # existing
└── mypackage/              # new
    ├── package.json
    ├── tsconfig.json
    └── src/
        └── index.ts
```

#### Step 2 — Define `mypackage/package.json`

```json
{
  "name": "@specverse/mypackage",
  "version": "5.0.0",
  "type": "module",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "exports": {
    ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
    "./package.json": "./package.json"
  },
  "scripts": { "build": "tsc", "clean": "rm -rf dist" },
  "dependencies": {
    "@specverse/types": "^5.1.1"
  },
  "files": ["dist"],
  "license": "MIT"
}
```

#### Step 3 — Register in the monorepo

In `specverse-engines/package.json`:

```json
{
  "workspaces": ["types", "entities", "engines", "runtime", "mypackage"]
}
```

`npm install` at the monorepo root now symlinks your package into the workspace.

#### Step 4 — Follow R36 on consumers

Explicit registration, just like Path A.

### The `SpecVerseEngine` Interface

Required members:

```typescript
interface SpecVerseEngine {
  name: string;                        // Unique identifier
  version: string;                     // Semver; align with the package version
  capabilities: string[];              // Capability strings for lookup
  initialize(config?: any): Promise<void>;  // Idempotent setup
  getInfo(): EngineInfo;               // Metadata
}
```

### Capability Naming Convention

Kebab-case, scoped by engine:

- Parser: `parse`, `validate`, `convention-processing`, `import-resolution`
- Inference: `infer`, `logical-inference`, `deployment-inference`, `rule-engine`
- Realize: `realize`, `code-generation`, `manifest-resolution`, `instance-factories`
- Generators: `generate-diagrams`, `generate-docs`, `generate-uml`
- AI: `ai-prompts`, `ai-suggestions`, `ai-templates`
- Registry: `engine-discovery`, `capability-resolution`

Use dotted form for sub-capabilities: `mytool.doThing`, `mytool.validate`.

### How Engines Relate to Entity Modules

Engines consume entity-module facets from `@specverse/entities`:

| Engine | Entity facet used | Relationship |
|---|---|---|
| parser | `schema`, `conventionProcessor` | Validates + expands entity syntax |
| inference | `inferenceRules` | Loads rule JSON per-entity for pattern matching |
| realize | `generators` | Discovers instance-factory metadata per-entity |
| generators (diagrams) | `diagramPlugins` | Discovers declared diagram types per-entity |

When a new entity type is registered, all engines pick up its facets automatically. You don't need to modify engines to support new entities — see [Adding a New Entity Type](#adding-a-new-entity-type) above.

### Engine Testing

```typescript
import { describe, it, expect } from 'vitest';
import { testEngine } from '../../test-helpers.js';
import { engine } from '../index.js';

testEngine(engine);   // metadata, capabilities, initialize

describe('mytool engine', () => {
  it('does the thing', async () => {
    await engine.initialize();
    const result = await engine.doThing({ /* ... */ });
    expect(result).toBeTruthy();
  });
});
```

### Engine Checklist

- [ ] Chose Path A (subpath of `@specverse/engines`) or Path B (new workspace package)
- [ ] Implements `SpecVerseEngine` interface from `@specverse/types`
- [ ] Exports `engine` as default or named export
- [ ] `initialize()` is idempotent
- [ ] `capabilities` array uses kebab-case with dotted sub-capabilities
- [ ] **Path A:** subpath added to `engines/package.json` exports
- [ ] **Path B:** workspace added to `specverse-engines/package.json` workspaces array
- [ ] Consumer code explicitly imports + registers via `registry.register(engine)`
- [ ] `npm run build` succeeds from the monorepo root
- [ ] `npm test` passes

---

## Adding a New Composition Pipeline

SpecVerse is structured so every extensibility pipeline walks the same entity/engines tree. The existing ones compose the schema, examples, docs, the realize output, the workspace validation report, the inference rule set, the generator registry, the entity registry. Adding a new one is an **additive** operation — every future entity bundle gets picked up for free.

See [SPECVERSE-ARCHITECTURE.md → Composition Pipelines](SPECVERSE-ARCHITECTURE.md#composition-pipelines) for the current map of pipelines + their inputs + outputs.

### The pattern

```
tree (entities/ + engines/ + optional _shared/)
          │
          │  walker reads facet-specific files
          ▼
   composer (TS or JS script in self/scripts/ or engines/scripts/
             — runtime aggregator in a TS module)
          │
          │  deterministic single artifact
          ▼
   output (tracked file in a known location,
           or value returned from a function call)
          │
          ▼
   downstream consumer (end-user tooling, realize,
                         another pipeline, docs site)
```

Three rules of the pattern:

1. **Sources live in the tree.** No hand-maintained "master list." Any entity bundle is a legitimate contributor to your pipeline; you discover them by walking `entities/src/**` (core + extensions) and reading the facet you care about.
2. **Output is deterministic + one-shot.** Same tree → identical output. The composed artifact is either tracked in git (for downstream consumers who shouldn't have to run your composer themselves) or returned from a function call at runtime.
3. **The composer is the only hand-maintained logic.** If the output looks wrong, the failure localises: either a source fragment is wrong or the composer is wrong. There's no third place to look.

### Concrete example — how the examples pipeline is wired

Input:
- `entities/src/core/<entity>/__examples__/*.specly` + colocated `.md` + `.example.yaml`
- `entities/src/extensions/<entity>/__examples__/...`
- `entities/src/_shared/examples/...` (cross-entity)
- `engines/assets/examples-{decomposed,inference}/...`

Composer: [`specverse-self/scripts/compose-examples.mjs`](../../scripts/compose-examples.mjs)

```js
// 1. Resolve packages (post-consolidation layout)
const ENTITIES_PKG = findPackage('entities');
const ENGINES_PKG  = findPackage('engines');

// 2. Walk the tree, collect .specly + .example.yaml pairs
for (const group of ['core', 'extensions']) {
  for (const mod of readdirSync(join(entitiesSrc, group))) {
    discoverFromDir(join(entitiesSrc, group, mod, '__examples__'), `entities/${group}/${mod}`);
  }
}
discoverFromDir(join(entitiesSrc, '_shared', 'examples'), 'entities/_shared');
if (ENGINES_PKG) {
  discoverFromDir(join(ENGINES_PKG, 'assets', 'examples-decomposed'), 'engines/examples-decomposed');
  discoverFromDir(join(ENGINES_PKG, 'assets', 'examples-inference'),  'engines/examples-inference');
}

// 3. Group by category, sort, emit to OUTPUT dir
writeFileSync(destSpecly, readFileSync(item.speclyPath, 'utf8'));
```

Output: `specverse-self/examples/` (tracked — 14 categories, ~120 files).

Downstream: `documentation/scripts/generate-diagrams.js` + `generate-sidebar.js` read from `specverse-self/examples/` to produce MDX docs. `spv realize all` copies `specverse-self/examples/` into every generated project's `examples/` dir.

### Checklist

- [ ] **Identify which facet your pipeline consumes** (`schema/`, `inference/`, `generators/`, `conventions/`, `__examples__/`, `__tests__/`, `__behaviour__/`, or a new facet).
- [ ] **Write the walker** — follow `compose-examples.mjs` or `compose-schema.cjs` as reference. Both walk `entities/src/core/*` and `entities/src/extensions/*` uniformly.
- [ ] **Make the output deterministic** — same input tree should produce byte-identical output. No timestamps in the composed artifact itself; no non-reproducible ordering.
- [ ] **Choose where the output lives.** For end-user-facing artifacts (schema, examples, docs), track in git so consumers don't have to run your composer. For internal runtime aggregates (entity registry, rule set), return from a function call and cache.
- [ ] **Add a test.** Give your composer a fixture input tree and assert the composed output is what you expect. `@specverse/engines/bundles` has this pattern — see `__tests__/fixtures/mock-bundle/` + `deriveCatalog.test.ts`.
- [ ] **Wire into the pre-release ritual** if the output needs to be regenerated before publish — add an npm script, list it in `PUBLISHING.md`.
- [ ] **Document in [SPECVERSE-ARCHITECTURE.md](SPECVERSE-ARCHITECTURE.md#composition-pipelines)** — add a row to the composition-pipelines table.

### Anti-patterns

- **Hand-maintained combined output** with no composer. Guaranteed to drift. If you catch yourself copy-pasting from several entity modules into one file, write the composer instead.
- **Sources scattered outside the facet pattern.** If your pipeline needs data that can't live in a per-entity facet, consider whether the concept is really entity-scoped. If it's truly cross-entity, put it in `entities/src/_shared/` with a clear reason.
- **Non-deterministic output.** Sorting alphabetically, deriving timestamps from `new Date()`, picking up file modification times — all make your composed artifact diff noisy. Sort by an authoritative key (e.g. entity name), drop timestamps, read only content.

---

## Adding a New Instance Factory

Instance factories generate code for specific technologies. They're the most common extension point — typically you're adding support for a new framework, ORM, or frontend stack.

1. **Create a YAML definition** in `engines/libs/instance-factories/{category}/`
2. **Write TypeScript generator functions** in `templates/{technology}/`
3. **Map capabilities** in the manifest

### YAML factory definition

```yaml
# my-factory.yaml
name: ExpressAPI
version: "1.0.0"
type: api-server
capabilities:
  provides: ["api.rest", "api.rest.crud"]
  requires: ["storage.database"]
technology:
  runtime: node
  framework: express
  language: typescript
codeTemplates:
  routes:
    engine: typescript
    generator: "templates/express/routes-generator.ts"
    outputPattern: "routes/{controller}.ts"
  services:
    engine: typescript
    generator: "templates/express/services-generator.ts"
    outputPattern: "services/{model}Service.ts"
  server:
    engine: typescript
    generator: "templates/express/server-generator.ts"
    outputPattern: "main.ts"
dependencies:
  runtime:
    - "express@^4.0.0"
    - "body-parser@^1.20.0"
  dev:
    - "@types/express@^4.0.0"
configuration:
  port: 3000
  healthCheck: true
```

### Generator template signature

```typescript
// templates/express/routes-generator.ts
import type { ControllerSpec, ModelSpec, TemplateContext } from '@specverse/types';

export function generate(context: TemplateContext & { controller: ControllerSpec }): string {
  const { controller, spec, manifest } = context;
  return `// Generated Express routes for ${controller.name}
import { Router } from 'express';
// ... generate the actual code
`;
}
```

### Manifest mapping

```yaml
# manifests/my-implementation.yaml
instanceFactories:
  - name: "ExpressAPI"
    source: "./custom-factories/express-api.yaml"
capabilityMappings:
  - capability: "api.rest.crud"
    instanceFactory: "ExpressAPI"
```

See [SPECVERSE-REALIZING.md](SPECVERSE-REALIZING.md) for capability taxonomy and how the resolver uses your factory.

---

## Adding a New LLM Provider

The AI engine wraps the [Vercel AI SDK](https://sdk.vercel.ai/) (`ai` v6 + `@ai-sdk/anthropic` + `@ai-sdk/openai-compatible` + `@ai-sdk/provider`). Engines 6.0 retired the bespoke `LLMProvider` / `ProviderFactory` / `SpecVerseOrchestrator` abstraction in favour of `LanguageModelV3` from the SDK. Adding a new provider means writing a `LanguageModelV3` implementation and registering it with the model resolver.

The four-mode default already covers most needs:

- `claude-cli` (custom adapter, spawns the local `claude` binary; ~98% input-token savings via session-resume on a Max subscription)
- `anthropic` (metered API)
- `openai-compatible` (DeepSeek / Groq / Together / Ollama / vLLM / etc., set via `SPECVERSE_AI_BASE_URL`)
- `stub` (no LLM, ambient-runtime mode for MCP servers)

You only need to add a new provider when integrating something the openai-compatible bridge can't reach (e.g. an entirely custom protocol, a binary wrapped like claude-cli, or a privacy-isolated on-device model).

### Step 1 — Implement `LanguageModelV3`

```typescript
// engines/src/ai/providers/my-provider.ts
import type {
  LanguageModelV3,
  LanguageModelV3CallOptions,
  LanguageModelV3GenerateResult,
  LanguageModelV3StreamResult,
} from '@ai-sdk/provider';

export interface MyProviderOptions {
  model?: string;
  timeout?: number;
  // ... whatever your provider needs
}

export function myProvider(options: MyProviderOptions = {}): LanguageModelV3 {
  return {
    specificationVersion: 'v3',
    provider: 'my-provider',
    modelId: options.model ?? 'default',
    supportedUrls: {},

    async doGenerate(opts: LanguageModelV3CallOptions): Promise<LanguageModelV3GenerateResult> {
      const { system, user } = flattenPrompt(opts.prompt);
      const text = await callMyBackend({ system, user, model: options.model });
      return {
        content: [{ type: 'text', text }],
        finishReason: { unified: 'stop', raw: undefined },
        usage: {
          inputTokens: { total: undefined, noCache: undefined, cacheRead: undefined, cacheWrite: undefined },
          outputTokens: { total: undefined, text: undefined, reasoning: undefined },
        },
        warnings: [],
      };
    },

    async doStream(opts: LanguageModelV3CallOptions): Promise<LanguageModelV3StreamResult> {
      // v1: buffer the doGenerate result and emit as a single chunk
      const result = await this.doGenerate(opts);
      // ... see engines/src/ai/providers/claude-cli.ts for the canonical shape
    },
  };
}
```

The `claude-cli` provider at `engines/src/ai/providers/claude-cli.ts` is the canonical reference — ~150 LOC, including session-resume caching and an `isSessionError` predicate for retry-on-collision.

### Step 2 — Wire into the model resolver

```typescript
// engines/src/ai/model-resolver.ts (excerpt)
import { myProvider } from './providers/my-provider.js';

export function resolveModel(): LanguageModelV3 {
  const providerId = resolveProviderId();
  switch (providerId) {
    case 'claude-cli': return claudeCli({ ... });
    case 'anthropic':  return anthropic('claude-sonnet-4-6');
    case 'openai-compatible': return createOpenAICompatible({ baseURL, apiKey })('default');
    case 'stub':       return stubModel();
    case 'my-provider': return myProvider({ ... });   // new
    default: throw new Error(`Unknown provider: ${providerId}`);
  }
}
```

Add a corresponding case to `resolveProviderId()` so `SPECVERSE_AI_PROVIDER=my-provider` selects it.

### Step 3 — Configure via env vars

```bash
SPECVERSE_AI_PROVIDER=my-provider
MY_PROVIDER_API_KEY=sk-...           # if your provider needs it
MY_PROVIDER_BASE_URL=https://...     # if applicable
```

Now `spv ai analyse` / `spv ai create` / behaviour-gen during `spv realize` all use your provider through the same model-resolver entrypoint. Anthropic prompt-caching providerOptions (e.g. `cacheControl: 'ephemeral'`) flow through naturally because the SDK is the canonical plumbing — no custom caching code needed.

### When to wrap a binary instead of an HTTP API

If your provider is a CLI (like `claude` is for the claude-cli provider), follow the spawn-based pattern: build args, pipe stdin, capture stdout. See `engines/src/ai/providers/claude-cli.ts`'s `spawnClaude()` helper — handles timeout, error-stream capture, and session-id collision recovery (TODO #42 codified `isSessionError`).

See [SPECVERSE-AI.md](SPECVERSE-AI.md) for the four-mode user-facing config and [SPECVERSE-AI-ARCHITECTURE.md](SPECVERSE-AI-ARCHITECTURE.md) for how prompts and providers compose.

---

## Formal Verification with Quint

Each entity type has Quint specifications that formally verify invariants. Quint specs define:

### Rules — transformation actions

```quint
action generateController(m: Model): bool = all {
  not(m.hasParentRelationship),
  controllers' = controllers.union(Set({
    name: m.name + "Controller",
    model: m.name,
    operations: Set("create", "retrieve", "update", "delete")
  }))
}
```

### Invariants — properties that must always hold

```quint
val modelsHaveAttributes: bool =
  models.forall(m => m.attributes.keys().size() > 0)

val lifecycleStatesNonEmpty: bool =
  models.forall(m => m.lifecycles.forall(lc => lc.states.size() > 0))

val relationshipTargetsExist: bool =
  models.forall(m => m.relationships.forall(r =>
    models.exists(target => target.name == r.target)))
```

### Quint → TypeScript guard transpilation (validate-time)

The Quint transpiler converts the entity-module invariants to TypeScript guard functions that are run **in-process at validation time** to check the spec is well-formed:

```
entity .qnt invariants → TS guard functions → run by `spv validate --verify`
```

These guards verify **the spec**, not live runtime data. On the self-spec, raw shows 7/7 hold and inferred shows 14/14 (skipped guards filtered by missing state vars or fields).

**Tooling note:** Quint specs are type-checked with `quint typecheck` (a build-time gate that skips if the binary is absent). Despite older phrasing elsewhere, **Apalache model-checking is not wired** — `spv validate --verify` runs the transpiled guards in-process, not via Apalache. (Runtime enforcement of *business rules* in a generated backend is a separate mechanism — the per-model `<Model>.guards.ts` emitted from `model.constraints:`; see [SPECVERSE-REALIZING.md](SPECVERSE-REALIZING.md#the-constraint-generation-walkthrough).)

---

## Behavioural Conventions

Human-readable patterns that compile to Quint invariants. Let spec authors write constraints in natural language:

```yaml
# entities/src/core/models/behaviour/conventions/grammar.yaml
conventions:
  must_have_attributes:
    pattern: "{entities} must have attributes"
    body: "{entities}.forall(m => m.attributes.keys().size() > 0)"

  must_not_be_orphaned:
    pattern: "{entities} must not be orphaned"
    body: "{entities}.forall(e => components.exists(c => c.{entities}.contains(e)))"

  referenced_target_must_exist:
    pattern: "referenced {target} must exist"
    body: "referenced.forall(r => {target}.exists(t => t.name == r.target))"
```

Spec authors can then write:

```yaml
components:
  MyApp:
    constraints:
      - "model names must be unique"
      - "models must have attributes"
      - "referenced models must exist"
```

The convention engine expands these human-readable constraints into typed Quint invariants at parse time, matching against grammar patterns across all registered entity modules. The expanded Quint is then type-checked (`quint typecheck`) and transpiled to guard functions. (Apalache model-checking is not currently integrated — see the tooling note above.)

This is the meta-circular property — **conventions defining how conventions work** — that makes the language extensible by domain experts who aren't Quint experts.

---

## Related

- [SPECVERSE-INTRO.md](SPECVERSE-INTRO.md) — the documentation hub
- [SPECVERSE-SPECIFYING.md](SPECVERSE-SPECIFYING.md) — current entity types you can use
- [SPECVERSE-REALIZING.md](SPECVERSE-REALIZING.md) — how your instance factory runs at realize time
- [SPECVERSE-ARCHITECTURE.md](SPECVERSE-ARCHITECTURE.md) — internal architecture
- [SPECVERSE-SELF-HOSTING.md](SPECVERSE-SELF-HOSTING.md) — R36 explicit registration rationale
- [GOLDEN-RULES.md](../GOLDEN-RULES.md) — the 44 permanent principles (R12, R16d, R24, R36/R36a/R36b especially relevant)
