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:
./dv check .
You can also point the plugin-side entrypoint at a theme path directly, which is what the shim ultimately calls:
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.
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:
./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 for how that hook and shim are generated and kept current.