# dv check

`dv check` validates a Divine theme. You run it from the theme root as `./dv check .`, or against any theme directory as `dv check <theme-path>`. It parses the theme's files, runs a registry of rules across them, and reports every finding it sees with a stable identifier and a file and line. The command is the same one the pre-commit hook runs and the same one you run by hand, so a clean local check and a clean committed check mean the same thing.

The command is built to be safe to run anywhere. In its default fast mode it does not bootstrap WordPress, open a network connection, or shell out to another process, which is what lets it run on every commit and inside CI without a live site. A `--full` mode is available when you want the extra WordPress render lane, but it is opt-in precisely because it needs a booted WordPress and Blockstudio runtime.

## Running The Check

The minimal invocation checks the current directory from inside the theme:

```bash
./dv check .
```

You can also point the plugin-side entrypoint at a theme path directly, which is what the shim ultimately calls:

```bash
bin/divine-check wp-content/themes/my-theme
```

Both forms accept a single theme path. Passing more than one path, an unknown option, or no path at all is a usage error.

## Exit Codes

`dv check` communicates its result through the process exit code, so a hook or a CI step can branch on it without parsing output. There are exactly three codes.

| Exit code | Meaning |
| --- | --- |
| `0` | The check ran and found nothing. The theme passed. |
| `1` | The check ran and reported one or more findings. |
| `2` | A usage error or an internal error. The check did not produce a clean result. |

A usage error is something like a missing path, an unknown flag, more than one path, or an empty `--rules` or `--layers` value. An internal error is a thrown exception during analysis, or, under `--full`, a missing WordPress bootstrap. Treat `2` as "the check could not run", distinct from `1`, which means "the check ran and the theme has problems".

## Fast Mode And Full Mode

Fast mode is the default. It runs the static layers entirely in-process against the theme's files: no WordPress, no network, no subprocess. This is the mode the pre-commit hook uses and the mode you want for routine local checks, because it is fast and has no environment requirements beyond PHP and the bundled Divine runtime.

Full mode is opt-in with `--full`. It adds the WordPress render layer, which performs a render dry-run of each block, and to do that it bootstraps WordPress in-process. If a booted WordPress with Blockstudio is already present, the command reuses it; otherwise it loads WordPress from the `DIVINE_WORDPRESS_BOOTSTRAP` path if you set one, or by walking up from the theme path to find a `wp-load.php`. If it cannot find a bootstrap, `--full` fails with exit code 2 and tells you to run inside wp-env or set `DIVINE_WORDPRESS_BOOTSTRAP`. Run full mode inside wp-env or CI, not from a bare shell.

## Flags

The flags below shape what runs and how results are printed. Mode flags are last-wins, so a later `--fast` overrides an earlier `--full` and vice versa.

| Flag | Effect |
| --- | --- |
| `--fast` | Run only the static layers in-process. No WordPress, network, or subprocess. This is the default. |
| `--full` | Add the WordPress render layer and bootstrap WordPress in-process to run it. |
| `--json` | Emit the result as a JSON payload with a target, a summary, and the findings, instead of plain text. |
| `--layers=<list>` | Run only the named layers. Accepts `divine`, `blockstudio`, `tailwind`, and `wp` (an alias for the WordPress layer), comma-separated. |
| `--rules=<list>` | Keep only findings whose identifier is in the comma-separated list; all other findings are dropped from the report. |
| `--help`, `-h` | Print usage and exit `0`. |

When you combine `--layers` with `--full`, the WordPress layer is added to your selection so the render lane still runs. An empty `--layers` or `--rules` value is rejected as a usage error rather than silently meaning "everything".

## Validation Layers

The check is organized into four layers. Fast mode runs the first three; the fourth runs only under `--full` or when you name it. Selecting layers with `--layers` lets you narrow a run, for example to iterate on Tailwind alone.

| Layer | Token | Runs in fast mode | What it covers |
| --- | --- | --- | --- |
| Divine | `divine` | Yes | Theme project shape, `divine.json`, semantic pages, element mapping, Tailwind tokens, output escaping, the asset contract, and forbidden patterns. |
| Blockstudio | `blockstudio` | Yes | Native Blockstudio contracts: `block.json`, `page.json`, `field.json`, schemas, hooks, settings paths, block tags, and attribute access. |
| Tailwind | `tailwind` | Yes | Compiles the theme's Tailwind against the bundled TailwindPHP and flags unknown utilities. |
| WordPress | `wordpress` (alias `wp`) | No | A render dry-run of each block against a booted WordPress and Blockstudio runtime. |

## The Rule Catalog

Each layer registers a set of rules, and each rule emits findings under one or more stable identifiers. The table below is the working catalog of what the check enforces. Identifiers in the `divine.*` namespace come from Divine's own rules; identifiers in the `blockstudio.*` namespace come from the native Blockstudio contract rules.

| Rule | Layer | Identifiers | What it catches |
| --- | --- | --- | --- |
| Theme project | `divine` | `divine.theme.textDomain`, `divine.theme.pageJson.missing`, `divine.performance.config`, `divine.blockstudio.config` | A malformed theme project: wrong text domain, a page directory with `index.php` but no `page.json`, and invalid `divine.json` or `blockstudio.json` shape. |
| Function prefix | `divine` | `divine.function.prefix` | A function declared in the theme's `functions.php` that does not start with the theme's required function prefix. |
| Raw $wpdb write | `divine` | `divine.data.rawWpdbWrite` | A direct `$wpdb->insert()`, `update()`, `delete()`, or `query()` write instead of a WordPress API. |
| Forbidden patterns | `divine` | `divine.check.forbiddenFunction`, `divine.check.registerPostType` | Calls to `exec`, `system`, `shell_exec`, `passthru`, `proc_open`, or `popen`, and direct `register_post_type()` calls in a file/block theme. |
| Forbidden eval | `divine` | `divine.check.forbiddenFunction` | Use of the `eval()` language construct in theme code. |
| Block tag alias | `divine` | `divine.blockTag.alias` | A direct `<bs:bsui-*>` authoring tag where the Divine `<dv-*>` alias should be used. |
| Semantic pages | `divine` | `divine.theme.serializedBlocks`, `divine.blockTag.unknown` | Serialized `<!-- wp:... -->` block comments in a file-backed page, and unknown `<dv-*>` prefix tags that resolve to no real block. |
| Element mapping | `divine` | `divine.theme.elementMapping` | A `register_block_type()` call inline in a template or `functions.php` instead of the theme's element-mapping file. |
| Tailwind semantic tokens | `divine` | `divine.tailwind.semanticToken` | A hardcoded `bg-[#...]` or `bg-[rgb(...)]` value that exactly matches a `--color-*` token defined in the theme. |
| Output escaping | `divine` | `divine.output.unescaped` | A dynamic Blockstudio attribute echoed or printed without an escaping function such as `esc_html` or `wp_kses_post`. |
| Asset contract | `divine` | `divine.theme.manualEnqueue`, `divine.blockStyle.selectorScope`, `divine.field.default`, `divine.field.repeaterBounds` | Manual `wp_enqueue_*` in block PHP, a block `style.scss` missing `%selector%` scoping, a field with no `default`, and a repeater without integer min/max bounds. |
| Native Blockstudio | `blockstudio` | `blockstudio.blockJson*`, `blockstudio.pageJson*`, `blockstudio.fieldJson*`, `blockstudio.extensionJson*`, `blockstudio.dbSchema*`, `blockstudio.rpcSchema*`, `blockstudio.cronSchema*`, `blockstudio.hook`, `blockstudio.settingsPath`, `blockstudio.blockTag.unknown`, `blockstudio.blockTag.attribute`, `blockstudio.templateField` | Native Blockstudio contract violations across `block.json`, `page.json`, `field.json`, extension JSON, and the `db.php`, `rpc.php`, and `cron.php` schemas, plus unknown hooks, settings paths, block tags, and template fields. |
| Blockstudio attribute access | `blockstudio` | `blockstudio.field` | An `$a['...']` or `$attributes['...']` access for a field that does not exist in the block's `block.json`, with a "did you mean" suggestion. |
| Tailwind | `tailwind` | `divine.tailwind.unknownUtility`, `divine.tailwind.compile` | A class that looks like a Tailwind utility but compiles to nothing, and any failure of the bundled Tailwind compiler itself. |
| WordPress render | `wordpress` | `divine.wp.render`, `divine.wp.slowRender`, `divine.wp.unavailable` | A block that errors during the render dry-run, a block whose render takes longer than the slow threshold, and the case where `--full` ran without a booted WordPress runtime. |

## How Findings Are Reported

By default `dv check` prints one line per finding in a fixed format: the file, a colon and line number, the message, and the identifier in brackets.

```text
wp-content/themes/my-theme/pages/home/index.php:14: Unknown Divine block prefix tag "<dv-hreo>" in file-backed page. [divine.blockTag.unknown]
```

The identifier is the part you key automation off, because it is stable across versions and messages. Identifiers read as dotted paths, layer to area to specific check, such as `divine.tailwind.unknownUtility`, `divine.output.unescaped`, or `blockstudio.field`. When the run is clean, the command prints `No findings.` and exits `0`.

With `--json` the command emits a structured payload instead. It carries a `target` block describing the theme, a `summary` with the finding and error counts and an `ok` flag, and a `findings` array where each entry has the `message`, `identifier`, `file`, `line`, and `severity`. The exit code is unchanged: `0` when the findings array is empty, `1` when it is not.

## Filtering To Specific Rules

When you are working on one class of problem, `--rules` lets you focus the report. Pass a comma-separated list of identifiers, and the command keeps only findings whose identifier is in that list:

```bash
./dv check . --rules=divine.tailwind.unknownUtility,divine.output.unescaped
```

Filtering happens after analysis, so it changes only what is printed, not what is computed; the exit code still reflects whether any of the kept findings exist. An empty list is a usage error, which keeps `--rules=` from being mistaken for "match everything".

## The Pre-Commit Gate

The theme harness installs a `.githooks/pre-commit` hook that runs `./dv check .` and sets git's `core.hooksPath` to `.githooks` so the hook fires. Because the hook uses fast mode, it bootstraps nothing and stays fast enough for every commit, and because it returns the check's exit code, a finding fails the commit. This is what keeps invalid pages, blocks, Tailwind, and Blockstudio contracts out of history at the point they would enter it. CI can run `./dv check --full .` inside wp-env to add the render dry-run on top of the same checks. See [The Theme Harness](theme-harness) for how that hook and shim are generated and kept current.
