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:
{
"$schema": "./conformance/pgdl-schema.json",
"version": "1.0",
"name": "My Schema"
}Note: there is no hosted
https://phony.cloud/schemas/…URL yet — use the generatedconformance/pgdl-schema.jsonfrom thephony-corerepo (or a local copy). The examples on this page keep a placeholder$schemaURL 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
{
"$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
generatorskey is the built generator layer (runnable with the engine today, plususereferences to compositions). Theentities,relationships, andscenarioskeys are the cloud-platform target design — the engine has no entity layer (see the status callout above and the labeled sections below).
Version Declaration
{
"version": "1.0"
}| Version | Status | Features |
|---|---|---|
1.0 | Current | Full PGDL support |
Locale Declaration
{
"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
inheritskeyword is target design. Today layering is achieved by loading packages additively: the project's dependency closure loads leaves-first, and later packages (or explicit--packagelayers) extend/override earlier contributions per(asset, locale).
{
"inherits": "base"
}Or with scoped packages:
{
"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
{
"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.*):
| Algorithm | Parameters | Output |
|---|---|---|
uuid_v4 | - | UUID v4 string |
uuid_v7 | - | UUID v7 string (time-sortable, synthetic seed-derived timestamp) |
ulid | - | ULID string (Crockford base32) |
nanoid | length (default: 21) | Nano ID string |
int_between | min, max, except | Integer |
float_between | min, max, precision | Float |
boolean | probability (default: 0.5) | Boolean |
datetime_between | start, end | ISO-8601 datetime string |
date_between | start, end | ISO-8601 date string |
time_between | start, end | Time string |
timestamp | start, end | Unix timestamp |
sequence | start, step | Incrementing integer (start + row × step) |
gaussian | mean, stddev | Float (normal dist.) |
exponential | lambda | Float (exp. dist.) |
digits | digits, strict, leading_zero | Fixed-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 insideuuid_v7/ulidare seed-derived (synthetic), not wall-clock — that keeps them reproducible.
List Generator
{
"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.
{
"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. Thegenerationblock defaults towordmode; set it explicitly for the others.Note:
paramscontrols generation behavior (e.g.,starts_with), whileconstraintsvalidates 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.
{
"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.
{
"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:
| Mode | Description | Parameters |
|---|---|---|
categorical | Preserve frequency distribution | values with weights |
continuous | Follow statistical distribution | distribution, params, constraints |
algebraic | Preserve mathematical relationships | columns, relationship |
multivariate | Preserve correlations | columns, 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.
{
"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.
{
"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:
| Option | Description | Example |
|---|---|---|
base | The anchor event, generated first | true |
after | This event occurs after specified event | "created_at" |
delay | Time range between events | { "min": "1d", "max": "7d" } |
probability | Chance this event occurs (null if < 1.0) | 0.85 |
condition | Only 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).
{
"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
{
"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.
{
"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
| Type | Description | Example |
|---|---|---|
one-to-one | Each A has exactly one B | User ↔ Profile |
one-to-many | Each A has many Bs | User → Orders |
many-to-one | Many As belong to one B | Orders → User |
many-to-many | Many As to many Bs | Products ↔ 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.
{
"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):
{
"type": "logic",
"algorithm": "int_between",
"params": { "min": 18, "max": 85 },
"nullable": 0.1
}phony generate age.json --seed 42 -n 5 -f jsonlA 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:
{
"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"]
}
}
}
}# 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 jsonlThe 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.
{
"$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:
| Rule | Description |
|---|---|
| Version required | version field must be present |
| Name required | name field must be present |
| Valid generators | All generator references must exist |
| Valid refs | All ref: references must point to existing entity fields |
| No circular refs | Entity references cannot be circular |
| Unique constraints | Fields with unique: true must have sufficient entropy |
| Valid algorithms | Logic generator algorithms must be recognized |
| Valid sources | List/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, cyclicletreferences, exhausteduniqueretries); the polishedphony validateCLI diagnostics shown in the shell blocks are target design, as is everything involving entities.
Generator Reference Doesn't Exist
$ 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
{
"generators": {
"a": { "type": "template", "pattern": "{{b}}" },
"b": { "type": "template", "pattern": "{{a}}" }
}
}Error: Circular reference detected
→ a → b → a
→ Break the cycle by using a non-template generatorEmpty Variants Array
{
"generators": {
"address": { "type": "template", "variants": [] }
}
}Error: Template generator must have at least 1 variant
→ at generators.address.variantsInvalid Locale Code
{
"locale": "invalid_LOCALE"
}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:
{
"generators": {
"status": {
"type": "list",
"source": "inline",
"values": ["a", "b", "c"],
"unique": true
}
}
}Generating 100 records will fail after 3:
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:
{
"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:
{
"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:
Error: Dependency conflict
→ @phony/ecommerce requires @phony/base ^1.0.0
→ @phony/healthcare requires @phony/base ^2.0.0
→ Cannot resolve compatible versionResolution: 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 onlyvalidatesubcommand in the built CLI.- Generator/composition validation happens at load time. Compiling a composition statically checks every reference against its own
paramsandletnames (typos fail at registration, not mid-run), ordersletsteps 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_onecall (and every sub-generator inside a composition or arepeatstep) validates the suppliedparamsagainst the target generator's manifest before running it: unknown params, wrong types, and missing-required all fail loudly with aBadParamsmessage. 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.
# 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 1Target 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.