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-07-02

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.

Acceptance-criteria coverage

A contract must account for every acceptance criterion in the scope. An assertion claims one with an ac field — ac: "AC1" or ac: ["AC1", "AC2"]. An AC that no assertion can mechanically pin is excused with a coverage_waivers entry: ac, kind, and reason, where kind is judgment (untestable by nature) or retired (deliberately dropped). reason is required for both.

yaml
coverage_waivers:
  - ac: AC4
    kind: judgment
    reason: "Error-message helpfulness is a human judgment, not mechanically testable."

At ana artifact save, a pre-seal coverage gate blocks the seal when a scope AC has neither a covering assertion nor a waiver. It runs only for contract version 1.1 or higher, on a scope whose criteria it can parse; legacy 1.0 contracts and unparseable scopes no-op, warning without blocking. ana plan coverage <slug> is the read-only, plan-time preview of the same join — run it while writing the contract.

Coverage is a structural guarantee: a link exists. It does not prove the linked test actually exercises the AC — that remains a judgment for Verify.

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.