# Validating a project

`dcupl validate` is a one-shot health check. It loads a project, classifies any errors it hits, and reports empty or auto-generated models — without starting a long-running daemon. Run it locally against your working files or remotely against a Console version before you push, deploy, or merge.

> **Note:** **Validating remotely?** The `--source remote` mode loads a Console version, so it needs project credentials. Pass `--project-id` / `--api-key` inline, or configure them once with `dcupl config set` (see [Cloud Sync](/cli/cloud-sync)). Local validation needs no credentials.

## Choosing a source

`--source` selects where the project is loaded from. If you omit it, the CLI infers the source from the flags you pass: `--project-id` selects remote, otherwise it is local. For a remote run, pass `--source remote` anyway: the report's `source` block follows the flag, so an inferred remote run is reported as `"mode": "local"`.

**Local**

```bash
dcupl validate --source local --lc-json dcupl/dcupl.lc.json
```

**Remote**

```bash
dcupl validate \
  --source remote \
  --project-id my-project \
  --api-key <api-key> \
  --version draft
```

| Flag | Source | Purpose |
| --- | --- | --- |
| `--source <local\|remote>` | both | Where to load from (inferred if omitted, see above) |
| `--lc-json <path>` | local | Path to the loader config |
| `--project-id <str>` | remote | Console project id |
| `--api-key <str>` | remote | API key |
| `--version <id>` | remote | Version to load |
| `--cdn-base-url <url>` | remote | CDN host to load the version from. Defaults to the dcupl CDN, also when consoleApiUrl points at a self-hosted Console, so set it for a self-hosted Console (also DCUPL\_CDN\_BASE\_URL, or cdnBaseUrl in dcupl.config.json) |

## Processing & performance

Shape how the project is built — apply an application, environments, tags, and variables — and bound the loader's resources.

```bash
dcupl validate \
  --app-key web \
  --env production \
  --tag eu,published \
  --var REGION=eu --var TIER=pro
```

### Processing

| Flag | Purpose |
| --- | --- |
| `--app-key <str>` | Application key to load |
| `--env <key>` | Environment to apply (repeatable) |
| `--tag <tag>` | Tag filter (repeatable; comma-separate for AND) |
| `--var <KEY=VALUE>` | Loader variable (repeatable) |

### Performance

| Flag | Default | Purpose |
| --- | --- | --- |
| `--auto-serve` | true | Auto-spawn dcupl serve to load the project |
| `--max-memory <MB>` | — | Cap the loader's memory |
| `--quality <bool>` | true | Run quality checks |
| `--limit <n>` | 10 | Max error records in the report |

## Reading the report

> **Watch out:** `validate` exits **0 even when the project has errors** — it runs in report mode, and the report's top-level `ok` is always `true` (it only means "a report was produced"). **Do not gate on `ok`.** Parse the JSON and check `summary.critical` (and `load.ok`) instead. When no report can be produced — for example a missing or malformed loader config, or `dcupl serve` not starting — validate prints an error envelope (`{"success":false,"error":…,"code":…}`) and exits 1, so check the exit code too.

Emit machine-readable output with `--json`:

```console title="zsh — dcupl validate"
# one-shot health check, machine-readable
$ dcupl validate --json
{
  "ok": true,
  "source": { "mode": "local", "loaderPath": "dcupl/dcupl.lc.json" },
  "load": { "ok": true },
  "models": { "total": 4, "empty": ["Review"], "autoGenerated": [] },
  "errors": {
    "total": 3,
    "bySeverity": { "critical": 1, "warning": 2 },
    "byGroup": { "ResourceError": 1, "PropertyDataError": 1, "ReferenceDataError": 1 },
    "byType": { "InvalidResource": 1, "WrongDataType": 1, "RemoteReferenceKeyNotFound": 1 },
    "truncatedGroups": [],
    "records": [
      { "title": "http://127.0.0.1: 8083/data/missing.data.json", "model": "Review",
        "errorGroup": "ResourceError", "errorType": "InvalidResource" },
      { "title": "Wrong Data Type", "model": "Order", "attribute": "total",
        "errorGroup": "PropertyDataError", "errorType": "WrongDataType" },
      { "title": "Remote Reference Key Not Found", "model": "Order", "attribute": "customer",
        "errorGroup": "ReferenceDataError", "errorType": "RemoteReferenceKeyNotFound" }
    ]
  },
  "summary": { "critical": 1, "warnings": 2 }
}
```

✗ summary.critical: 1

4 models · load ok

1 critical

2 warnings

- critical

  http://127.0.0.1:8083/data/missing.data.json

  Review · ResourceError / InvalidResource

- warning

  Wrong Data Type

  Order.total · PropertyDataError / WrongDataType

- warning

  Remote Reference Key Not Found

  Order.customer · ReferenceDataError / RemoteReferenceKeyNotFound

Models:

empty · Review

auto-generated · none

The same report, decoded — `errors.records[]` with the severity of each `errorGroup`, plus the **empty** and **auto-generated** model signals (`models.empty`, `models.autoGenerated`).

The counts are exact: `errors.total`, `bySeverity`, `byGroup`, and `byType` are summed from the SDK's `$DcuplErrorTrackingSummary` model. `errors.records[]` is only a sample (`--limit`, default 10), and error tracking stores at most 1000 records per error group. Every group that hit that cap is listed in `errors.truncatedGroups` as `{ model, attribute, errorType, totalCount, storedCount }`, and the text output repeats them under *Truncated groups*. An empty array means nothing was capped.

Gate a script on the exit code, `load.ok` and the critical count — and on `summary.warnings` too when warnings should block:

```bash
# exit 1 = no report (e.g. the loader config could not be loaded)
report=$(dcupl validate --json) || { echo "$report"; exit 1; }

# gate on load + critical count — warnings won't block
loaded=$(echo "$report" | jq -r '.load.ok')
critical=$(echo "$report" | jq -r '.summary.critical')
if [ "$loaded" != "true" ] || [ "$critical" -gt 0 ]; then
  echo "Validation failed: load.ok=$loaded, $critical critical error(s)"; exit 1
fi

# stricter: warnings block too
warnings=$(echo "$report" | jq -r '.summary.warnings')
[ "$warnings" -gt 0 ] && { echo "$warnings warning(s)"; exit 1; }
```

## Error severity

Each record carries an `errorGroup`, `errorType`, `model`, `attribute`, and `title`. Groups roll up into two severities.

| Severity | Error groups | Meaning |
| --- | --- | --- |
| Critical | ModelDefinitionError, ResourceError (and any error group not listed here) | The workspace can't load or a model is malformed — blocks everything downstream. Must fix. |
| Warning | ReferenceDataError, PropertyDataError, DataContainerError | Data-level issues (stray foreign keys, type mismatches). May resolve as more data loads. |

## Next steps

- [Local data apps](/cli/local-data-apps) — Build and query a project interactively
- [Cloud Sync](/cli/cloud-sync) — Push a validated project to the cloud
- [CI/CD integration](/cli/ci-cd) — Gate the deploy on validation
- [Commands](/cli/commands) — All CLI commands
