Skip to content

PGDL (Phony Generator Definition Language) Specification ​

PGDL is a declarative JSON-based language for defining data generation schemas. This document provides the complete language specification.

Format Decision: PGDL uses JSON as the canonical format with JSON Schema for IDE autocomplete and validation. String templates ({{expression}}) provide readable composition. CLI validation catches template reference errors at build time.

Implementation status (phony-pgdl)

Taxonomy note: the built engine has five core generators (Logic, List, Model, Statistical, Event Sequence) plus composition (the universal composer). The template and linked types described below are folded in: template → a composition with a body (and optional weighted variants); linked → either a list over an asset of coherent objects, or a composition output that computes coherent fields over prior let steps.

Built (the generator layer): the five core generators, composition (let/body/variants/output), PEL, the PGDL→envelope compiler (compile_generator), the modifier pipeline (unique/nullable/transform), locale-chain asset resolution, and package loading (phony.json compositions + list/model assets, with static reference validation at registration).

Not in the engine — by decision, not "pending": the entity layer. Generators are pure: (seed, index, assets, bindings) → value — no tables, rows, self. fields, primary keys, or FK ordering. entities / relationships / scenarios (and everything that rides on them: ref, computed fields over sibling columns, cross-entity aggregates, per_user distribution counts, on_delete, indexes, check constraints) are the cloud-product target design, documented in the second half of this page and clearly labeled. An earlier prototype entity layer was deliberately removed. See Generator capabilities & the platform boundary.

Deferred within the generator layer: unique_within/unique_fallback, per-event condition on event sequences, and the statistical algebraic mode (by design a composition computed step).

JSON Schema ​

A JSON Schema for PGDL is generated from the generator manifests and checked into the engine repo at conformance/pgdl-schema.json (in phony-core). Because it is derived from the manifests, it stays in lock-step with the built generators rather than drifting from a hand-maintained file. Point your editor at that file for IDE support:

json
{
  "$schema": "./conformance/pgdl-schema.json",
  "version": "1.0",
  "name": "My Schema"
}

Note: there is no hosted https://phony.cloud/schemas/… URL yet — use the generated conformance/pgdl-schema.json from the phony-core repo (or a local copy). The examples on this page keep a placeholder $schema URL purely for illustration.

The JSON Schema provides:

  • Autocomplete: Property suggestions as you type
  • Validation: Real-time error highlighting for structure errors
  • Documentation: Hover tooltips with property descriptions

Note: JSON Schema validates structure but not template content ({{...}}). Template references are validated by the CLI at build time with clear error messages.

Schema Structure ​

json
{
  "$schema": "https://phony.cloud/schemas/pgdl-1.0.json",
  "version": "1.0",
  "locale": "tr_TR",
  "name": "Schema Name",
  "description": "Optional description",

  "metadata": {
    "author": "Author Name",
    "license": "MIT",
    "tags": ["ecommerce", "turkish"]
  },

  "inherits": "base",

  "generators": {
    "generator_name": {
      "type": "logic | list | model | statistical | event_sequence"
    }
  },

  "entities": {
    "EntityName": {
      "fields": {
        "field_name": {
          "generator": "generator_name"
        }
      }
    }
  },

  "relationships": [
    {
      "from": "EntityA.field",
      "to": "EntityB.field",
      "cardinality": "one-to-one | one-to-many | many-to-one | many-to-many"
    }
  ],

  "scenarios": {
    "scenario_name": {
      "EntityName": 100
    }
  }
}

Layer split: the generators key is the built generator layer (runnable with the engine today, plus use references to compositions). The entities, relationships, and scenarios keys are the cloud-platform target design — the engine has no entity layer (see the status callout above and the labeled sections below).


Version Declaration ​

json
{
  "version": "1.0"
}
VersionStatusFeatures
1.0CurrentFull PGDL support

Locale Declaration ​

json
{
  "locale": "tr_TR"
}

In the engine, locale is a resolution chain (most specific first, e.g. tr_TR,*; set with --locale): every list/model asset name resolves to its best matching per-locale contribution. The locale affects:

  • Which model contribution an asset name resolves to
  • Which list contribution an asset name resolves to
  • (Target design) default date/number formatting

Supported locales follow the language_COUNTRY format:

  • tr_TR - Turkish (Turkey)
  • en_US - English (United States)
  • en_GB - English (United Kingdom)
  • de_DE - German (Germany)
  • fr_FR - French (France)
  • etc.

Package Inheritance ​

Status: the inherits keyword is target design. Today layering is achieved by loading packages additively: the project's dependency closure loads leaves-first, and later packages (or explicit --package layers) extend/override earlier contributions per (asset, locale).

json
{
  "inherits": "base"
}

Or with scoped packages:

json
{
  "inherits": "@phony/ecommerce-base"
}

Inheritance allows packages to extend other packages:

┌─────────────────────────────────────────────────────────────────────────┐
│           INHERITANCE RESOLUTION ORDER                                   │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  my-package (most specific)                                             │
│       │                                                                  │
│       └─▶ inherits: tr_TR                                               │
│               │                                                          │
│               └─▶ inherits: base (least specific)                       │
│                                                                          │
│  Resolution:                                                             │
│  1. Look in my-package                                                  │
│  2. If not found, look in tr_TR                                         │
│  3. If not found, look in base                                          │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Generator Definitions ​

These are the built generator types. Each per-type shape compiles to the uniform envelope { use, params, constraints, modifiers } (e.g. type: "logic", algorithm: "int_between" → use: "@phony/core:logic.int_between"); an explicit use reaches any registered generator, including package compositions. In the engine, a list/model source is a namespaced asset name (e.g. @phony/person:first_names) resolved through the locale chain — file paths appear only inside a package's phony.json asset map. Examples below that show path-style sources predate this and read as asset names.

Logic Generator ​

json
{
  "generators": {
    "user_id": {
      "type": "logic",
      "algorithm": "uuid_v7"
    },
    "age": {
      "type": "logic",
      "algorithm": "int_between",
      "params": {
        "min": 18,
        "max": 85
      }
    },
    "price": {
      "type": "logic",
      "algorithm": "float_between",
      "params": {
        "min": 0.01,
        "max": 9999.99,
        "precision": 2
      },
      "nullable": false,
      "description": "Product price in local currency"
    }
  }
}

Available algorithms (15, as registered in the engine — @phony/core:logic.*):

AlgorithmParametersOutput
uuid_v4-UUID v4 string
uuid_v7-UUID v7 string (time-sortable, synthetic seed-derived timestamp)
ulid-ULID string (Crockford base32)
nanoidlength (default: 21)Nano ID string
int_betweenmin, max, exceptInteger
float_betweenmin, max, precisionFloat
booleanprobability (default: 0.5)Boolean
datetime_betweenstart, endISO-8601 datetime string
date_betweenstart, endISO-8601 date string
time_betweenstart, endTime string
timestampstart, endUnix timestamp
sequencestart, stepIncrementing integer (start + row × step)
gaussianmean, stddevFloat (normal dist.)
exponentiallambdaFloat (exp. dist.)
digitsdigits, strict, leading_zeroFixed-digit-count integer

Date/time ranges accept ISO-8601 or relative expressions (-1year, now, now+7days) resolved against a deterministic reference instant (--reference-time). Timestamps inside uuid_v7/ulid are seed-derived (synthetic), not wall-clock — that keeps them reproducible.

List Generator ​

json
{
  "generators": {
    "city": {
      "type": "list",
      "source": "lists/geo/cities.json"
    },
    "status": {
      "type": "list",
      "source": "inline",
      "values": ["active", "pending", "cancelled"]
    },
    "order_status": {
      "type": "list",
      "source": "inline",
      "values": [
        { "value": "completed", "weight": 60 },
        { "value": "pending", "weight": 25 },
        { "value": "cancelled", "weight": 10 },
        { "value": "refunded", "weight": 5 }
      ]
    },
    "http_method": {
      "type": "list",
      "source": "inline",
      "values": ["GET", "POST", "PUT", "DELETE"],
      "locale_independent": true
    },
    "country": {
      "type": "list",
      "source": "lists/geo/countries.json",
      "locale_independent": true,
      "nullable": false,
      "description": "ISO 3166 country"
    }
  }
}

Model Generator ​

Model generators take an optional generation block specifying the output mode (defaults to word). See N-gram Models for complete details.

json
{
  "generators": {
    "first_name": {
      "type": "model",
      "source": "models/person_names.ngram",
      "generation": { "mode": "word" }
    },
    "username": {
      "type": "model",
      "source": "models/usernames.ngram",
      "generation": { "mode": "word" },
      "constraints": {
        "min_length": 4,
        "max_length": 16
      }
    },
    "tagline": {
      "type": "model",
      "source": "models/slogans.ngram",
      "generation": {
        "mode": "sentence",
        "params": {
          "word_count": "{{number:3-8}}",
          "punctuation": [".", "!"]
        }
      }
    },
    "company_name": {
      "type": "model",
      "source": "models/companies.ngram",
      "generation": {
        "mode": "word",
        "params": { "starts_with": "A" }
      },
      "constraints": {
        "min_length": 3,
        "max_length": 50
      },
      "nullable": false,
      "description": "Turkish company name starting with A"
    }
  }
}

Generation modes: word, sentence, text, paragraph, poem, acrostic, real_word. The generation block defaults to word mode; set it explicitly for the others.

Note: params controls generation behavior (e.g., starts_with), while constraints validates output (e.g., min_length - rejects if not met).

Template Generator (folded into composition) ​

Folded — type: "template" is not accepted by the engine

The engine rejects type: "template" with a pointer to the replacement: author a composition with a body template (optionally weighted variants); the old operations post-pipeline becomes PEL function calls in the body. The examples below document the original design; write them as compositions today. unique works as a modifier; unique_fallback/unique_within are deferred.

json
{
  "generators": {
    "email": {
      "type": "template",
      "pattern": "{{lowercase(first_name)}}.{{lowercase(last_name)}}@{{pick(domains)}}"
    },
    "street_name": {
      "type": "template",
      "variants": [
        { "pattern": "{{first_name}} Sokak", "weight": 35 },
        { "pattern": "{{last_name}} Caddesi", "weight": 35 },
        { "pattern": "{{number:1-2000}}. Sokak", "weight": 30 }
      ]
    },
    "user_email": {
      "type": "template",
      "pattern": "{{lowercase(first_name)}}.{{lowercase(last_name)}}{{number:1-999}}@example.com",
      "unique": true,
      "unique_fallback": "user_{{uuid}}@example.com"
    },
    "slug": {
      "type": "template",
      "pattern": "{{product_name}}",
      "operations": ["slugify", "lowercase"]
    },
    "full_address": {
      "type": "template",
      "variants": [
        {
          "pattern": "{{street_name}} No:{{number:1-200}} {{district}}/{{city}}",
          "weight": 45,
          "condition": "locale == 'tr_TR'"
        },
        {
          "pattern": "{{number:1-200}} {{street_name}}, {{city}}, {{state}} {{zip}}",
          "weight": 55,
          "condition": "locale == 'en_US'"
        }
      ],
      "unique": false,
      "unique_within": null,
      "operations": [],
      "nullable": false,
      "description": "Full mailing address"
    }
  }
}

Statistical Generator ​

Statistical generators produce data matching real-world distributions.

json
{
  "generators": {
    "order_status": {
      "type": "statistical",
      "mode": "categorical",
      "values": [
        { "value": "completed", "weight": 70 },
        { "value": "pending", "weight": 20 },
        { "value": "cancelled", "weight": 10 }
      ]
    },
    "customer_age": {
      "type": "statistical",
      "mode": "continuous",
      "distribution": "normal",
      "params": { "mean": 35, "stddev": 12 },
      "constraints": { "min": 18, "max": 85 }
    },
    "salary": {
      "type": "statistical",
      "mode": "continuous",
      "distribution": "lognormal",
      "params": { "mu": 10.5, "sigma": 0.8 },
      "differential_privacy": {
        "enabled": true,
        "epsilon": 1.0
      }
    },
    "order_totals": {
      "type": "statistical",
      "mode": "algebraic",
      "columns": ["subtotal", "tax", "discount", "total"],
      "relationship": "total = subtotal + tax - discount"
    }
  }
}

Available modes:

ModeDescriptionParameters
categoricalPreserve frequency distributionvalues with weights
continuousFollow statistical distributiondistribution, params, constraints
algebraicPreserve mathematical relationshipscolumns, relationship
multivariatePreserve correlationscolumns, correlations

Available distributions: normal, lognormal, exponential, uniform, poisson, beta, gamma

Linked Generator (folded into composition) ​

Folded — type: "linked" is not accepted by the engine

The engine rejects type: "linked" with a pointer to the replacements: use a list over an asset of coherent objects (pick one whole record) or a composition output that computes coherent fields over shared let steps. The example below is kept to document the original design intent; the entity wiring it shows is cloud-platform target design.

Linked generators ensure related columns generate coherent data together.

json
{
  "generators": {
    "location": {
      "type": "linked",
      "columns": ["city", "district", "country", "postal_code", "latitude", "longitude", "phone_prefix", "currency"],
      "source": "lists/geo/locations.json"
    },
    "person_info": {
      "type": "linked",
      "columns": ["first_name", "gender", "title"],
      "source": "lists/person/names_with_gender.json",
      "rules": {
        "title": {
          "male": ["Bay", "Mr."],
          "female": ["Bayan", "Ms.", "Mrs."]
        }
      }
    },
    "financials": {
      "type": "linked",
      "columns": ["salary", "bonus", "tax", "net_income"],
      "rules": {
        "salary": { "type": "statistical", "distribution": "lognormal", "params": { "mu": 10, "sigma": 0.5 } },
        "bonus": "salary * uniform(0.05, 0.20)",
        "tax": "(salary + bonus) * 0.25",
        "net_income": "salary + bonus - tax"
      }
    }
  },
  "entities": {
    "Employee": {
      "fields": {
        "city": { "generator": "location.city" },
        "country": { "generator": "location.country" },
        "salary": { "generator": "financials.salary" },
        "net_income": { "generator": "financials.net_income" }
      }
    }
  }
}

Event Sequence Generator ​

Event sequences generate chronologically valid date/time series.

json
{
  "generators": {
    "order_timeline": {
      "type": "event_sequence",
      "events": [
        {
          "name": "created_at",
          "base": true,
          "range": { "start": "-1year", "end": "now" }
        },
        {
          "name": "paid_at",
          "after": "created_at",
          "delay": { "min": "0h", "max": "24h" },
          "probability": 0.95
        },
        {
          "name": "shipped_at",
          "after": "paid_at",
          "delay": { "min": "1d", "max": "3d" },
          "probability": 0.90
        },
        {
          "name": "delivered_at",
          "after": "shipped_at",
          "delay": { "min": "1d", "max": "7d" },
          "probability": 0.85
        }
      ]
    }
  },
  "entities": {
    "Order": {
      "fields": {
        "created_at": { "generator": "order_timeline.created_at" },
        "paid_at": { "generator": "order_timeline.paid_at" },
        "shipped_at": { "generator": "order_timeline.shipped_at" },
        "delivered_at": { "generator": "order_timeline.delivered_at" }
      }
    }
  }
}

Event options:

OptionDescriptionExample
baseThe anchor event, generated firsttrue
afterThis event occurs after specified event"created_at"
delayTime range between events{ "min": "1d", "max": "7d" }
probabilityChance this event occurs (null if < 1.0)0.85
conditionOnly generate if condition met (deferred — not yet in the engine)"status = 'paid'"

Entity Definitions ​

Cloud-platform target design

The entity layer (entities, fields, primary_key, unique at field level, ref, computed over siblings, self. references, constraints/check, indexes) is not in the built engine — it was deliberately removed so generators stay pure (seed, index, assets, bindings) → value. This section is the design contract for the cloud product. At the generator layer, coherence across "fields" is expressed with a composition output over shared let steps.

Entities represent data structures (like database tables).

json
{
  "entities": {
    "User": {
      "table": "users",
      "fields": {
        "id": {
          "generator": "user_id",
          "primary_key": true
        },
        "first_name": {
          "generator": "first_name"
        },
        "last_name": {
          "generator": "last_name"
        },
        "email": {
          "generator": "email",
          "unique": true
        },
        "age": {
          "generator": "age",
          "nullable": true
        },
        "created_at": {
          "generator": "created_at"
        },
        "is_active": {
          "type": "logic",
          "algorithm": "boolean",
          "params": { "probability": 0.85 }
        },
        "full_name": {
          "type": "template",
          "pattern": "{{self.first_name}} {{self.last_name}}"
        },
        "company_id": {
          "ref": "Company.id"
        }
      },
      "constraints": [
        { "type": "unique", "fields": ["email"] },
        { "type": "check", "expression": "age >= 18" }
      ],
      "indexes": [
        { "fields": ["email"], "unique": true },
        { "fields": ["company_id", "created_at"] }
      ]
    }
  }
}

Field Options ​

json
{
  "fields": {
    "field_name": {
      "generator": "generator_name",
      "primary_key": false,
      "unique": false,
      "nullable": false,
      "default": null,
      "description": "Field description"
    },
    "inline_field": {
      "type": "logic",
      "algorithm": "uuid_v7"
    },
    "foreign_key_field": {
      "ref": "Entity.field"
    },
    "computed_field": {
      "computed": "expression"
    }
  }
}

Relationship Definitions ​

Cloud-platform target design

Relationships (FK-ordered generation, cardinality, on_delete) require the entity layer and are not in the engine. The generator-layer primitive that survives into this design is deterministic keyed generation (word_for(key)), which the platform will use to propagate PK→FK consistently.

Explicit relationship declarations for referential integrity.

json
{
  "relationships": [
    {
      "from": "Order.user_id",
      "to": "User.id",
      "cardinality": "many-to-one",
      "on_delete": "cascade"
    },
    {
      "from": "ProductCategory.product_id",
      "to": "Product.id",
      "cardinality": "many-to-one"
    },
    {
      "from": "ProductCategory.category_id",
      "to": "Category.id",
      "cardinality": "many-to-one"
    },
    {
      "from": "Employee.manager_id",
      "to": "Employee.id",
      "cardinality": "many-to-one",
      "nullable": true
    }
  ]
}

on_delete options: "cascade" (delete referencing rows), "set_null" (set FK to null), "restrict" (prevent deletion)

Cardinality Types ​

TypeDescriptionExample
one-to-oneEach A has exactly one BUser ↔ Profile
one-to-manyEach A has many BsUser → Orders
many-to-oneMany As belong to one BOrders → User
many-to-manyMany As to many BsProducts ↔ Categories

Scenario Definitions ​

Cloud-platform target design

Scenarios orchestrate entity generation and are not in the engine. Today the equivalent is phony generate --seed S -n COUNT per generator; named scenarios, per-entity counts, and distribution-based counts (per_user gaussian) are cloud-product design.

Scenarios define how much data to generate.

json
{
  "scenarios": {
    "development": {
      "User": 100,
      "Product": 50,
      "Order": 500
    },
    "staging": {
      "User": 10000,
      "Product": 1000,
      "Order": 50000
    },
    "load_test": {
      "User": 100000,
      "Product": 5000,
      "Order": 1000000
    },
    "custom": {
      "User": {
        "count": 1000,
        "seed": 12345
      },
      "Product": {
        "count": 500,
        "filter": "category == 'electronics'"
      },
      "Order": {
        "count": 5000,
        "distribution": {
          "per_user": "gaussian",
          "params": { "mean": 5, "stddev": 2 }
        }
      }
    }
  }
}

Full Examples ​

Runnable today — the generator layer ​

Two runnable forms, verified against the engine's compile path (compile_generator for a single definition, phony.json package loading for compositions + assets).

A single generator definition. phony generate accepts a file containing one PGDL generator (per-type shape, or a use envelope):

json
{
  "type": "logic",
  "algorithm": "int_between",
  "params": { "min": 18, "max": 85 },
  "nullable": 0.1
}
bash
phony generate age.json --seed 42 -n 5 -f jsonl

A package: compositions + locale assets. A phony.json package document registers composition generators (pure data — params/let/body) and contributes per-locale assets. list/model source values are namespaced asset names resolved through the locale chain, not file paths:

json
{
  "name": "@acme/demo",
  "version": "0.1.0",
  "generators": [
    {
      "name": "@acme/demo:employee_email",
      "description": "first_name + counter at a configurable domain",
      "params": {
        "domain": { "type": "string", "default": "example.com" }
      },
      "let": {
        "first": { "type": "list", "source": "@acme/demo:first_names" },
        "n": { "type": "logic", "algorithm": "int_between", "params": { "min": 1, "max": 99 } }
      },
      "body": "{{ lowercase(first) }}{{ n }}@{{ domain }}"
    }
  ],
  "assets": {
    "lists": {
      "@acme/demo:first_names": {
        "*": ["Ada", "Grace", "Alan"],
        "tr_TR": ["Mehmet", "Ayşe", "Deniz"]
      }
    }
  }
}
bash
# resolve the tr_TR contribution, fall back to "*"
phony generate --use "@acme/demo:employee_email" \
  --package ./demo --locale "tr_TR,*" --seed 42 -n 3 -f jsonl

The same composition can also be invoked from a definition file as { "use": "@acme/demo:employee_email", "params": { "domain": "acme.dev" } }.

Target design — the entity layer (cloud platform) ​

Target design — not runnable on the built engine

The schema below exercises the entity layer, which the engine deliberately does not have. Several features are design-only even within that layer: SUM(...) cross-entity aggregates, per_user/per_order distribution counts, on_delete, indexes, check constraints, computed over sibling fields, self. references (including inside logic params), and ref: foreign keys. It is kept as the design contract for the cloud product.

json
{
  "$schema": "https://phony.cloud/schemas/pgdl-1.0.json",
  "version": "1.0",
  "locale": "tr_TR",
  "name": "Turkish E-Commerce Platform",
  "description": "Complete e-commerce data generation schema",

  "metadata": {
    "author": "Phony Team",
    "version": "1.0.0",
    "license": "MIT",
    "tags": ["ecommerce", "turkish", "retail"]
  },

  "inherits": "base",

  "generators": {
    "uuid": {
      "type": "logic",
      "algorithm": "uuid_v7"
    },
    "timestamp": {
      "type": "logic",
      "algorithm": "datetime_between",
      "params": { "start": "-2years", "end": "now" }
    },
    "future_date": {
      "type": "logic",
      "algorithm": "date_between",
      "params": { "start": "now", "end": "+1year" }
    },
    "price": {
      "type": "logic",
      "algorithm": "float_between",
      "params": { "min": 10, "max": 50000, "precision": 2 }
    },
    "quantity": {
      "type": "logic",
      "algorithm": "int_between",
      "params": { "min": 1, "max": 10 }
    },
    "rating": {
      "type": "logic",
      "algorithm": "float_between",
      "params": { "min": 1, "max": 5, "precision": 1 }
    },
    "is_active": {
      "type": "logic",
      "algorithm": "boolean",
      "params": { "probability": 0.85 }
    },

    "city": {
      "type": "list",
      "source": "lists/geo/cities.json"
    },
    "district": {
      "type": "list",
      "source": "lists/geo/districts.json"
    },
    "category": {
      "type": "list",
      "source": "lists/commerce/categories.json"
    },
    "payment_method": {
      "type": "list",
      "source": "inline",
      "values": [
        { "value": "credit_card", "weight": 50 },
        { "value": "debit_card", "weight": 25 },
        { "value": "bank_transfer", "weight": 15 },
        { "value": "cash_on_delivery", "weight": 10 }
      ]
    },
    "order_status": {
      "type": "list",
      "source": "inline",
      "values": [
        { "value": "pending", "weight": 10 },
        { "value": "processing", "weight": 15 },
        { "value": "shipped", "weight": 20 },
        { "value": "delivered", "weight": 45 },
        { "value": "cancelled", "weight": 7 },
        { "value": "refunded", "weight": 3 }
      ]
    },

    "first_name": {
      "type": "model",
      "source": "models/person/first_names.ngram",
      "generation": { "mode": "word" }
    },
    "last_name": {
      "type": "model",
      "source": "models/person/last_names.ngram",
      "generation": { "mode": "word" }
    },
    "company_name": {
      "type": "model",
      "source": "models/business/company_names.ngram",
      "generation": { "mode": "word" }
    },
    "product_name": {
      "type": "model",
      "source": "models/commerce/product_names.ngram",
      "generation": { "mode": "word" }
    },
    "street_name": {
      "type": "model",
      "source": "models/address/street_names.ngram",
      "generation": { "mode": "word" }
    },

    "full_name": {
      "type": "template",
      "pattern": "{{first_name}} {{last_name}}"
    },
    "email": {
      "type": "template",
      "pattern": "{{lowercase(first_name)}}.{{lowercase(last_name)}}@{{pick(['gmail.com', 'hotmail.com', 'yahoo.com', 'outlook.com'])}}",
      "unique": true
    },
    "phone": {
      "type": "template",
      "pattern": "+90 {{pick(['532', '533', '535', '542', '543', '544', '545'])}} {{pattern:### ## ##}}"
    },
    "address": {
      "type": "template",
      "variants": [
        { "pattern": "{{street_name}} No:{{number:1-200}} {{district}}/{{city}}", "weight": 40 },
        { "pattern": "{{street_name}} {{number:1-150}}/{{number:1-20}} {{city}}", "weight": 35 },
        { "pattern": "{{pattern:?????}} Mah. {{street_name}} No:{{number:1-100}} {{city}}", "weight": 25 }
      ]
    },
    "sku": {
      "type": "template",
      "pattern": "{{uppercase(substring(category.code, 0, 3))}}-{{pattern:######}}",
      "unique": true
    },
    "order_number": {
      "type": "template",
      "pattern": "ORD-{{format(now(), 'Ymd')}}-{{pattern:######}}",
      "unique": true
    }
  },

  "entities": {
    "User": {
      "table": "users",
      "fields": {
        "id": { "generator": "uuid", "primary_key": true },
        "first_name": { "generator": "first_name" },
        "last_name": { "generator": "last_name" },
        "full_name": { "generator": "full_name" },
        "email": { "generator": "email", "unique": true },
        "phone": { "generator": "phone" },
        "address": { "generator": "address" },
        "is_active": { "generator": "is_active" },
        "created_at": { "generator": "timestamp" }
      },
      "indexes": [
        { "fields": ["email"], "unique": true }
      ]
    },
    "Category": {
      "table": "categories",
      "fields": {
        "id": { "generator": "uuid", "primary_key": true },
        "name": { "generator": "category" },
        "slug": { "type": "template", "pattern": "{{slugify(self.name)}}" },
        "parent_id": { "ref": "Category.id", "nullable": true },
        "created_at": { "generator": "timestamp" }
      }
    },
    "Product": {
      "table": "products",
      "fields": {
        "id": { "generator": "uuid", "primary_key": true },
        "sku": { "generator": "sku", "unique": true },
        "name": { "generator": "product_name" },
        "description": { "type": "template", "pattern": "{{product_name}} - Yüksek kaliteli ürün" },
        "price": { "generator": "price" },
        "category_id": { "ref": "Category.id" },
        "is_active": { "generator": "is_active" },
        "rating": { "generator": "rating" },
        "created_at": { "generator": "timestamp" }
      },
      "indexes": [
        { "fields": ["sku"], "unique": true },
        { "fields": ["category_id"] }
      ]
    },
    "Order": {
      "table": "orders",
      "fields": {
        "id": { "generator": "uuid", "primary_key": true },
        "order_number": { "generator": "order_number", "unique": true },
        "user_id": { "ref": "User.id" },
        "status": { "generator": "order_status" },
        "payment_method": { "generator": "payment_method" },
        "shipping_address": { "generator": "address" },
        "subtotal": { "computed": "SUM(order_items.line_total)" },
        "tax": { "computed": "subtotal * 0.18" },
        "total": { "computed": "subtotal + tax" },
        "ordered_at": { "generator": "timestamp" },
        "delivered_at": {
          "type": "logic",
          "algorithm": "datetime_between",
          "params": { "start": "self.ordered_at", "end": "self.ordered_at + 14days" },
          "nullable": true
        }
      },
      "indexes": [
        { "fields": ["order_number"], "unique": true },
        { "fields": ["user_id"] }
      ]
    },
    "OrderItem": {
      "table": "order_items",
      "fields": {
        "id": { "generator": "uuid", "primary_key": true },
        "order_id": { "ref": "Order.id" },
        "product_id": { "ref": "Product.id" },
        "quantity": { "generator": "quantity" },
        "unit_price": { "type": "template", "pattern": "{{ref:Product.price}}" },
        "line_total": { "computed": "quantity * unit_price" }
      },
      "indexes": [
        { "fields": ["order_id"] },
        { "fields": ["product_id"] }
      ]
    }
  },

  "relationships": [
    { "from": "Category.parent_id", "to": "Category.id", "cardinality": "many-to-one", "nullable": true },
    { "from": "Product.category_id", "to": "Category.id", "cardinality": "many-to-one" },
    { "from": "Order.user_id", "to": "User.id", "cardinality": "many-to-one" },
    { "from": "OrderItem.order_id", "to": "Order.id", "cardinality": "many-to-one", "on_delete": "cascade" },
    { "from": "OrderItem.product_id", "to": "Product.id", "cardinality": "many-to-one" }
  ],

  "scenarios": {
    "development": {
      "User": 50,
      "Category": 10,
      "Product": 100,
      "Order": 200,
      "OrderItem": 500
    },
    "staging": {
      "User": 5000,
      "Category": 50,
      "Product": 2000,
      "Order": 10000,
      "OrderItem": 30000
    },
    "production_mirror": {
      "User": { "count": 100000, "seed": 12345 },
      "Category": 100,
      "Product": 10000,
      "Order": {
        "count": 500000,
        "distribution": { "per_user": "gaussian", "params": { "mean": 5, "stddev": 3 } }
      },
      "OrderItem": {
        "count": 1500000,
        "distribution": { "per_order": "gaussian", "params": { "mean": 3, "stddev": 1 } }
      }
    },
    "load_test": {
      "User": 1000000,
      "Category": 100,
      "Product": 50000,
      "Order": 5000000,
      "OrderItem": 15000000
    }
  }
}

Validation Rules ​

Status: rules touching entities (ref:, circular entity refs) are target design. Built today: envelope/manifest param validation, composition reference + cycle checks at registration, and asset-existence errors at resolution time.

PGDL schemas are validated against these rules:

RuleDescription
Version requiredversion field must be present
Name requiredname field must be present
Valid generatorsAll generator references must exist
Valid refsAll ref: references must point to existing entity fields
No circular refsEntity references cannot be circular
Unique constraintsFields with unique: true must have sufficient entropy
Valid algorithmsLogic generator algorithms must be recognized
Valid sourcesList/model sources must exist in package

Edge Cases & Error Handling ​

Status: the behaviours below are the design contract. Today the engine reports the equivalent errors at load/generation time (unknown generator reference, empty variants, cyclic let references, exhausted unique retries); the polished phony validate CLI diagnostics shown in the shell blocks are target design, as is everything involving entities.

Generator Reference Doesn't Exist ​

bash
$ phony validate schema.pgdl.json

Error: Unknown generator reference 'nonexistent_generator'
  → at entities.User.fields.name.generator
  → Did you mean: 'first_name', 'last_name'?

Circular Template References ​

json
{
  "generators": {
    "a": { "type": "template", "pattern": "{{b}}" },
    "b": { "type": "template", "pattern": "{{a}}" }
  }
}
bash
Error: Circular reference detected
  → a → b → a
  → Break the cycle by using a non-template generator

Empty Variants Array ​

json
{
  "generators": {
    "address": { "type": "template", "variants": [] }
  }
}
bash
Error: Template generator must have at least 1 variant
  → at generators.address.variants

Invalid Locale Code ​

json
{
  "locale": "invalid_LOCALE"
}
bash
Warning: Unknown locale 'invalid_LOCALE'
  → Falling back to 'en_US'
  → Valid formats: 'en_US', 'tr_TR', 'de_DE'

Uniqueness Exhaustion ​

When a unique constraint can't be satisfied:

json
{
  "generators": {
    "status": {
      "type": "list",
      "source": "inline",
      "values": ["a", "b", "c"],
      "unique": true
    }
  }
}

Generating 100 records will fail after 3:

bash
Error: Cannot generate unique value for 'status'
  → 3 unique values available, 100 requested
  → Add more values or remove 'unique: true'

For templates, use unique_fallback:

json
{
  "email": {
    "type": "template",
    "pattern": "{{lowercase(first_name)}}@example.com",
    "unique": true,
    "unique_fallback": "user{{number:10000-99999}}@example.com"
  }
}

Self-Referential Entities ​

Self-referential relationships (like categories with parent) require nullable: true:

json
{
  "entities": {
    "Category": {
      "fields": {
        "id": { "generator": "uuid", "primary_key": true },
        "parent_id": { "ref": "Category.id", "nullable": true }
      }
    }
  }
}

Without nullable: true, you'd have infinite recursion.

Package Dependency Conflicts ​

When two packages require incompatible versions:

bash
Error: Dependency conflict
  → @phony/ecommerce requires @phony/base ^1.0.0
  → @phony/healthcare requires @phony/base ^2.0.0
  → Cannot resolve compatible version

Resolution: Update packages or use explicit version override in manifest


CLI Validation ​

What validation exists today:

  • phony validate <model.ngram> validates a trained model file (format and integrity); an invalid file prints the error and exits with code 4. This is the only validate subcommand in the built CLI.
  • Generator/composition validation happens at load time. Compiling a composition statically checks every reference against its own params and let names (typos fail at registration, not mid-run), orders let steps topologically (cycles are rejected), and validates envelope params against the referenced generator's manifest.
  • Param validation also runs on the execution path — automatically. Every generate / generate_one call (and every sub-generator inside a composition or a repeat step) validates the supplied params against the target generator's manifest before running it: unknown params, wrong types, and missing-required all fail loudly with a BadParams message. This is no longer opt-in behind an explicit validate call — mistyped or misspelled params can no longer be silently ignored and degrade to junk output.
bash
# Validate a trained model file (exit code 4 on failure)
phony validate models/names.ngram

# Loading a package validates its compositions and assets
phony generate --use "@acme/demo:employee_email" --package ./demo --seed 1

Target design (not built): a phony validate schema.pgdl.json schema-file validator with JSON-Schema compliance checks, entity ref: resolution, and source-file existence checks — this arrives with the entity layer on the cloud platform side.

Phony Cloud — Documentation & Specification