# Spec reference

A spec is one JSON file, passed with `-spec`. It names the suite command, the positive control, and the mutations to try. A [JSON Schema](schema/spec.v1.json) for editors and linters is published at `https://muhar.assay.guide/schema/spec.v1.json`.

```json
{
  "$schema": "https://muhar.assay.guide/schema/spec.v1.json",
  "root": "/home/dev/limits",
  "test": "go test ./... -count=1 -timeout 60s",
  "control": {
    "name": "CONTROL: never refuse an over-limit withdrawal",
    "file": "limits.go",
    "old": "\t\treturn ErrOverLimit",
    "new": "\t\treturn nil"
  },
  "mutations": [
    {
      "name": "zero is accepted as an amount",
      "file": "limits.go",
      "old": "if amount <= 0 {",
      "new": "if amount < 0 {"
    }
  ]
}
```

## Top-level fields

| Field | Type | Required | Meaning |
|---|---|---|---|
| `root` | string | no | Directory the suite runs in and relative `file` paths resolve against. Empty or absent: the current working directory. A relative `root` is itself relative to the current directory. |
| `test` | string | yes | Suite command, run as `sh -c '<test>'` in `root`. Exit status non-zero means the suite failed. Must be non-empty. |
| `control` | mutation | yes | The [positive control](concepts.md#control): a mutation the suite must catch. Its `name` and `old` must be non-empty. |
| `mutations` | array of mutation | no | The guards under test, run and reported in this order. Empty or absent: only the baseline and control run, and the report says `Totals: 0 caught, 0 NOT CAUGHT, 0 could-not-mutate.` |

## Mutation fields

`control` and every entry of `mutations` have the same shape.

| Field | Type | Meaning |
|---|---|---|
| `name` | string | The label in the report. Name the bug the edit simulates. Not required to be unique, but a duplicate makes the report ambiguous. |
| `file` | string | File to edit. Absolute: used as is. Relative: joined to `root`, or to the current directory when `root` is empty. |
| `old` | string | Exact text to replace. Must be non-empty and present in the file. |
| `new` | string | Replacement text. Must differ from `old`. May be empty, to delete `old`. |

## Exact match, first occurrence

`old` is matched byte for byte: no regular expressions, no whitespace folding, no trimming. muhar replaces the first occurrence only. If the same text appears more than once and you mean a later one, widen `old` with surrounding context until it is unique, or until its first occurrence is the one you mean.

The worked example relies on this. The mutation "a withdrawal that lands exactly on the limit is refused" uses `old` `spentToday+amount > limit`, which also occurs inside the previous mutation's `old`. Each mutation is planted on pristine source, so they do not interfere.

## Whitespace and line endings

Write tabs and newlines as JSON escapes: `\t`, `\n`. A file with Windows line endings needs `\r\n` in any `old` that spans a line break; a `\n`-only `old` will not match it, and the mutation is reported `COULD_NOT_MUTATE` with `Old text not found`. Non-ASCII text is matched as UTF-8 bytes.

## What happens to the file

For each mutation muhar reads the file, computes the edit, writes the result with the file's existing permission bits, reads it back to prove the bytes landed, runs the suite, and then writes the original bytes back. Restoration is unconditional once the write has happened, whatever the verdict. A symbolic link is followed: the edit lands in the link's target.

One exception: an interrupt (`SIGINT`, `SIGTERM`) during a sequential run ends the process before the restore, so the file under test is left mutated. See [How a run works](internals.md#signals).

## Absolute paths and parallel runs

With `-j` above 1, each worker runs the suite in its own copy of `root`. A relative `file` resolves inside that copy. An absolute `file` does not: every worker edits the same original file while running the suite in its copy, which produces wrong verdicts. Use relative paths in any spec you run with `-j` above 1 or `-j 0`. This is tracked as [assay#2643](https://github.com/medici-finance/assay/issues/2643).

## Decoding rules

muhar decodes the spec with Go's `encoding/json`. Three consequences:

- Unknown keys are ignored. That is what makes `$schema` safe to include, and also means a misspelt key such as `"mutation"` is silently dropped. The published schema is stricter and rejects unknown keys, so validate against it to catch typos.
- Key matching is case-insensitive: `"Test"` and `"TEST"` both set `test`.
- A wrong type is an error: `{"test": 1}` fails with `json: cannot unmarshal number into Go struct field spec.test of type string`.

## Spec errors

These stop muhar before any suite runs. Each is printed to stderr and exits 1.

| Message | Cause |
|---|---|
| `muhar: read spec: <error>` | The `-spec` file cannot be read. |
| `muhar: parse spec: <error>` | The file is not valid JSON, or a field has the wrong type. |
| `muhar: spec.test (the suite command) is required` | `test` is empty or absent. |
| `muhar: spec.control is required (a mutation the suite MUST catch) — a harness with no failing control is a green light with no bulb (#34)` | `control.name` or `control.old` is empty or absent. |

## COULD_NOT_MUTATE reasons

When a mutation cannot be planted or proven, the report line carries the reason in parentheses. `<name>` is the mutation's name, `<file>` is `file` as written in the spec, and `<path>` is the resolved path.

| Reason | Cause |
|---|---|
| `mutation "<name>": Old is empty — nothing to match, edit cannot land` | `old` is empty. |
| `mutation "<name>": Old == New — edit is a no-op` | `old` and `new` are identical. |
| `mutation "<name>": Old text not found in <file> — edit did not land` | The file does not contain `old`. The usual cause is drift: the code changed and the spec did not. |
| `mutation "<name>": content unchanged after replace — silent no-op` | A final check that the edited content differs from the original. |
| `mutation "<name>": read <path>: <error>` | The file cannot be read; also `stat`, `write` and `re-read` variants. |
| `mutation "<name>": post-write verify failed — on-disk content does not match the intended mutation` | The bytes read back differ from the bytes written. |
| `mutation "<name>": post-write verify failed — file is byte-identical to the original` | The bytes read back equal the original. |

The same reasons apply to the control, but there they discard the run: `positive control could not be planted (<reason>) — the harness is not mutating; discard the run`, exit 2.

## JSON Schema

[`/schema/spec.v1.json`](schema/spec.v1.json) is a JSON Schema (draft 2020-12) for this format. It requires `test` and `control`, requires a non-empty `name`, `file` and `old` on the control and on every mutation, and rejects unknown keys. It is deliberately stricter than muhar's decoder; a spec that passes it is always accepted by muhar.

To validate in an editor, add `"$schema": "https://muhar.assay.guide/schema/spec.v1.json"` as the first key. With a command-line validator:

```sh
check-jsonschema --schemafile https://muhar.assay.guide/schema/spec.v1.json mutations.json
```

---

Source of this page: https://muhar.assay.guide/spec
