PDL (Phony Definition Language) Specification
PDL is a declarative JSON-based language for defining data generation schemas. This document provides the complete language specification.
Format Decision: PDL 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.
JSON Schema
All PDL schemas should reference the JSON Schema for IDE support:
{
"$schema": "https://phony.cloud/schemas/pdl-1.0.json",
"version": "1.0",
"name": "My Schema"
}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/pdl-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 | template"
}
},
"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
}
}
}Version Declaration
{
"version": "1.0"
}| Version | Status | Features |
|---|---|---|
1.0 | Current | Full PDL support |
Locale Declaration
{
"locale": "tr_TR"
}The locale affects:
- Which model files are loaded (from locale-specific paths)
- Which list files are loaded
- 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
{
"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
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:
| Algorithm | Parameters | Output |
|---|---|---|
uuid_v4 | - | UUID v4 string |
uuid_v7 | - | UUID v7 string |
ulid | - | ULID string |
nanoid | length (default: 21) | Nano ID string |
int_between | min, max | Integer |
float_between | min, max, precision | Float |
boolean | probability (default: 0.5) | Boolean |
datetime_between | start, end, format | DateTime string |
date_between | start, end, format | Date string |
time_between | start, end, format | Time string |
timestamp | start, end | Unix timestamp |
sequence | start, step | Incrementing integer |
gaussian | mean, stddev | Float (normal dist.) |
exponential | lambda | Float (exp. dist.) |
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 require a generation block specifying the output mode. 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 is required - there is no default mode.Note:
paramscontrols generation behavior (e.g.,starts_with), whileconstraintsvalidates output (e.g.,min_length- rejects if not met).
Template Generator
{
"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
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 | "status = 'paid'" |
Entity Definitions
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
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
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 Example
{
"$schema": "https://phony.cloud/schemas/pdl-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
PDL 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
Generator Reference Doesn't Exist
$ phony validate schema.pdl.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
Use the phony CLI to validate schemas before building:
# Validate a schema file
phony validate schema.pdl.json
# Validate with verbose output
phony validate schema.pdl.json --verbose
# Validate a built package
phony validate my-package.phonyWhat gets validated:
- JSON syntax and structure
- JSON Schema compliance
- Generator references (all
{{generator_name}}must exist) - Entity references (all
ref:must point to valid fields) - Circular dependency detection
- Source file existence (list/model files)
Example output:
$ phony validate schema.pdl.json
✓ JSON syntax valid
✓ Schema structure valid
✓ 12 generators defined
✓ 5 entities defined
✓ All generator references resolved
✓ All entity references resolved
✓ No circular dependencies
✓ All source files exist
Schema valid! Ready to build.