Status: Accepted (2026-04-14)
The legacy validator surface (pre-0.2.0) pushed raw strings into
result.errors. Consumers who wanted to react programmatically — gate CI
on a specific kind of failure, or route a subset of errors to a
different channel — had two bad options:
Every validator emits ValidationError instances, not strings. A single
source of truth lives at plugins/dotbabel/src/lib/errors.mjs:
class ValidationError extends Error with stable fields: code,
message, optional file, pointer, line, expected, got,
hint, category.ERROR_CODES, an Object.freeze({ SPEC_STATUS_INVALID: ..., ... })
enum. Adding codes is safe; renaming is a breaking change.ValidationError.prototype.toString() returns the legacy
"<file>: <message>" format so existing /regex/.test(err) CI scripts
continue to work without migration.ValidationError.prototype.toJSON() returns a plain object
consumable by JSON.stringify. Every bin’s --json flag emits these
under .details.jq -r '.events[] |
select(.kind == "fail") | .details.code' is the documented pipeline.ERROR_CODES entries are load-bearing strings.
Renames require a major bump; additions don’t.ERROR_CODES is the authoritative index; the
troubleshooting guide mirrors it one-to-one.hint. Non-trivial overhead vs errors.push("msg"), but
one-time.Result<ok, err> style (Rust-inspired). Consumers would have to
import a Result helper. Too much ceremony for Node; {ok, errors} is
idiomatic enough.StructuredError shape.hint messages becomes in-scope.