# Validation

A document is schema-valid or it is rejected. Completeness is a **score**, never a gate.

## The pipeline

1. **JSON Schema** (Draft 2020-12) against the type's schema.
2. **Invariants** — the rules JSON Schema cannot express.

Invariants only run if the schema passed. A rule reading a field of the wrong type reports
a second problem that is really the first one wearing a different name.

Everything comes back as one list, whatever stage produced it:

```json
{ "path": "/arc/need", "rule": "want-differs-from-need", "message": "…", "severity": "error" }
```

`severity` is `error` (the write is rejected) or `warning` (recorded, write proceeds).

The list is **capped at 20**, and the response says `errorCount` and `truncated` so a
truncated list is never read as a complete one. The cap exists because this list is fed
back to a model for one retry, and a model handed four hundred errors rewrites the document
instead of fixing it.

## The invariants

| Rule | What it protects |
| --- | --- |
| `want-differs-from-need` | Once `arc` is started, want and need may not be the same sentence. The arc *is* the distance between what a character thinks would fix it and what actually would. |
| `complete-system-needs-cost-limit-tell` | A ruleset system the writer marked `complete` must have `cost`, `limit` and `tell` started. A system with no cost is not a system. |
| `endpoints-resolve` | Both ends of a relationship must resolve against this payload + committed work + the work's library overlay. |
| `extension-shadows-core` | An overlay may not define `name`, `id`, `status`… — two readers would disagree about the same document. |
| `spec-version-pin` | A work pins a major. Objects declaring a different major are invalid inside it. |
| `duplicate-slug` | Two objects claiming one id. Fails **that row**, never the batch, and nothing is auto-renamed. |
| `ref-resolves` | A reference pointing nowhere. `warning` — reported, never repaired, because a confidently wrong substitution reads as fact. |

Scoped to what is present: starting a group applies that group's rules, omitting it applies
nothing. There is deliberately **no completeness threshold that switches invariants off** —
a one-seventh-filled character with `want == need` is still wrong.

## Completeness

```
completeness = filled depth groups / total depth groups for the type
```

Unweighted. Any weighting is a judgement about craft that this spec does not get to make.

Filled means: a string that trims to length > 0, an array with length > 0, or an object
with at least one non-empty child. `{}` is started but not filled — which is exactly the
distinction that lets an agent open a group, fail to find anything, and leave an honest
record of having looked.

Shown on a sheet as a hint. In v0 it drives no HTTP status, no council threshold and no
analytics gate.

## Rejected writes

Invalid JSON, a missing required core field, a duplicate slug, or a present-field invariant
failure rejects the write. The object on record is left exactly as it was, and the rejected
payload stays with the run that produced it — it is never copied onto the stored object.

Empty optional groups are valid and always were.

## Using it

```bash
bin/validate examples/tidewardens          # a directory is read as one work
bin/validate character.json --json         # machine-readable
bin/validate examples/minimal --pin 0.1.0  # check against a work's pin
```

Exit code is 0 when everything is valid, 1 when anything is not, 2 on bad input.

```js
import { validateObject, validateWork } from '@asc-me/narrative-spec'

validateObject(character)                       // schema + single-object invariants
validateObject(rel, { known, pin: '0.1.0' })    // + cross-object rules
validateWork(objects, { committed, pin })       // + duplicate slugs, mean completeness
```

Pass `known` (or use `validateWork`) to enable the cross-object rules. Omit it and they are
**skipped rather than failed** — a single-object check must not invent dangling references.
