{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://omnify.dev/schemas/omnify-config-schema.json",
  "title": "Omnify Configuration",
  "description": "Configuration file for Omnify (omnify.yaml)",
  "type": "object",
  "properties": {
    "input": {
      "type": "string",
      "description": "Consumer-mode shortcut: path/URL/npm-spec for an upstream schemas.json. When set, omnify-ts skips the connections+migrations indirection and uses this value directly. Accepts a local path (relative to this file), an http(s) URL (cached + pinned via .omnify/input.lock.json), or a scoped npm package specifier (e.g. '@famgia/dxs-product-schemas/schemas.json'). Frontend / consumer projects should use this; backend projects use schemasDir + connections instead."
    },
    "package": {
      "type": "boolean",
      "default": false,
      "description": "Enable package mode: use stable deterministic timestamps for all Laravel migration outputs so the same schema produces the same filenames on every machine. Shorthand for setting `stableTimestamps: true` on every migration config. Issue #60."
    },
    "schemasDir": {
      "type": "string",
      "default": "./schemas",
      "description": "Directory containing schema YAML files"
    },
    "packages": {
      "type": "array",
      "items": {
        "$ref": "#/definitions/PackageConfig"
      },
      "description": "External schema packages to consume (short form: path only, long form: explicit schemasDir + lockFile)"
    },
    "lockFilePath": {
      "type": "string",
      "default": ".omnify/lock.json",
      "description": "Path to the lock file for tracking migration state"
    },
    "connections": {
      "type": "object",
      "additionalProperties": {
        "$ref": "#/definitions/Connection"
      },
      "description": "Database connection configurations"
    },
    "default": {
      "type": "string",
      "default": "default",
      "description": "Default connection name"
    },
    "locale": {
      "$ref": "#/definitions/Locale",
      "description": "Locale settings for multi-language support"
    },
    "compoundTypes": {
      "type": "array",
      "items": {
        "type": "string",
        "enum": ["japan"]
      },
      "description": "Built-in compound type packs to enable (e.g., 'japan' for JapaneseName, JapaneseAddress)"
    },
    "codegen": {
      "$ref": "#/definitions/Codegen",
      "description": "Code generation configuration (connection-independent, reads schemas.json)"
    },
    "file": {
      "$ref": "#/definitions/FileConfig",
      "description": "File upload configuration for polymorphic file attachments"
    },
    "audit": {
      "$ref": "#/definitions/AuditConfig",
      "description": "Audit columns (created_by_id / updated_by_id / deleted_by_id) and audit-log history table feature (issue #94)"
    },
    "workflow": {
      "$ref": "#/definitions/WorkflowConfig",
      "description": "Plan/debug state backend selection. Default 'local' writes plans/ and debugs/ folders; future backends (github, wiki, ...) plug in via internal/workflow.Backend without touching skills."
    },
    "verbose": {
      "type": "boolean",
      "default": false,
      "description": "Enable verbose output during generation"
    }
  },
  "definitions": {
    "FileConfig": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "tempFlow": {
          "type": "boolean",
          "default": false,
          "description": "Enable temporary → permanent file upload flow"
        },
        "tempTtl": {
          "type": "string",
          "description": "Temporary file TTL (e.g., '24h', '12h')"
        },
        "cleanupSchedule": {
          "type": "string",
          "description": "Phase 1 schedule: soft-delete expired temp records (e.g., 'hourly')"
        },
        "purgeAfter": {
          "type": "string",
          "description": "Grace period before physical file deletion (e.g., '72h')"
        },
        "purgeSchedule": {
          "type": "string",
          "description": "Phase 2 schedule: force-delete + remove physical files (e.g., 'daily')"
        },
        "defaultDisk": {
          "type": "string",
          "description": "Default storage disk (e.g., 'public', 's3', 'local')"
        }
      }
    },
    "WorkflowConfig": {
      "type": "object",
      "additionalProperties": true,
      "properties": {
        "backend": {
          "type": "string",
          "default": "local",
          "description": "Backend kind: 'local' (default) writes to plans/ and debugs/ folders. Future values (github, wiki, ...) supported by newer omnify versions; older binaries fall back to local with a warning rather than crashing."
        },
        "local": {
          "type": "object",
          "additionalProperties": false,
          "description": "LocalBackend overrides — only consulted when backend is 'local' or omitted.",
          "properties": {
            "plansDir": {
              "type": "string",
              "default": "plans",
              "description": "Directory holding plan-NNN/ subdirectories, relative to project root."
            },
            "debugsDir": {
              "type": "string",
              "default": "debugs",
              "description": "Directory holding debug-NNN/ subdirectories, relative to project root."
            }
          }
        }
      }
    },
    "AuditConfig": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "model": {
          "type": "string",
          "description": "Schema name of the actor / user model (must have options.authenticatable: true). Required when log: true."
        },
        "createdBy": {
          "type": "boolean",
          "default": false,
          "description": "Global default for the created_by_id audit column. Per-schema options.audit.createdBy overrides."
        },
        "updatedBy": {
          "type": "boolean",
          "default": false,
          "description": "Global default for the updated_by_id audit column. Per-schema options.audit.updatedBy overrides."
        },
        "deletedBy": {
          "type": "boolean",
          "default": false,
          "description": "Global default for the deleted_by_id audit column. Per-schema options.audit.deletedBy overrides."
        },
        "log": {
          "type": "boolean",
          "default": false,
          "description": "Toggle the audits-history table feature globally (issue #94). Per-schema options.audit.log overrides. Records every model lifecycle event into a single polymorphic audits table."
        },
        "logExclude": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Additional sensitive columns scrubbed from old_values / new_values BEFORE audit row is written. Built-in defaults (password, remember_token, api_token, two_factor_secret, two_factor_recovery_codes) are always merged in — your list ADDS to them, never replaces."
        },
        "logRetention": {
          "type": "string",
          "pattern": "^[0-9]+[dwmy]$",
          "description": "Prunable retention period: '90d', '12w', '6m', '1y'. Empty string keeps audits forever (no scheduled prune)."
        },
        "logQueue": {
          "type": "string",
          "description": "Laravel queue connection name for WriteAuditLog dispatch. Empty string forces synchronous dispatch — only acceptable in dev / low-traffic apps. Production should set a queue backed by a real driver (redis, sqs, etc.)."
        }
      }
    },
    "PackageConfig": {
      "type": "object",
      "properties": {
        "path": {
          "type": "string",
          "description": "Path to the package root directory (short form: auto-discovers omnify.yaml inside)"
        },
        "name": {
          "type": "string",
          "description": "Package identifier. Defaults to directory basename (short form) or required (long form)"
        },
        "schemasDir": {
          "type": "string",
          "description": "Path to schemas directory (long form only, required when path is not set)"
        },
        "lockFile": {
          "type": "string",
          "description": "Path to package lock file (long form only, required when path is not set)"
        },
        "codegen": {
          "$ref": "#/definitions/Codegen",
          "description": "Codegen overrides for this package (merged with package's own codegen config)"
        },
        "inline": {
          "type": "boolean",
          "default": false,
          "description": "Schemas-only package mode (issue #94). When true, the package ships ONLY YAML schemas — no omnify.yaml, no lock.json, no migrations/ tree. The host project owns migrations + lock + codegen for this package's schemas. Requires `path` or `schemasDir`; forbids `lockFile`."
        }
      }
    },
    "Connection": {
      "type": "object",
      "required": ["driver"],
      "properties": {
        "driver": {
          "type": "string",
          "enum": ["mysql", "pgsql", "postgres", "sqlite"],
          "description": "Database driver"
        },
        "migrations": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/Migration"
          },
          "description": "Migration output configurations"
        }
      }
    },
    "Migration": {
      "type": "object",
      "required": ["type", "path"],
      "properties": {
        "type": {
          "type": "string",
          "enum": ["laravel", "sql"],
          "description": "Migration output type"
        },
        "path": {
          "type": "string",
          "description": "Output directory for migration files"
        },
        "schemasPath": {
          "type": "string",
          "description": "Path for exported schemas JSON (laravel only)"
        },
        "dialect": {
          "type": "string",
          "enum": ["mysql", "postgresql", "sqlite"],
          "description": "SQL dialect for generated migrations (sql only)"
        },
        "stableTimestamps": {
          "type": "boolean",
          "default": false,
          "description": "Use deterministic timestamps derived from schema order instead of clock time. For packages published to Packagist — ensures identical filenames on every machine. Timestamps use a fixed year-2000 base so they always sort before consumer migrations. Issue #60."
        },
        "options": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "enumStrategy": {
              "type": "string",
              "enum": ["string", "native", "conditional"],
              "default": "string",
              "description": "Enum column strategy: 'string' (cross-DB, default), 'native' (MySQL ENUM), or 'conditional' (runtime if/else)"
            }
          }
        }
      },
      "allOf": [
        {
          "if": {
            "properties": { "type": { "const": "laravel" } }
          },
          "then": {
            "properties": {
              "schemasPath": { "type": "string" }
            }
          }
        },
        {
          "if": {
            "properties": { "type": { "const": "sql" } }
          },
          "then": {
            "properties": {
              "dialect": { "type": "string", "enum": ["mysql", "postgresql", "sqlite"] }
            }
          }
        }
      ]
    },
    "Codegen": {
      "type": "object",
      "properties": {
        "typescript": {
          "description": "TypeScript codegen configuration. Single-target legacy form (object) or multi-target form (array of named targets). Issue #43.",
          "oneOf": [
            { "$ref": "#/definitions/CodegenTypeScript" },
            {
              "type": "array",
              "items": { "$ref": "#/definitions/CodegenTypeScript" }
            }
          ]
        },
        "laravel": {
          "description": "Laravel PHP codegen configuration. Single-target legacy form (object) or multi-target form (array of named targets). Issue #43.",
          "oneOf": [
            { "$ref": "#/definitions/CodegenLaravel" },
            {
              "type": "array",
              "items": { "$ref": "#/definitions/CodegenLaravel" }
            }
          ]
        },
        "go": {
          "description": "Go codegen configuration: plain structs + migrations, with optional uptrace/bun tag emission. Issue omnify-jp/omnify-go#65 + #103.",
          "oneOf": [
            { "$ref": "#/definitions/CodegenGo" },
            {
              "type": "array",
              "items": { "$ref": "#/definitions/CodegenGo" }
            }
          ]
        }
      }
    },
    "CodegenGo": {
      "type": "object",
      "properties": {
        "name": { "type": "string", "description": "Target identifier (used by --target CLI flag)." },
        "enable": { "type": "boolean", "description": "Enable this Go target. Default false." },
        "rootPath": { "type": "string", "description": "Root path for generated Go files (e.g. './backend')." },
        "database": {
          "type": "string",
          "enum": ["sqlite", "mysql", "mariadb", "postgres"],
          "description": "SQL dialect used by the migrations generator. Default 'sqlite'."
        },
        "modulePath": {
          "type": "string",
          "description": "Go module path (e.g. 'github.com/org/app'). When empty, omnify reads go.mod under rootPath."
        },
        "domain":     { "$ref": "#/definitions/CodegenGoPath" },
        "enums":      { "$ref": "#/definitions/CodegenGoPath" },
        "migrations": { "$ref": "#/definitions/CodegenGoPath" },
        "sqlc":       { "$ref": "#/definitions/CodegenGoPath" },
        "repo":       { "$ref": "#/definitions/CodegenGoPath", "description": "Trivial CRUD repository generator (Phase 2). Emits <schema>_repo.go per object schema with Get/Insert/Update/Delete/List backed by Bun, plus shared errors.go (ErrNotFound)." },
        "schemas": {
          "type": "object",
          "properties": {
            "include": { "type": "array", "items": { "type": "string" } },
            "exclude": { "type": "array", "items": { "type": "string" } }
          }
        },
        "bun": {
          "type": "object",
          "description": "Bun ORM (uptrace/bun) tag emission. When enable=true, generated structs gain bun:\"...\" tags + an embedded bun.BaseModel.",
          "properties": {
            "enable": { "type": "boolean", "description": "Turn Bun tag emission on. Default false." },
            "alias": { "type": "string", "description": "Table alias on bun.BaseModel. Empty → derive from schema name. '-' suppresses alias." },
            "baseModelImport": { "type": "string", "description": "Import path for bun.BaseModel. Default 'github.com/uptrace/bun'." },
            "softDeleteTag": { "type": "string", "description": "bun tag fragment used on soft-delete columns. Default ',soft_delete,nullzero'." }
          }
        },
        "migrationStyle": {
          "type": "string",
          "enum": ["inline", "numbered_files"],
          "description": "How migrations are emitted. 'inline' → generated/<pkg>/migrations.go with Migrations []string slice. 'numbered_files' → migrations/NNNN_*.up.sql + .down.sql (golang-migrate compatible). Default depends on database."
        },
        "jsonTags": { "type": "boolean", "description": "Emit json:\"...\" struct tags. Default true." },
        "omitemptyForNullable": { "type": "boolean", "description": "Append ,omitempty to json tags for nullable / pointer fields. Default true." },
        "emitTranslations": { "type": "boolean", "description": "Auto-generate <entity>_translations sidecar tables + Go structs (Bun has-many relation when bun.enable=true) for properties marked translatable: true. Default true when any translatable field is present." }
      },
      "additionalProperties": false
    },
    "CodegenGoPath": {
      "type": "object",
      "properties": {
        "outputPath":  { "type": "string" },
        "packageName": { "type": "string" }
      },
      "additionalProperties": false
    },
    "CodegenTypeScript": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "Target identifier (used by --target CLI flag in v2). Defaults to 'default' when omitted in legacy single-object form."
        },
        "enable": {
          "type": "boolean",
          "default": false,
          "description": "Enable this TypeScript target. Defaults to false."
        },
        "modelsPath": {
          "type": "string",
          "description": "Output directory for generated TypeScript model files (legacy alias for `output`; either is accepted, `output` wins when both are set)"
        },
        "output": {
          "type": "string",
          "description": "Output directory for generated TypeScript model files. Preferred field name; symmetrical with the top-level `input` field used by consumer-mode configs."
        },
        "platform": {
          "type": "string",
          "enum": ["web", "expo"],
          "default": "web",
          "description": "Target runtime platform. 'web' (default) emits browser-friendly code; 'expo' adapts for React Native / Expo (AppState focusManager, async auth headers). Issue #63."
        },
        "auth": {
          "type": "string",
          "enum": ["cookie", "secureStore"],
          "default": "cookie",
          "description": "Auth header strategy for generated service layer. 'cookie' (default) uses browser cookies; 'secureStore' uses Expo SecureStore for async bearer token auth. Issue #63."
        },
        "includeGroups": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Schema groups to include in this target (e.g. ['Inventory', 'Product']). Combined with includeSchemas / excludeSchemas. Issue #43."
        },
        "includeSchemas": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Allow-list of schema names to include in this target. Combined with includeGroups."
        },
        "excludeSchemas": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Deny-list of schema names to exclude from this target (always overrides includes)."
        }
      }
    },
    "CodegenLaravel": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "Target identifier (used by --target CLI flag in v2). Defaults to 'default' when omitted in legacy single-object form."
        },
        "includeGroups": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Schema groups to include in this target. Issue #43."
        },
        "includeSchemas": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Allow-list of schema names to include in this target."
        },
        "excludeSchemas": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Deny-list of schema names to exclude (overrides includes)."
        },
        "enable": {
          "type": "boolean",
          "default": false,
          "description": "Enable Laravel PHP codegen. Defaults to false."
        },
        "rootPath": {
          "type": "string",
          "description": "Filesystem prefix applied to all generated paths. Use this for monorepo setups where Laravel lives in a subdirectory (e.g. 'backend' makes everything write under backend/app/...)."
        },
        "structure": {
          "type": "string",
          "enum": ["legacy", "modular"],
          "default": "legacy",
          "description": "Directory layout for generated base files. 'modular' uses app/Modules/{Schema}/."
        },
        "layout": {
          "type": "string",
          "enum": ["legacy", "canonical"],
          "default": "legacy",
          "description": "High-level layout convention (issue #98, v5.7+). 'legacy' (default) keeps editable stubs in the same dir as the base. 'canonical' splits bases under `app/Omnify/<Layer>/` and editable stubs at canonical Laravel paths (`app/Models/`, `app/Http/Requests/`, etc.) — a one-line shortcut for the canonical opt-in. Per-layer userEditablePath/Namespace overrides win. v6.0 plan: flip default to 'canonical'."
        },
        "model": {
          "$ref": "#/definitions/CodegenLaravelPath",
          "description": "Model output configuration (default: app/Models/Omnify)"
        },
        "request": {
          "$ref": "#/definitions/CodegenLaravelPath",
          "description": "Request output configuration (default: app/Http/Requests)"
        },
        "resource": {
          "$ref": "#/definitions/CodegenLaravelPath",
          "description": "Resource output configuration (default: app/Http/Resources)"
        },
        "factory": {
          "$ref": "#/definitions/CodegenLaravelPath",
          "description": "Factory output configuration (default: database/factories)"
        },
        "provider": {
          "$ref": "#/definitions/CodegenLaravelPath",
          "description": "Provider output configuration (default: app/Providers)"
        },
        "policy": {
          "$ref": "#/definitions/CodegenLaravelPath",
          "description": "Policy output configuration (default: app/Policies/Omnify)"
        },
        "controller": {
          "$ref": "#/definitions/CodegenLaravelPath",
          "description": "Controller output configuration (default: app/Http/Controllers)"
        },
        "service": {
          "$ref": "#/definitions/CodegenLaravelPath",
          "description": "Service output configuration (default: app/Services)"
        },
        "route": {
          "$ref": "#/definitions/CodegenLaravelPath",
          "description": "Route file output configuration (default: routes/api/omnify)"
        },
        "config": {
          "$ref": "#/definitions/CodegenLaravelPath",
          "description": "Path for the omnify-schemas.php config file (default: config/omnify-schemas.php)"
        }
      }
    },
    "CodegenLaravelPath": {
      "type": "object",
      "properties": {
        "enable": {
          "type": "boolean",
          "default": true,
          "description": "Enable generation for this layer. Currently honored by the Laravel service layer; set service.enable to false to keep other Laravel artifacts while emitting no services."
        },
        "path": {
          "type": "string",
          "description": "Output directory for BASE (auto-regenerated) class."
        },
        "namespace": {
          "type": "string",
          "description": "PHP namespace for BASE class (default: derived from path)."
        },
        "userEditablePath": {
          "type": "string",
          "description": "Output directory for USER-EDITABLE stub. Defaults to `path`. Set to e.g. `app/Models` to put stubs at canonical Laravel locations while bases stay isolated under `app/Omnify/`. Issue #96, v5.4+."
        },
        "userEditableNamespace": {
          "type": "string",
          "description": "Namespace for USER-EDITABLE stub. Defaults to `namespace`. Issue #96, v5.4+."
        },
        "flatBase": {
          "type": "boolean",
          "default": false,
          "description": "Drop the `Base/` (or `OmnifyBase/`) subfolder + `*Base*` class suffix. Base lives at `path` with class name `<Schema>` (models) or `<Schema><Layer>` (service / resource / etc.). Editable stub extends the bare base FQN. Recommended when `path` already isolates generated code (e.g. `app/Omnify/Models/`). Issue #96, v5.4+."
        },
        "userEditableGroupByFolder": {
          "type": "boolean",
          "default": false,
          "description": "Mirror the schema's group folder onto the USER-EDITABLE layer's path + namespace (issue #98 v5.8.5). Default false — Laravel-canonical flat editable (e.g. `app/Models/User.php`, `App\\Models\\User`); group is encoded only in the base layer + the `use ... as Base` import. Set true to restore v5.8.x grouped editable shape (e.g. `app/Models/Auth/User.php`, `App\\Models\\Auth\\User`). Per layer."
        }
      }
    },
    "Locale": {
      "type": "object",
      "properties": {
        "locales": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "List of supported locale codes (e.g., ['ja', 'en', 'vi'])"
        },
        "defaultLocale": {
          "type": "string",
          "description": "Default locale code"
        },
        "fallbackLocale": {
          "type": "string",
          "description": "Fallback locale when translation is missing"
        }
      }
    }
  }
}
