The contract

A contract is the seal between Plan and Build. It defines “done” as a list of assertions with matchers — mechanical, testable, unambiguous. Once sealed, it can’t be changed. Both Build and Verify work against the frozen version.

Reading time · 7 minLast reviewed · 2026-05-25

What a contract is

A YAML file with three parts: header fields (version, sealed_by, feature), an assertions array, and a file_changes list declaring which files will be created, modified, or deleted. Plan writes it. The CLI validates the structure and seals it with a content hash.

Each assertion has required fields: id (A001, A002...), says (human-readable description), block (test block name), target (what's checked), and matcher (how it's checked). value is required for most matchers.

yaml
# contract.yaml (from security-hardening — real data)
version: "1.0"
sealed_by: "AnaPlan"
feature: "Security Hardening — Command Injection Elimination"

assertions:
  - id: A001
    says: "Slugs with shell injection characters are rejected before any operation"
    block: "rejects slug with shell metacharacters"
    target: "error.message"
    matcher: "contains"
    value: "Invalid slug"

  - id: A003
    says: "Valid kebab-case slugs pass validation"
    block: "accepts valid kebab-case slugs"
    target: "result"
    matcher: "truthy"

  - id: A005
    says: "Branch names with shell injection characters are rejected"
    block: "rejects branch name with shell metacharacters"
    target: "error.message"
    matcher: "contains"
    value: "invalid"

file_changes:
  - path: "packages/cli/src/utils/validators.ts"
    action: modify
  - path: "packages/cli/tests/utils/validators.test.ts"
    action: create

Matcher types

MatcherChecksExample
equalsExact matchRegex pattern is exactly /^[a-z0-9]...$/
containsSubstring presentError message contains "Invalid slug"
not_containsSubstring absentTest output does not contain "pwned"
not_equalsNot equalExit code is not 0
existsExists or is presentTest for early-validation exists
greaterNumeric comparisonTest count is greater than 10
truthyValue is truthyValidation result is truthy

The sealing moment

When ana artifact save contract runs, the CLI validates the YAML structure, checks that every assertion has the required fields, and computes a SHA-256 hash. This hash is stored in .saves.json. From that point forward, any modification is detectable — ana verify pre-check compares the current hash against the sealed version before verification begins.

Rule

A contract is what makes a verifier possible. Without it, "done" is opinion.

How assertions become tests

Plan writes assertion A001. Build writes a test tagged // @ana A001 whose describe/it text matches the assertion's block field. Verify checks that each assertion ID has a matching test tag and that the test passes independently. The block field is the bridge — it tells Build what the test should be named, and Verify uses it to locate the test.

Writing good assertions

Good assertions are specific, testable, and mechanical. "The code is well-structured" is not an assertion. "Slugs with shell injection characters are rejected before any operation" is. The says field is for humans; the matcher, target, and value are for machines. The block names the test.