# Validator Output Schema

Universal JSON. Every adapter emits this shape. Validator core never parses tool-specific output.

## Schema

```json
{
  "tool": "phplint",
  "file": "src/Foo.php",
  "ok": true,
  "count": 0,
  "errors": [],
  "duration_ms": 42
}
```

## Fields

| Field         | Type             | Required | Notes                                                                 |
|---------------|------------------|----------|-----------------------------------------------------------------------|
| `tool`        | string           | yes      | Adapter id (`phplint`, `phpstan`, `phpunit`, `prettier`, ...)         |
| `file`        | string           | yes      | Path validated. Repo-relative.                                        |
| `ok`          | bool             | yes      | Pass/fail. Validator rolls back on `false` if op marked rollback.     |
| `count`       | int              | yes      | Issue count. Used for before/after diff arithmetic.                   |
| `errors`      | array of objects | yes      | `[]` when ok. Each: `{line, col, severity, code, msg}`.               |
| `duration_ms` | int              | yes      | Wall time. For perf tuning.                                           |
| `metrics`     | object           | no       | Tool-specific counters (`tests_total`, `tests_passed`, etc.). Numeric values. Used by renderer for before/after diff on metric keys even when `count` is unchanged. |

| `diff`        | string           | no       | Unified diff produced by the tool (e.g. rector). Rendered as a fenced block below all errors in verbose mode. Ignored in default mode. |

### Error object

| Field            | Type             | Required | Notes                                              |
|------------------|------------------|----------|----------------------------------------------------|
| `line`           | int \| null      | yes      | 1-indexed. `null` if tool gives no location.       |
| `col`            | int \| null      | yes      | 1-indexed. `null` if not provided.                 |
| `severity`       | string           | yes      | `error` \| `warning` \| `info`                     |
| `code`           | string \| null   | yes      | Rule id (`missingType`, `PSR12.Files...`). Nullable. |
| `msg`            | string           | yes      | Human message. Single line preferred.              |
| `source_context` | array of strings | no       | Source lines near the error. The line containing the error uses `→` as separator; surrounding lines use `:`. Example: `["40:     return foo;", "41: ", "42→     bar();", "43: }", "44: "]`. Rendered indented under the error in verbose mode. Ignored in default mode. |

## Contract

- Adapter **always exits 0** if it produced JSON (even on tool failure). Core reads `.ok`.
- Adapter exits non-zero only on infrastructure failure (tool missing, file unreadable). Core treats as `{ok: false, errors: [{msg: "adapter failed"}]}`.
- Adapter writes **one JSON object on stdout**. Nothing else. Logs go to stderr.
- Input: one arg = file path.

## Spec fields (`.supertool.json` validators/formatters entries)

| Field             | Type             | Required | Notes                                                                                     |
|-------------------|------------------|----------|-------------------------------------------------------------------------------------------|
| `cmd`             | string           | yes      | Shell command. `{file}` and `{supertool_dir}` are substituted before execution.           |
| `hooks_into`      | array of strings | yes      | Ops that trigger this validator automatically (`edit`, `paste`, `vim`, ...).              |
| `match`           | string           | no       | Glob pattern to filter by filename (e.g. `*.php`). Matches all files when absent.        |
| `exclude`         | string or list   | no       | Glob (or list of globs) to skip even when `match` matches (e.g. `*tests/*`). Skip if any matches. |
| `timeout`         | int              | no       | Seconds before the subprocess is killed. Default 60 (validators), 30 (formatters).       |
| `rollback_on_fail`| bool             | no       | Revert the file if the validator reports a regression. Default false.                     |
| `opt_in`          | bool             | no       | When true, validator only runs when explicitly requested (not on every hook).             |
| `resolve`         | string           | no       | Shell command that maps the edited file to the file the adapter should receive.           |
| `env`             | object           | no       | Extra environment variables merged into the subprocess env (`os.environ \| spec.env`). Values are coerced to strings. Example: `{"PHPSTAN_LEVEL": "8", "PHPSTAN_CONFIG": "phpstan.neon"}`. |
| `engine_glitches` | list of strings  | no       | Adapter-specific (rector-mcp). Case-sensitive substrings identifying non-deterministic warm-daemon engine glitches (PHP fatals / engine-state corruption — NOT findings about the file) that the adapter drops at the source so they never surface as a red or reach the cache. The adapter reads this prop straight from `.supertool.json`; built-in defaults apply when absent. Example: `["System error:", "toMutatingScope() on null"]`. |
| `tier`            | string           | no       | `"fast"` (default) or `"slow"`. `fast` runs inline per-op with per-op rollback semantics. `slow` defers to end-of-call, deduped by `(validator, path)` — runs once per unique pair regardless of how many ops touched the file. Failures reported under `[validators-deferred]` header; no auto-rollback. Use `slow` for heavy validators (phpstan, phpunit, rector) to avoid re-running on every edit in a multi-op batch. |

## Adding a validator

1. Write `validators/<tool>/<tool>.sh` (or `.py`)
2. Register in `.supertool.json` under `validators` block
3. Done. Core picks it up automatically.
