yass — Language Reference
yass — Yet Another Spec Syntax. A YAML-based specification language for AI-agent development: agents (or humans) write specs that capture desired behavior, and tooling consumes them to drive test (TDD) and implementation generation.
File
- A yass file uses the
.yass.yamlextension — with at least one character before it, so a file named exactly.yass.yamlis not a yass file — and is a YAML multi-document stream (----separated). - Read as UTF-8.
- SHOULD carry the schema modeline as its first line, so editors validate against the
JSON Schema with no per-repo config:
# yaml-language-server: $schema=https://shakefu.github.io/yass/v1.schema.json
Project root (root.yass.yaml)
- A file named exactly
root.yass.yamlis required, and its presence defines the project root: the project root is the directory that contains it. Every root-relative ref path — a path token with no leading dot — resolves from there. - A project root is not a repository root. It is defined by the presence of the file and by nothing else: the anchor is deliberately decoupled from the repository, so nothing ties it to a VCS checkout, a package manifest, or a build directory. A project root may sit at the top of a checkout, or nowhere near it.
- Discovery is by name alone. There is no
.gitsearch and no deepest-.yass.yaml-ancestor fallback; a project root is never inferred from layout. Absence is an error. - Layouts are arbitrary. A repository MAY hold several projects, each with its own
root.yass.yaml, and the one that governs a given file is the nearest at or above it — a nested root takes over its own subtree, and the outer root does not govern within it. - Contents: exactly one
spec:document, describing the project as a whole — what it is and what it must do — written with the ordinary five slots. There is no new document type; the project is a normal spec. A project need not produce an invocable program: it may produce a library, a data format, or a language (this repository’s project is yass itself, which has no entry point), so read the slots structurally when the subject is not function-shaped (see Slots). The file MAY also carrydesign:documents. More than one spec, or none, is a validation error. (How to write that spec: see GUIDANCE, The root file.) - Membership is reachability. Every spec and design under a project root MUST be
reachable from the root spec through a chain of references, and
CONFORMS,USES, andSEEall count as edges — reachability is a property of the reference graph, not of any one relation. A spec that no chain reaches is either dead or evidence that the root file is missing an obligation. - Encoded in the self-definition as the
Rootconstruct, whichCONFORMS: Document— a root file is an ordinary yass file (preamble first,----separated stream) that has been given a fixed name and a constrained payload.
Preamble
- Required, exactly one, and MUST be the first document of every file. Making it mandatory simplifies parsing and validation.
- Identified structurally by the absence of a
spec:ordesign:key. - The file name is the identity of the whole unit; the preamble does not repeat it.
- Keys:
description— required; a string file-level summary (block scalar>allowed). Surfaced by tooling that summarizes files.version— required; the yass language version the file targets. Pinned atv1in the current revision.related— optional; a list of freeform external resource locations (paths, URLs, issue links). A distinct channel from theSEErelation; never resolved by tooling.
Spec document
- A
spec:key naming the spec (may be namespaced, e.g.pkg.Symbol) plus zero or more slots. A document carries exactly one ofspec:ordesign:— carrying both is a validation error. - Name charset: ASCII letters and digits,
.,_, and-(so a slot-keyword name likeSlot.SIDE-EFFECTis legal and addressable). The composition regex^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$also rejects a leading dot, a trailing dot, and consecutive dots. - Names MUST NOT case-insensitively match any yass keyword — a slot name, a
normativity keyword,
WHEN, or a relation (CONFORMS,USES,SEE). - Names MUST be unique within a file, so refs resolve unambiguously.
- Granularity: one public symbol / endpoint → one spec. Also the unit of retrieval.
- The five slots are themselves defined in
yass.yass.yamlasSlot.INPUT,Slot.RETURN,Slot.ERROR,Slot.SIDE-EFFECT, andSlot.INVARIANT.
Design document
-
A
design:key naming the block, plus requiredtypeandcontentkeys — and no slots. A peer ofspec:in the YAML stream:--- design: StartupSequence type: ordered-steps content: | 1. Warm the cache 2. Connect the database 3. Mark the service live -
Names share the file-wide uniqueness namespace with spec names (same name grammar), so a ref target stays unambiguous — a bare name or
path@Nameresolves to exactly one document, spec or design. -
type— required; a brief freeform interpretation hint, one or two words (pseudocode,ordered-steps,constraint,stack, …), opaque to tooling. Deliberately not a closed set: tooling never branches on the value, so there is no out-of-set case to pin. -
content— required; a string holding the freeform body, written as a literal block scalar (|) by convention — the rule fixes the value’s type, not its scalar style, since a YAML parser cannot distinguish styles after loading. The body is opaque: no refs are recognized inside it. -
Normative force: an implementation bound to a design block via reference MUST follow it. The block is not commentary; it binds through the reference graph (the
USESrelation — see References). -
The home for normative content that is not an obligation on observable behavior: algorithms / pseudocode, technology or stack constraints from outside forces, and multi-spec lifecycle procedures that have no owning symbol.
-
An unreferenced design block is dead weight — declared normative content nothing binds to — and is flagged by lint.
Slots
The recognized set (each is a YAML list of obligations), written UPPERCASE (see Casing convention below):
INPUTRETURNERRORSIDE-EFFECTINVARIANT
The first four are function-shaped and map onto Design by Contract: INPUT =
precondition (what it accepts), RETURN = postcondition (what it yields), ERROR =
failure modes, SIDE-EFFECT = observable effects beyond the return. INVARIANT
completes the contract: constraints that always hold, independent of any call.
INVARIANT is universal (any spec may use it) but is a slot of last resort: an
obligation SHOULD be placed in INVARIANT only when none of INPUT, RETURN, ERROR,
or SIDE-EFFECT is suitable. It exists so always-true constraints are not forced into
SIDE-EFFECT (which means effects) — not as a general catch-all.
In the ERROR slot specifically, a guarded obligation (with WHEN) names one
specific failure mode, and a guard-less obligation is the residual — the policy
for any failure not matched by a guarded obligation in the same slot. A residual is
meaningful only when the guards leave inputs unmatched: if the guarded obligations
already account for every input, the residual set is empty and a guard-less catch-all
is dead — it can never fire, and MUST-NOT be carried. State that exhaustiveness in
the spec’s INVARIANT instead: a guard-less ERROR obligation is itself read as the
residual, so exhaustiveness cannot live there.
ERROR is also where rejections live, and only there: a rule phrased as be rejected
sits in an ERROR slot, and validation enforces exactly those rules — a MUST or
MUST-NOT in any other slot binds authoring without being a validation rejection
(reachability and the modeline SHOULD are lint’s advisory domain).
The residual discipline generalizes beyond ERROR: whenever a slot branches on a
closed set of values — most commonly an INPUT that dispatches on a subcommand,
mode, or enum — it must state the behavior for a value outside that set, or one that is
missing. The residual is to a dispatch what the guard-less catch-all is to the error
table. (The draft-time pass that walks every such case — closed sets, guards,
segmentation boundaries, failures, trusted inputs: see GUIDANCE, Case coverage.)
When a spec describes something that is not a function (e.g. the language defining
itself), read the function-shaped slots structurally: INPUT = the form a thing takes,
RETURN = what a well-formed thing denotes, ERROR = malformed forms that must be
rejected, SIDE-EFFECT = real effects of processing it.
Obligation
An obligation is a YAML mapping (a list item under a slot):
- Normativity — exactly one key, unless ref-only. RFC 2119 / RFC 8174 vocabulary,
negatives hyphenated:
MUST,MUST-NOT,SHOULD,SHOULD-NOT,MAY. Value = the obligation prose. - Guard (optional) —
WHEN, value = condition prose. Expresses a sufficient condition. IfWHENis present it MUST be accompanied by a normativity keyword. - References (optional) —
CONFORMS/USES/SEE, each at most once per obligation, value = a single ref-target string. - Closed key set — a normativity keyword,
WHEN, and the relation keys are the only keys an obligation mapping may carry; any other key is a validation error. - Prose values — a normativity keyword’s or
WHEN’s value must be a string: mappings, sequences, core-schema nulls, and non-string scalars (numbers, booleans) are rejected. An absent value and unquotednull,Null,NULL, or~resolve to null; quoted empty strings and quoted"null"or"~"remain strings. Resolve YAML tags before checking this rule rather than testing only the scalar text. - Ref-only — a mapping with one or more relation keys and no normativity keyword
and no
WHEN. Allowed; it adds no obligation of its own and resolves per its relation (a slot-targetedCONFORMStranscludes; a whole-specCONFORMSstays in place as a declaration that the enclosing spec conforms to the referenced spec as a whole; a ref-onlyUSES— a bare- USES: Namelist item — transcludes too, appending its design block’s content).
References
-
Relation is the YAML key; target is a single string.
-
Target syntax:
path@SpecName::SLOT- A bare name (
SpecName) addresses a spec or design in the same file. @separates a path from the spec name (path@SpecName) for other files.::separates the spec name from a slot (SpecName::SLOT); omit it to address the whole spec. TheSLOTtoken must name one of the five recognized slots (see Slots); any other slot token is a validation error.- Path resolution: a
./or../path is relative to the referencing file; a path with any other lead resolves from the project root (see Project root above) — including one whose leading dot opens a hidden name, such as.config, which is a root-relative token, not a file-relative one. A path beginning with/is rejected: yass paths are file-relative or root-relative, never absolute. A path may climb with../past the project root; the reached file stays governed by its own nearest root. The.yass.yamlextension is omitted. - The slot is the finest addressable unit. Named anchors, never line numbers.
Slots belong to specs only:
::SLOTon a target that resolves to a design is a validation error — a design carries no slots. - (
#was rejected as a separator — a leading#is a YAML comment, and@is a reserved YAML indicator that can’t start a plain scalar either. Making same-file refs bare names means no target ever leads with an indicator, so none need quoting.)
- A bare name (
-
Relations:
Relation Target Resolution Meaning CONFORMSspec or slot slot: inlined; whole-spec: not hard requirement — must match the spec or slot USESdesign block content appended + provenance binding design the implementation must follow SEEspec, slot, or design pointer, never inlined open-ended pointer — imposes nothing on its own CONFORMSandUSESeach pin a target kind;SEEtakes any. A relation/target-kind mismatch —USES→ spec,CONFORMS→ design — is a validation error, so the CONFORMS-vs-USES distinction is machine-checkable rather than a judgment call.CONFORMSis a hard requirement with two aspects of one meaning: the carrier must match the referenced spec or slot, and inlining is how that match is made checkable in place. A slot-targetedCONFORMS(…::SLOT) inlines the referenced slot’s obligations in front of the carrier. A whole-specCONFORMS(no::SLOT) is a conformance reference to the entire spec — it is not transcluded (there is no single slot to splice); the conformer must satisfy the referenced spec as a whole. This matches the language meaning thatCONFORMSmust “match the referenced spec or slot.”USESbinds the carrier to a design block: resolution appends the block’s typed content to the emitted fragment, once, with a provenance comment (# USES: StartupSequence), deduplicated when several obligations reference the same block — a design block has no obligations to splice into a slot. When a guarded obligation carries aUSES, the guard scopes when the design binds.SEEnames a spec or design for the reader: it is never inlined and imposes nothing on the carrier beyond what the obligation’s own prose already states. -
Choosing a relation — three questions, in order.
- Must this obligation match what the target says? →
CONFORMS. Slot-targeted (…::SLOT) when one specific slot is the contract; whole-spec when another spec owns cross-cutting rules this one must obey (see GUIDANCE, Composition). - Is the target a design block whose freeform body binds the implementation? →
USES. Design blocks only —USESat a spec is a validation error. - Everything else →
SEE. It is the open-ended catch-all, and the right key even when a genuine dependency exists: a dispatcher spec naming the handler specs it dispatches to, a stage that must run after another spec, or plain “go read this next.” The dependency is stated in the obligation’s own prose;SEEonly says where the named thing is defined. WhatSEEmust never carry is a constraint — if the target’s rules bind this spec, that is whole-specCONFORMS, and reaching forSEEsilently drops the requirement.
- Must this obligation match what the target says? →
-
Guards conjoin when an inlined obligation is itself guarded. When a slot-targeted
CONFORMScarrier has aWHENguard and an inlined obligation carries its ownWHEN, the inlined obligation applies only when both guards hold — the carrier’s guard conjoined with the inner guard. (This is the language-level meaning; how a tool renders the combined guard text is the tool’s concern.) -
Slot-targeted
CONFORMScarries the dataflow reading. When one spec’sINPUTconsumes the data another spec’s slot produces — characteristically anINPUTthat points at a producer’sRETURN,CONFORMS <producer>::RETURN— it means the obligation must match the data that slot produces: the data crossing the boundary is exactly what that slot yields, so the producer’sRETURNguarantees characterize it here, and inlining puts those guarantees in front of the consumer’sINPUT. This is the structural anchor for a pipeline or producer/consumer relationship. It does not by itself decide the trust boundary — which of the producer’s guarantees the consumer relies on versus re-checks is the consuming spec’s own obligation to state (see GUIDANCE, Composition and Case coverage). That obligation includes the residual on violation: for each guarantee the consumer relies on without re-validating, the consuming spec must also state what it does if that guarantee does not hold — even if only to declare the behavior unspecified. Stating the trust without pinning its violation leaves every implementer to invent the out-of-contract behavior. -
DRY is achieved by transclusion (inlining), not bare pointers.
Casing convention
Fixed-meaning keywords are written UPPERCASE; content-bearing field keys are lowercase.
- UPPERCASE (the vocabulary — the key is the meaning): slot names (
INPUT,RETURN,ERROR,SIDE-EFFECT,INVARIANT), normativity (MUST,MAY, …), the guardWHEN, and the relations (CONFORMS,USES,SEE). Also the slot portion of a ref target (the part after::). - lowercase (labels for values):
spec,design,type,content,description,version,related. - In prose, write a keyword UPPERCASE when you mean the keyword itself (e.g. “the
RETURNslot”), so it cannot be confused with the ordinary word.
Rationale: it mirrors the RFC 2119 uppercase-keyword convention already used for
normativity, unifies the structural vocabulary as one visually distinct layer, and
disambiguates homographs (RETURN the slot vs. “return” the word).
In the self-definition the rule is encoded once as the Keyword construct; slots,
normativity, the WHEN guard, and relations declare CONFORMS: Keyword::INVARIANT
rather than restating it — transcluding the invariant instead of conforming to it,
because the keyword constructs are the rule’s home, not its conformers.
Provenance (emitted fragments)
- Inlined obligations carry a YAML comment naming the source, e.g.
# CONFORMS: db/user@User::RETURN. Appended design blocks carry the same, e.g.# USES: StartupSequence. - An emitted fragment identifies the queried spec; there is no host
file:header (redundant and able to contradict — the filesystem is the source of truth).
Footgun
- Obligation prose containing
:(colon-space) mis-parses as a nested YAML mapping and must be quoted.
Meta-rules (the spec system, distinct from in-spec obligations)
- Every unit of specification MUST be paired with its
.yass.yamlfile. A unit is the project’s chosen pairing — a command, module, package, or similar cohesive component — not necessarily one source file, and a spec file may hold severalspec:documents. - Every public symbol or endpoint SHOULD have a definition in the
.yass.yamlfile. - The
.yass.yamlfile MUST be structured parseably. - The
.yass.yamlfile MUST-NOT require special syntax to be read by an AI agent. - The
.yass.yamlfile MUST conform to the format when written, and MUST be validatable by tooling.
Resolution and scope
- Resolution: a slot-targeted
CONFORMSis inlined into the referencing spec (one level in v1); a whole-specCONFORMS(no::SLOT) is not inlined — it is a conformance reference to the entire spec.SEEis a pure pointer, never inlined.USESappends its design block’s typed content to the emitted fragment, once per block, with provenance. - Verification is out of scope — tooling routes/retrieves, never verifies obligation
content (keeps it language-agnostic). A
COMPATIBLErelation was deliberately excluded. WHENstates a sufficient condition only.- Extension:
.yass.yaml, chosen over bare.specafter a collision review. Bare.specis owned by RPM spec files / PyInstaller / Vim+Neovim filetype / GitHub Linguist / freedesktop MIME, and — decisively — a bare.specnever gets theyamllanguage id, so it earns neither free YAML highlighting nor JSON-Schema association. A trailing.yamlfixes that; the.yassinfix namespaces it.