cwright validate
cwright validate <model> [--json]
Parses a model document and checks it as a whole, reporting every problem found rather than
stopping at the first one — every reference resolves (a step's resource exists, a rework target is
an earlier step, a dedication chain is used consistently), every probability is in [0, 1], and
every declared constraint is internally consistent.
A model path of - reads the document from stdin.
Output
On success:
ok: inspection-booth (1 resources, 2 products, 2 releases)
model_hash: <hex>
model_hash is a content hash of the canonicalised document — the same model always hashes the
same way, regardless of incidental formatting differences in the file. It is what
run --store uses to recognise a model it has already evaluated, and what every
result document's manifest.model_hash field points back to.
On failure, validate exits non-zero and prints every problem it found, each naming the field it
concerns, so a document with several unrelated mistakes does not need several separate runs to
diagnose.
--json, for an editor or a script
The same answer as a machine-readable document on stdout. Every problem carries a JSON pointer to the field it concerns, so a consumer does not have to read prose to find out what to fix.
On an accepted document:
{
"model": "inspection-booth",
"model_hash": "<hex>",
"ok": true,
"problems": []
}
On a document that parses and is refused — problems is the list the human form prints, one
entry per problem, in the same order:
{
"model": "inspection-booth",
"model_hash": "<hex>",
"ok": false,
"problems": [
{
"message": "resource 'booth' has no servers; work sent to it would wait forever",
"path": "/resources/0"
},
{
"message": "step names resource 'nope', which does not exist; known resources: booth",
"path": "/products/0/steps/0"
}
]
}
A document that could not be read has no problems list, because there is no document to
point into. ⭐ Branch on line and path to learn whether there is a location, and read
error for the cause — at most one locator is ever present, so that branch is decidable, and
null on both is a legitimate answer rather than a single cause.
A file that could not be opened has neither, because nothing was ever parsed:
{
"column": null,
"error": "reading absent.yaml: No such file or directory (os error 2)",
"line": null,
"ok": false,
"path": null
}
A document that does not parse carries the line and column the parser itself reported:
{
"column": 5,
"error": "torn.yaml: read as YAML, not a valid model: resources[0]: missing field `servers` at line 3 column 5",
"line": 3,
"ok": false,
"path": null
}
A document that parses and is then refused by a reader guard that knows which field is at fault has no position, because there is no longer any text to point at. It carries a path instead:
{
"column": null,
"error": "nan.yaml: read as YAML, /horizon_seconds: NaN is not a finite number. JSON cannot express one, so the canonical form of this model -- which is what `model_hash` hashes -- would lose it, and `validate`'s range checks pass a NaN because every comparison against it is false. Give it a number.",
"line": null,
"ok": false,
"path": "/horizon_seconds"
}
The contract
| key | when it is there |
|---|---|
ok | always, and it is the answer |
error | the document could not be read, and this names the cause |
line, column | the parser reported a position, so the text did not parse |
path | the text parsed and a reader guard refused one field, naming it |
| neither | there is no location to give. More than one refusal lands here — see below |
model, model_hash, problems | the document was read. problems is [] when the model is accepted |
⚠️ Which refusals have no location at all
Two reader guards are about the document as a whole rather than any one field, so they have
no location to report and arrive with line, column and path all null — the same shape as
a file that could not be opened:
| refusal | what it means |
|---|---|
| the file could not be opened | the path is wrong, or stdin was empty |
| it does not survive its canonical form | model_hash is a hash of the canonical JSON, and reading the model back from it gives a different model |
| the two readers disagree | the text is valid JSON and valid YAML, and the two parsers read a number differently |
⭐ So a consumer that wants to jump somewhere branches on the two locators and gives up
gracefully on this bucket; a consumer that wants to explain the refusal reads error, which
is the only thing that separates these three. Naming the bucket after any one of them would be
wrong: an editor told null, null means "file not found" reports that about a file it has just
opened and parsed.
A semantic problem has no line and column, and the document says nothing rather than guessing: those rules run against the parsed document, where there is no longer any text to point at. A JSON pointer is the more useful location anyway — it survives reformatting, and it names the same field in a YAML document and in its JSON equivalent, which are two spellings of one model.
--json does not change the exit status. A refused document still exits non-zero, so
cwright validate model.yaml --json can be used in a pipeline exactly as the plain form is,
and a script that ignores the document still cannot mistake a refusal for a pass.
When to use it
Run validate as the first step of any pipeline that generates model documents — a script, a
search loop, or an agent proposing variants. It is deliberately cheap (no simulation runs) and
answers a narrower question than run does: is this document well-formed and internally
consistent, not yet what does it produce. lint asks the question in between —
can what this describes actually run — and warns rather than refusing, because a transient
overload is a legitimate thing to model. propose performs an equivalent
check on a variant before ever running it, for the same reason.