Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

“Porque mis armas son los pinceles y mis municiones son las pinturas” — Peter de los Polvorines

espectacular

In brief: ah is a CLI that enforces a contract between your specs and your tests. You write behavioral specs in Markdown, attach a TOML contract to each scenario declaring which tests cover it, then run ah check in CI to catch drift.


espectacular is a behavioral verification tool for Rust CLI projects. It lets you write machine-readable specs that describe what your tool does, then continuously check that behavior with ah check. Each spec scenario is paired with a sidecar TOML contract that lists the tests verifying it — when a test is missing, unconfigured, or failing, ah check exits non-zero and blocks the merge.

  • Installation & Quick Start — get ah on your PATH and run your first check in minutes
  • Command Reference — all ah subcommands with flags, exit codes, and example output
  • Concepts — understand specs, scenarios, contracts, archetypes, and the gate model

Ecosystem

espectacular is part of the charly-vibes Tool Ecosystem — the map shows how it relates to its sibling tools.

Documentation provenance

Rendered docs are generated artifacts; the repository sources are authoritative — edit the repo sources, not the rendered output.

Release Status

Generated artifact (docs.yml, from Cargo.toml + git ref). Do not hand-edit — the repo source files remain authoritative.

  • Version: v0.12.0
  • Built from: main @ d362dfa

Installation & Quick Start

In brief: install ah, run ah init in your repo, then ah check to validate. The worked example below walks through writing a spec scenario, creating its contract, and seeing a passing check. Both ah and espectacular are installed — they are the same binary under two names.

Install

From source (requires Rust toolchain):

cargo install --path .

Or clone and build:

git clone https://github.com/charly-vibes/espectacular
cd espectacular
cargo build --release
# Binary is at target/release/ah

Verify:

ah --version
# ah 0.1.0
espectacular --version
# espectacular 0.1.0

Prerequisites

espectacular requires OpenSpec to manage your spec files. OpenSpec is the directory structure and tooling that stores specs under openspec/specs/ and staged changes under openspec/changes/. Run openspec init once in your repo to set it up before running ah init.

Set up espectacular

Run ah init once in the root of your repo:

ah init

This creates (or refreshes):

  • .espectacular/config.toml — runner and capability config
  • .espectacular/AGENTS.md — guidance block for AI agents
  • Hook integration for lefthook or prek if detected

If ah init reports concerns, use ah doctor to diagnose.

Run your first check

ah check

Output is always a JSON envelope:

{
  "scope": { "deployed": true, "changes": [] },
  "summary": { "structural": 0, "execution": 0, "passed": 2, "counts_by_kind": {} },
  "findings": []
}

Empty findings and structural: 0, execution: 0 means everything is green. If you have specs without contracts yet, you’ll see no-toml findings instead — see When check finds something below.

Worked example

This walks through the full loop: open a change → write a scenario → create a contract → run the check.

Step 1 — Open a change

In OpenSpec, work-in-progress lives under a “change.” Create one:

openspec new my-feature

This creates openspec/changes/my-feature/specs/ where your staged spec lives.

Step 2 — Write a scenario

Edit (or create) the spec for the component you’re working on, e.g. openspec/changes/my-feature/specs/parser/spec.md:

### Requirement: Input validation

#### Scenario: Empty input is rejected
- **GIVEN** an empty string is passed to the parser
- **WHEN** the parser runs
- **THEN** it exits non-zero with a descriptive error message

Step 3 — Create the contract

Generate the contract stub:

ah scenario new my-feature parser \
  --requirement "empty-input-is-rejected" \
  "Empty input is rejected"

This creates .espectacular/changes/my-feature/parser/empty-input-is-rejected.toml. Open it and add your test:

id = "empty-input-is-rejected"
description = "Empty input is rejected before parsing."
archetype = "PF"
status = "active"
superseded_by = ""
authored_with = "0.1.0"

[[tests.pytest]]
flags = "tests/test_parser.py::test_empty_input_rejected"
timeout_seconds = 60

For archetype choices (PF, SA, BP, CE, NR) see Concepts — Archetypes or run ah type.

Step 4 — Check with the change in scope

ah check --changes my-feature

A passing run exits 0 with passed: 1 in the summary.

Step 5 — Archive when merged

Once the change merges, promote the contract to deployed:

ah archive my-feature

After this, ah check (without --changes) will include the scenario in its scope.

When check finds something

A failing check exits 1 and includes findings in the JSON. For example, if a scenario has no contract yet:

{
  "scope": { "deployed": true, "changes": [] },
  "summary": { "structural": 1, "execution": 0, "passed": 0, "counts_by_kind": { "no-toml": 1 } },
  "findings": [
    {
      "kind": "no-toml",
      "category": "structural",
      "spec": "parser",
      "spec_path": "openspec/specs/parser/spec.md",
      "scenario": { "id": "empty-input-is-rejected", "title": "Empty input is rejected" },
      "suggested_action": "run_ah_scenario_new",
      "playbook_command": "ah explain run_ah_scenario_new"
    }
  ]
}

Each finding includes a suggested_action and a playbook_command you can run directly:

ah explain run_ah_scenario_new

Use ah explain --list to see all explainable topics.

What’s next

  • Command Reference — all ah subcommands with flags and examples
  • Concepts — understand specs, contracts, archetypes, and the gate model

Command Reference

In brief: ah is the espectacular CLI — espectacular is an alias for the same binary. The primary commands are ah check (run in CI to enforce specs) and ah init (run once per repo to set up). Use ah explain <topic> to get guidance on any finding or error.


ah init

Create or refresh .espectacular/ files and hook integration.

ah init

Idempotent: safe to re-run after updating specs or changing hook frameworks. Stubs contract files for any scenarios that have no existing contract. Installs ah check into lefthook or prek if detected.

Exit codes: 0 on success, non-zero if the OpenSpec directory is missing.


ah check

Validate deployed specs and run declared tests. Prints a stable JSON envelope to stdout.

ah check [--changes <id>]

Flags:

FlagDescription
--changes <id>Include one or more staged change overlays (repeat for multiple)

Exit codes: 0 when findings contains no structural or execution findings; 1 otherwise. Quality findings (quality-*) never cause a non-zero exit.

Note: All JSON examples below show the data field of the genesis envelope — actual output wraps these fields in {ok, envelope_version, cli_version, envelope_kind, data, warnings, hints, meta}.

Example — clean run:

ah check
{
  "ok": true,
  "envelope_version": "0.1",
  "cli_version": "0.4.0",
  "envelope_kind": "ok",
  "data": {
    "scope": { "deployed": true, "changes": [] },
    "summary": { "structural": 0, "execution": 0, "passed": 3, "counts_by_kind": {} },
    "findings": []
  },
  "warnings": [],
  "hints": [],
  "meta": { "duration_ms": 0, "tx": null, "request_id": null, "author": null }
}

Example — with a staged change:

ah check --changes add-parser-validation
{
  "scope": { "deployed": true, "changes": ["add-parser-validation"] },
  "summary": { "structural": 0, "execution": 0, "passed": 4, "counts_by_kind": {} },
  "findings": []
}

Example — with a finding:

{
  "scope": { "deployed": true, "changes": [] },
  "summary": { "structural": 1, "execution": 0, "passed": 0, "counts_by_kind": { "no-toml": 1 } },
  "findings": [
    {
      "kind": "no-toml",
      "category": "structural",
      "spec": "parser",
      "spec_path": "openspec/specs/parser/spec.md",
      "scenario": { "id": "empty-input-is-rejected", "title": "Empty input is rejected" },
      "suggested_action": "run_ah_scenario_new",
      "playbook_command": "ah explain run_ah_scenario_new"
    }
  ]
}

Run the playbook_command from any finding to get step-by-step guidance:

ah explain run_ah_scenario_new

Finding kinds:

KindCategoryMeaning
no-tomlstructuralscenario has no matching contract file
orphan-tomlstructuralcontract exists without a matching scenario
slug-collisionstructuraltwo scenarios in one spec have the same id
id-mismatchstructuralscenario slug, filename, and TOML id disagree
no-tests-declaredstructuralcontract has no runnable test entries
missing-runnerstructurala non-shell test type has no configured runner
malformed-contractstructuralTOML cannot be parsed or validated
missing-replacementstructuralsuperseded contract points to an absent replacement
overlay-conflictstructuralselected changes define conflicting staged scenarios
test-failingexecutiona declared test timed out or exited non-zero
quality-mutationqualitymutation score meets threshold (informational)
quality-propertyqualityproperty-based tests passing (informational)
quality-snapshotqualitysnapshot tests passing (informational)

ah lint

Statically analyze spec files for authoring-quality findings. Heuristic checks plus a specodelic (spk lint) bridge.

ah lint [<root>] [--changes <id>] [--check <kind>]

Arguments:

ArgumentDescription
<root>Optional repo root to lint (defaults to the current directory)

Flags:

FlagDescription
--changes <id>Lint a change’s spec overlay in addition to deployed specs
--check <kind>Run only a single check category (e.g. vague-qualifier)

Checks: vague-qualifier, imperative-step, conjunctive-bloat (configurable max and-steps, default 5), missing-negative-scenario, missing-non-goals, unresolved-ambiguity, entangled-spec, verifies-dangling, scenario-unlinked, property-untraced, plus spk.<rule_id> relays from the specodelic linter and spk.graph.typing / spk.graph.dangling relays from spk graph.

A spec file that cannot be parsed emits an error-severity finding instead of aborting the walk. A kind: intent file the dual-format bridge would skip emits spk-frontmatter-mismatch naming the expected id.

Exit codes: 0 always — lint findings are advisory; they do not gate.


ah sync

Create or refresh property-derived contracts from lint-clean specodelic Properties rows (design D1/D3 of derive-contracts-from-specodelic). See dual-format authoring for the coverage convention.

ah sync [--check] [--json]

Flags:

FlagDescription
--checkCI mode: write nothing; report missing and stale contracts; exit non-zero on drift
--jsonMachine-readable envelope

ah sync runs spk lint first and refuses to derive from a lint-dirty file; missing spk fails with spk-unavailable. Sync-owned fields (id, description, archetype, falsifiability_class, derived_from) are authoritative and rewritten; human-owned fields (tests, status, superseded_by) are never overwritten.

Exit codes: 0 on success; 1 on refusal, unavailable spk, or drift/missing in --check mode.


ah doctor

Detect configured frameworks and diagnose config, path, hook, and archetype issues.

ah doctor [--enable <capability>]

Flags:

FlagDescription
--enable <capability>Write the config block for a detected-but-unconfigured capability

Capabilities for --enable: pytest, cargo, vitest, mutation, property, snapshot

Example output:

framework: pytest (configured)
recommendation: vitest detected via manifest — run: ah doctor --enable vitest

Example — enable vitest:

ah doctor --enable vitest

Writes the [runners.vitest] entry to .espectacular/config.toml. If already configured, exits 0 with an “already enabled” message.

Exit codes: 0 when no problems are found (recommendations do not affect exit code); 1 when structural problems exist. --enable exits 0 on success, non-zero for unknown capabilities.

ah doctor --json

Emit diagnostic output as JSON, with each recommendation appearing as a structured finding.

ah doctor --json
{
  "findings": [
    {
      "kind": "recommendation",
      "suggested_action": "enable_capability",
      "playbook_command": "ah explain enable_capability",
      "apply_command": "ah doctor --enable cargo",
      "detail": "cargo detected via manifest",
      "capability": "cargo"
    }
  ]
}

Each recommendation finding carries playbook_command and apply_command for agent-consumable remediation.

Exit codes: same as ah doctor.


ah report

Display a conformance coverage matrix across all deployed specs and archetypes.

ah report [--json]

Flags:

FlagDescription
--jsonEmit the matrix as JSON

Text output (default):

spec                 archetype     covered   missing   failing    total
compiler                                      0         0         0         3
adapters                                      0         0         0         1
parser                                         0         1         0         1

covered: 3 | missing: 1 | failing: 0 | total: 5

JSON output (--json):

{
  "matrix": [
    {
      "spec": "compiler",
      "archetype": "",
      "covered": 3,
      "missing": 0,
      "failing": 0,
      "total": 3
    }
  ],
  "summary": {
    "total_scenarios": 5,
    "total_contracts": 4,
    "covered": 3,
    "missing": 1,
    "failing": 0
  }
}

Exit codes: 0 when all scenarios are covered by contracts; 1 when any scenarios are missing or failing.


ah explain

Print guidance for a finding kind or suggested action.

ah explain [<topic>] [--list] [--json]

Arguments:

ArgumentDescription
<topic>Finding kind or action slug to explain

Flags:

FlagDescription
--listList all available topics
--jsonEmit the topic list as JSON (use with --list)

Example — explain a finding:

ah explain no-toml
## no-toml — Missing contract file

A scenario declared in a spec file has no corresponding contract .toml file
in .espectacular/<component>/.

How to fix: run `ah scenario new` with the spec, scenario id, and heading
to generate the contract stub, then populate the test entries.

    ah scenario new <change> <spec> --requirement <scenario-id> <heading>

Example — list all topics:

ah explain --list

Exit codes: 0 on success; 1 for unknown topics (includes “did you mean” suggestions).


ah signals

Read dont rejection events and emit drift signals as JSON.

ah signals

dont is a companion tool that tracks epistemic claims made by AI agents. When an agent makes a claim that is later rejected, dont records it as an event. ah signals reads those events from .dont/events/*.json and re-emits them as structured DriftSignal JSON that CI or the wai project-context tool can consume to surface spec-behavior gaps.

Exit codes: always 0; returns an empty JSON array if no events are found.


ah type

List built-in archetypes, or print full documentation for one.

ah type [<code>]

Example — list all:

ah type
PF — Pure Functional: Deterministic behavior where outputs are a function of explicit inputs.
SA — Stateful API: Behavior involving state transitions, persisted data, or ordered operations.
BP — Boundary Protocol: Behavior at an external boundary or protocol seam.
CE — Contract/Event: Behavior expressed as emitted events, messages, claims, or cross-tool signals.
NR — Non-Regression: Behavior asserting existing guarantees remain true while nearby changes land.

Example — full docs for one archetype:

ah type PF
## PF — Pure Functional

Deterministic behavior where outputs are a function of explicit inputs.

Use for:
- parsers
- formatters
- validators
- pure transformations
- deterministic calculations

Typical test shapes:
- unit examples for representative inputs
- property-based tests for invariants
- boundary input examples

Exit codes: 0 on success; 1 for unknown archetype codes (includes “did you mean” suggestions).


ah scenario new

Append a new scenario to a spec and stage its TOML contract.

ah scenario new <change> <spec> --requirement "<scenario-id>" "<heading>"

Arguments:

ArgumentDescription
<change>Change id (the change directory must exist under openspec/changes/)
<spec>Spec name (e.g. parser)
--requirementRequirement grouping name — must match a ### Requirement: heading already in the spec; the new #### Scenario: is appended under it
<heading>Human-readable scenario heading to append

Appends the scenario under the named requirement block and creates the contract stub at .espectacular/changes/<change>/<spec>/<scenario-id>.toml.

Exit codes: 0 on success; 1 if the change or spec is missing, or the requirement block is absent.


ah scenario supersede

Stage a supersession update for an existing contract.

ah scenario supersede <spec> <old-id> --with=<new-id> --in-change=<change>

Marks the old contract status = "superseded" and sets superseded_by to the new scenario id. The replacement scenario must already exist in the named change overlay.

Exit codes: 0 on success; 1 if the deployed contract or replacement is missing.


ah archive

Move staged change contracts into deployed .espectacular/ locations.

ah archive <change>

Run after a change merges. Moves TOML files from .espectacular/changes/<change>/ into .espectacular/<spec>/, failing if any collision would overwrite an active contract without a supersession in place.

Exit codes: 0 on success; 1 on collision or missing staged change.


ah upgrade

Report tool-version drift and update .espectacular/config.toml.

ah upgrade

Compares tool_version in .espectacular/config.toml with the running binary version. Updates the config if they differ, then exits non-zero so CI can detect compatibility changes.

Exit codes: 0 when versions match; 1 when drift is detected (even after updating the config).

Implementation Status

This page maps the deployed behavioral specs to the commands and capabilities that implement them. Each row is a requirement in a spec (its scenarios under it); if ah check passes, that behavior is verified in CI.

Last refreshed from the deployed corpus at v0.11.0 (spec scenario counts re-derive any time via openspec/specs/*/spec.md).

Deployed specs

gate — Core verification engine (91 scenarios)

Covers what ah check does: scenario discovery, contract correspondence, test execution (including batched structured-output runners and range-based selection), JSON output, agent-actionability, quality signals, and property-derived contracts with ah sync.

RequirementWhat it verifies
Scenario Discoveryah check finds all scenarios from spec headings; mirrored headings deduplicate; same-id/different-body collisions fail
Sidecar Contract Correspondenceevery scenario has a contract; every contract has a scenario
Contract SchemaTOML contracts validate against the schema; superseded status requires a replacement
Test Runner Executiondeclared tests run per-binding with exit-code verdicts; eligible structured-output bindings batch into one invocation above the threshold (with per-binding attribution, matched-zero, fallback, timeout semantics)
JSON Findingsoutput is a stable JSON envelope with scope/summary/findings
Change Overlay Scope--changes adds staged scenarios; illegal overlays are rejected
Non-Regression ArchetypeNR contracts run in overlay scope
Deterministic Scope Boundarythe gate never inspects test internals or hashes prose
Agent-action fieldsevery finding carries suggested_action, playbook_command, and conditionally apply_command
Quality measurement capabilitiesquality-* findings are informational; tool failures fail the gate
Quality contract schemaquality config is not a test entry; property/snapshot are runnable
Conformance coverage matrixper-spec/per-archetype coverage, machine-readable
Property-Derived Contractslint-clean dual-format specs derive one contract per Properties row (ah sync, with stale-drift detection and human-owned field preservation)
Dual-Format Spec Corpusdual-format spec discovery and gating behave byte-identically for plain specs and for dual-format specs without Properties rows
Range-Based Test Selectionah check --run-tests --base/--head selects by commit range with total precedence and conservative fallback

(All gate scenarios are verified — run ah check --run-tests for the live count.)

cli — Command surface (53 scenarios)

Covers the full ah command interface: init, check, doctor, report, explain, type, scenario, archive, upgrade, lint, signals, feedback, sync, completions — plus JSON-envelope exit semantics (OK agrees with the exit code) and recommendation findings.

RequirementWhat it verifies
CLI Command Namebinary is named ah
Project Initializationah init creates .espectacular/ and hook integration
Correspondence Check Commandah check validates specs and runs tests
Spec Lint Commandah lint analyzes spec files for authoring-quality findings
Sync Commandah sync derives contracts from Properties rows (--check mode for CI)
Health Check Commandah doctor diagnoses config, paths, hooks, archetypes
Archetype Documentation Commandsah type lists and explains archetypes
Scenario Lifecycle Commandsah scenario new and ah scenario supersede
Archive Companion Commandah archive promotes staged contracts
Upgrade Commandah upgrade detects and reports tool-version drift
Doctor enable flagah doctor --enable <capability> writes config blocks
Explain subcommandah explain prints guidance for finding kinds and actions
Coverage report commandah report displays a conformance coverage matrix
Recommendation findingsfindings that need human review are flagged as such

lint — Spec-quality heuristics (32 scenarios)

Covers the ah lint checks: vague qualifiers, imperative steps, conjunctive bloat, missing negative scenarios, missing non-goals, unresolved ambiguity, entangled specs, finding schema, and the specodelic bridge (dual-format relay + relay completeness + property trace).

RequirementWhat it verifies
Vague Qualifier Detectionvague-qualifier precision (bounded qualifiers not flagged)
Imperative Step Detectionimperative-step detection
Conjunctive Step Bloat Detectionconjunctive-bloat with configurable limit
Missing Negative Scenario Detectionhappy-path-only requirements flagged
Missing Non-Goals / Unresolved Ambiguity / Entangled Specification Detectionstructural authoring checks
Lint Finding Schemafindings validate the lint schema
Dual-Format Lint Bridge / Relay Completeness / Scenario Property Tracespecodelic rule findings relay through ah lint; scenario/property trace advisories

adapters — Language adapter dispatch (27 scenarios)

Covers how ah check maps test types to runners and normalizes output.

RequirementWhat it verifies
Adapter detection precedenceconfig > manifest > binary on PATH
Python pytest adapterpytest detection, invocation, failure normalization
Rust cargo test adaptercargo detection, invocation, failure normalization
TypeScript vitest adaptervitest detection, invocation, failure normalization
No-adapter-configured pathmissing-runner finding when type has no runner
Custom runner plugin protocolcustom runners emit JSON envelopes parsed by ah check

explain — Playbook system (19 scenarios)

Covers the ah explain topic system and its compile-time completeness guarantee.

RequirementWhat it verifies
Playbook is compile-enforcedevery finding kind has an ah explain topic at compile time
Topic coverageall finding kinds and suggested actions are covered
Structured JSON output--json emits a machine-readable topic list
Topic listing / Quality finding topics / Adapter topicslisting shape; quality and adapter topics ship with their features
Unknown topic handlingunknown topics exit 1 with “did you mean” suggestions

config, cli-core, spec-authoring — Infrastructure (16 scenarios)

Shared config management (4), CLI scaffold guide + ah sync command (7 across 2 requirements), dual-format spec authoring (5).


How to read this page

  • Deployed means the scenario has a passing contract in ah check on main.
  • Planned means the scenario is staged in an OpenSpec change but not yet implemented (none currently — all active changes are fully implemented and archived).
  • Run ah check locally to see current pass/fail state.
  • Spec source lives in openspec/specs/.

Concepts

In brief: A spec declares what your tool should do. A contract says which tests verify one scenario in that spec. ah check is the gate — it exits non-zero when a contract is missing or a test fails. See the worked example in Installation to see these pieces together.


The mental model

espectacular enforces a contract between what you said your tool does (specs) and whether it actually does it (tests). Each behavioral claim lives in a spec file; each claim has a sidecar TOML contract that says how to verify it. ah check validates that every claim has a contract and that every contract’s tests pass.


Specs

A spec is a Markdown file under openspec/specs/<name>/spec.md. It describes the intended behavior of one component using ### Requirement: groupings and #### Scenario: headings nested under them.

### Requirement: Input validation

#### Scenario: Empty input is rejected
- **GIVEN** an empty string is passed to the parser
- **WHEN** the parser runs
- **THEN** it exits non-zero with a descriptive error message

Specs are checked into version control. They are append-only: you never rewrite a deployed scenario’s intent — instead you add new scenarios or supersede old ones. The git log becomes a traceable record of when each behavior was declared, extended, or superseded.


Scenarios

A scenario is one #### Scenario: heading nested under a ### Requirement: grouping in a spec file. It has:

  • A slug — derived from the scenario heading (by convention, lowercased with hyphens, e.g. empty-input-is-rejected). This slug must match the contract filename and the id field inside it.
  • A body in Given/When/Then form describing the behavior

ah check discovers scenarios by parsing #### Scenario: headings from spec files. Each scenario must have a corresponding contract file, or ah check emits a no-toml finding. Run ah explain no-toml for the fix.


Contracts

A contract is a TOML file at .espectacular/<spec>/<scenario-id>.toml. It is the machine-readable pairing between a scenario and the tests that verify it.

id = "empty-input-is-rejected"
description = "Empty input is rejected before parsing."
archetype = "PF"
status = "active"
superseded_by = ""
authored_with = "0.1.0"

[[tests.pytest]]
flags = "tests/test_parser.py::test_empty_input_rejected"
timeout_seconds = 60

Required fields: id, description, archetype, status, superseded_by, authored_with, tests.

The filename, the id field, and the scenario slug must all agree — ah check emits id-mismatch if they disagree.

Falsifiability class

Contracts may declare an optional falsifiability_class:

  • "safety" — the scenario asserts a safety claim: a bad state never occurs. A single deterministic test can falsify it. Annotation-only in v1; it records the author’s judgment and gives downstream tooling a hook.
  • "liveness" — the scenario asserts a liveness claim: a good state eventually occurs. A liveness claim is only falsifiable under bounded execution, so if every test entry omits timeout_seconds, ah check emits a missing-liveness-timeout warning suggesting an explicit timeout. Warnings do not fail the gate.

The field is optional and absent by default — contracts without it validate exactly as before. Invalid values emit an invalid-falsifiability-class structural finding and fail the gate.

Test entry types

EntryHow it runs
[[tests.shell]]Runs command directly via /bin/sh -c
[[tests.pytest]]Prepends the configured pytest runner to flags
[[tests.cargo]]Prepends the configured cargo runner to flags
[[tests.vitest]]Prepends the configured vitest runner to flags
[[tests.custom]]Invokes a custom runner and parses its JSON envelope
[[tests.<type>]]Any other type, looked up in [runners.<type>] in config

Staged contracts

During a change in progress, contracts live under .espectacular/changes/<change>/<spec>/. After the change merges, ah archive <change> moves them into .espectacular/<spec>/. See Installation — Step 5.


Archetypes

Every contract declares an archetype, a short code that classifies the kind of behavior being verified. Archetypes guide test design: a PF scenario should have deterministic unit tests; an SA scenario needs to cover state transitions.

CodeNameDescription
PFPure FunctionalDeterministic behavior; outputs are a function of explicit inputs
SAStateful APIState transitions, persisted data, or ordered operations
BPBoundary ProtocolBehavior at an external boundary or protocol seam
CEContract/EventEmitted events, messages, claims, or cross-tool signals
NRNon-RegressionExisting guarantees remain true while nearby changes land

Run ah type <code> for full documentation on any archetype, or ah type to list all.


The gate

ah check is the enforcement gate. It:

  1. Discovers all scenarios from deployed specs (and staged change overlays if --changes is passed)
  2. Checks structural correspondence: every scenario has a contract, every contract has a scenario, no collisions, no orphans
  3. Runs every declared test
  4. Emits a JSON envelope and exits 0 if clean, 1 if any structural or execution finding exists

Local pre-commit (ah init installs this): catches structural findings before git push instead of in CI. CI (ah check in a workflow step): the enforcement gate, source of truth.

Quality findings (quality-mutation, quality-property, quality-snapshot) are informational — they appear in the output but never cause a non-zero exit. See Command Reference — ah check for the full finding kind table.


Adapters

An adapter maps a test type name to the right runner invocation and normalizes failure output into a common shape. espectacular ships built-in adapters for pytest, cargo, and vitest.

Detection signals (highest-priority source wins):

AdapterExplicit configManifest detectionBinary detection
pytestrunners.pytest in configpyproject.toml [tool.pytest], pytest.inipytest on PATH or .py with import pytest
cargorunners.cargo in configCargo.toml present—
vitestrunners.vitest in configpackage.json devDependency—

ah doctor shows which frameworks are detected and their source. Use ah doctor --enable <framework> to write the config entry for a detected-but-unconfigured adapter.

Custom runners

A [[tests.custom]] entry invokes a configured runner and expects a JSON envelope on stdout:

{ "exit_code": 0, "passed": true, "findings": [] }

Non-zero exit_code or passed: false maps to a test-failing finding. The findings array lets custom runners emit structured findings directly into ah check output.


Quality signals

Quality signals are optional capabilities that surface test health metadata as findings:

SignalWhat it tracks
quality-mutationMutation testing kill rate vs. threshold
quality-propertyProperty-based testing is active and passing
quality-snapshotSnapshot testing is active and passing

Enable them in .espectacular/config.toml or via ah doctor --enable mutation (etc.). Quality findings are informational and never cause ah check to exit non-zero.


File layout

openspec/
└── specs/<spec>/spec.md            # deployed spec source

.espectacular/
├── config.toml                     # runner and capability config
├── AGENTS.md                       # AI agent guidance
├── <spec>/
│   └── <scenario-id>.toml          # deployed contracts
└── changes/
    └── <change>/
        └── <spec>/
            └── <scenario-id>.toml  # staged change contracts

schemas/
├── check-output.schema.json        # ah check JSON envelope
├── config.schema.json              # .espectacular/config.toml
├── scenario-contract.schema.json   # contract TOML files
└── custom-runner.schema.json       # custom runner JSON envelope

Agent Workflow: ah check as Specification Gate

This document describes how AI agents discover and use ah check — the espectacular verification gate — in the standard development cycle. It is extracted from observed patterns across the charly-vibes project ecosystem.


How Agents Discover ah check

Agents learn about ah check through the ah:managed block in a project’s AGENTS.md (or CLAUDE.md). This block is deployed by ah init and refreshed by ah init.

<!-- ah:managed:start -->
## espectacular

Run `ah check` to verify spec-test correspondence before committing.

- `ah check` — validate all deployed specs
- `ah check --changes <name>` — validate with a change overlay
- `ah init` — set up or refresh espectacular project files
- `ah doctor` — diagnose setup issues
- `ah explain <topic>` — playbook guidance for finding kinds
- `ah doctor --enable <adapter>` — write adapter config into config.toml
- `ah signals` — emit dont drift signals
<!-- ah:managed:end -->

Key design decisions:

  • The block is compact (fits in ~15 lines of agent context) — agents scan it early in a session and recognize ah check as a commit gate.
  • It lists only the most common commands — ah explain provides the full playbook for less frequent tasks.
  • The ah:managed delimiter signals to agents that this section is auto-generated and authoritative.

Typical Usage Patterns

1. Pre-commit gate (most common)

The standard loop when working on OpenSpec projects:

edit → ah check → fix → ah check → commit

Concrete example:

This pattern was observed in testaruda pi sessions, where ah check was the primary feedback loop during contract wiring and spec changes. Source sessions: 2026-07-08T15-35-34-510Z, 2026-07-16T01-38-10-034Z.

# 1. Make changes to spec or code
# 2. Run the gate
ah check

# 3. If findings appear, fix them
# 4. Re-run to verify
ah check

# 5. Commit when green
git add -p
git commit -m "fix: ..."

When to run ah check:

SituationWhether to run
Modified spec filesAlways
Modified contract TOML filesAlways
Modified production code tracked by contractsAlways
Edited tests onlyOptional (contracts pass/fail)
Edited docs onlyUsually not needed
Changed a change overlayah check --changes <name>

2. Change-overlay validation

When a staged OpenSpec change is in progress:

ah check --changes add-parser-validation

This validates the base spec plus the overlay’s additional scenarios, without requiring the overlay to be archived first.

Multiple overlays can be validated together by repeating the flag:

ah check --changes add-parser-validation --changes fix-dangling-contracts

Useful when interdependent changes are being developed simultaneously.

3. Fresh project setup

ah init            # Scaffold .espectacular/, stub contracts
ah doctor          # Verify setup is complete
ah doctor --enable cargo   # Configure runner
ah check           # Run initial validation

4. Post-ah init verification

After bootstrapping a new project, ah init stubs contracts for every scenario missing one. The pattern is:

ah init            # Stub missing contracts
ah check           # Verify — expect structural findings for no-tests-declared
# Fill in test commands for each stub contract, then:
ah check           # Green — all contracts wired
git add -A
git commit -m "chore: init espectacular and stub contracts"

How Findings Are Interpreted

ah check emits a JSON envelope with a findings array. Each finding has a kind, category, and suggested_action that agents can act on.

Finding categories

CategoryMeaningExit code impact
structuralSpec/contract mismatch (missing contract, orphan, duplicate ID)Non-zero
executionContract test failed or timed outNon-zero
quality-*Mutation score below threshold, property coverage gapZero (advisory)

Common findings and agent responses

Finding kindWhat it meansAgent action
missing-contractA scenario has no contract fileah explain run_ah_scenario_new → ah scenario new
orphan-contractA contract file has no matching scenarioRemove or archive the contract
no-tests-declaredContract exists but has no test commandsFill in [[tests.shell]] or [[tests.flags]]
collisionTwo specs declare the same scenario IDRename one scenario heading
test-failingA contract test exited non-zeroInspect test output, fix code
no-tests-ranShell test ran no tests (exit 0, no output)Run ah check --json to inspect the full captured output. The contract’s [[tests.shell]] command may not match any test files, or the adapter may not be detecting the test framework. Check config and test command.
duplicate-idSame scenario ID appears in two specsDeduplicate
mutation-below-thresholdMutation kill rate too lowAdd or improve test cases

Before starting work, ensure ah is available in PATH:

which ah || cargo install --git git@cv:charly-vibes/espectacular.git

Quick reference: interpreting output

{
  "summary": { "structural": 0, "execution": 0, "passed": 3, "counts_by_kind": {} },
  "findings": []
}

→ Green. All checks pass. Ready to commit.

{
  "summary": { "structural": 1, "execution": 0, "passed": 0, "counts_by_kind": { "missing-contract": 1 } },
  "findings": [ { "kind": "missing-contract", "category": "structural", ... } ]
}

→ Structural finding. A scenario has no contract file. Run ah scenario new to create one, then ah check again.

{
  "summary": { "structural": 0, "execution": 1, "passed": 2, "counts_by_kind": { "test-failing": 1 } },
  "findings": [ { "kind": "test-failing", "category": "execution", ... } ]
}

→ Execution finding. A contract test failed. Fix the code, then ah check again.

For full guidance on any finding kind:

ah explain <finding-kind>

CI/script usage

In CI pipelines or automated scripts, use --json for machine-readable output:

ah check --json
# Exit code: 0 if no structural/execution findings, 1 otherwise

The --run-tests flag forces contract test execution even when no spec changes are detected (useful in CI when dependencies may have changed).

Test selection (avoiding full contract runs)

By default ah check --run-tests executes every declared contract test. When a changed-file set is known, pass it to narrow the run:

# Explicit file list (repo-relative, comma-separated)
ah check --run-tests --files src/check.rs,openspec/specs/compiler/spec.md

# Omit --files to default to the worktree git diff
ah check --run-tests

# Escape hatch: ignore changes and run everything
ah check --run-tests --all-tests

Selection is layered and conservative — it never produces a hidden green:

  1. Capability selection (always on): changed files are mapped to capabilities via spec provenance (deployed specs, change-overlay specs, and contract trees). Only affected capabilities’ contracts run. Files that map to no capability trigger a conservative run-all, reported as source: "bypassed" in the JSON selection object.
  2. testaruda refinement (when a .testaruda/ store exists): within the run, contract bindings whose runner filter matches nothing in testaruda select --agent’s selected test set are pruned. Guards keep this safe: an EMPTY selection (exit 20 — no code affected) skips the refinement but still runs the capability set; unresolved changed files or a selection where no binding matches disable pruning entirely. Pruning is reported as selection.testaruda.pruned in JSON.

The JSON selection object summarizes what ran:

"selection": {
  "selected": ["compiler"],
  "skipped": 3,
  "source": "spec-provenance",
  "unmapped_files": 0,
  "testaruda": { "pruned": 1 }
}

source is spec-provenance (capability selection narrowed the run), all-tests (escape hatch), or bypassed (conservative run-all — e.g. the changed set contains files that map to no capability).

Forcing a commit through findings

On a WIP branch or when findings are acceptable, you can commit despite non-zero exit. This is common during staged change development:

ah check --changes my-change
# If findings are scoped to the change and expected, proceed
git commit -m "wip: my change"

For CI-only enforcement, run ah check in CI and allow local commits to pass — the managed block in AGENTS.md should still instruct agents to resolve findings.


Commit Workflow with ah check as Gate

Standard workflow

1. bd claim <ticket>        # Claim work
2. git pull --rebase        # Sync
3. [edit code / specs]      # Make changes
4. ah check                 # Verify spec-test correspondence
5. [fix findings]           # Iterate until green
6. git add <files>          # Stage specific files (never git add -A)
7. git commit -m "..."      # Commit with descriptive message
8. ah check                 # Verify pre-push (catches regressions from merge)
9. bd close <ticket>        # Close ticket

When findings are expected

Some tickets produce findings that are expected and resolved within the same ticket. Example: a ticket that adds a new scenario creates a missing-contract finding until the contract file is written.

In those cases, the workflow is:

1. ah check                 # Baseline — note existing findings
2. [edit code / specs]      # Make changes
3. ah check                 # Verify — expected findings should match ticket scope
4. [fix unexpected findings]
5. ah check                 # Green — all expected findings resolved
6. git add && git commit

Pre-push hooks (optional)

Projects with lefthook or prek can integrate ah check as a pre-push hook. This is configured by ah init and runs automatically:

# .lefthook.yml (auto-generated by ah init)
pre-push:
  commands:
    ah-check:
      run: ah check
      skip: false  # Block push on failure

If you want ah check to run but not block the push (advisory mode), set skip: true. This lets you push while still seeing the output.

The actual config generated by ah init uses skip: false by default.


---

## Agent Workflow Diagram

┌─────────────────────────────────────────────────────────┐ │ Session Start │ │ │ │ 1. Agent reads AGENTS.md → discovers ah:managed block │ │ 2. Agent runs ah check to establish baseline │ │ 3. Agent reads any existing findings │ └─────────────────────┬───────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ Development Loop │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ Edit │───▶│ah check │───▶│ Fix │ │ │ │ code/ │ │ │ │ findings │──┐ │ │ │ specs │ └──────────┘ └──────────┘ │ │ │ └──────────┘ ▲ │ │ │ │ findings remain │ │ │ └────────────────────────┘ │ │ │ │ │ green │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ ah check passes │ │ │ └────────┬──────────┘ │ └─────────────────────────────────┬───────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ Commit │ │ │ │ 1. git add │ │ 2. git commit -m “fix: …” │ │ 3. (optional) ah check pre-push │ │ 4. bd close │ └─────────────────────────────────────────────────────────┘


---

## Adding `ah check` to a New Project

For agent operators who want to replicate this pattern:

```bash
# 1. Install ah
cargo install --git git@cv:charly-vibes/espectacular.git

# 2. Ensure the project has an openspec/ directory
#    (the root AGENTS.md or CLAUDE.md will be updated
#     automatically by ah init)

# 3. Run init
ah init

# 4. Verify
ah doctor    # Should show healthy
ah check     # Should have findings from stub contracts

# 5. Verify the ah:managed block was written to AGENTS.md
#    (ah init does this automatically — no manual edits needed)

Sources

This document is extracted from observed agent workflow patterns in the following pi sessions:

  • 2026-07-08T15-35-34-510Z — Heavy ah check usage during contract wiring for the v0.2.0 release (espectacular project)
  • 2026-07-16T01-38-10-034Z — Recent workflow showing the edit → ah check → fix → ah check → commit cycle (testaruda project)

Both sessions demonstrate the ah check feedback loop in practice. Testaruda was the first OpenSpec project to adopt ah check as a regular development gate, and its patterns were replicated across the charly-vibes ecosystem.

Audit: Spec-Validation Patterns Across Charly Projects

Ticket: espectacular-892 Scope: All charly-vibes OpenSpec projects (2026-04 through 2026-07) Source session: 019f72d1-e2bf-7d6f-a707-403228d4ee3b


Executive Summary

Before ah check was deployed to all 13 charly projects on 2026-07-20, agents had no standard tool for spec-test correspondence validation. They relied on a mix of openspec validate --strict (structure-only), cargo test / just test (test-only), and manual inspection. This audit documents those patterns as evidence for the pi extension ticket (espectacular-c9v).


Patterns Observed

Pattern 1: openspec validate --strict (primary pre-espectacular tool)

Used consistently from April through July 2026 across all projects with OpenSpec specs. Validates spec structure (headings, scenarios, IDs) but does not verify contracts or tests.

openspec validate --strict
openspec validate --all --strict
openspec validate --specs --strict
openspec validate add-change-name --strict

Found in: 22+ log entries across 10 projects (atril, fabbro, fotos, khipu, miblioteca, nayra, paranoid, pretender, wai, tRAGar).

Pain points:

  • --strict flag required every invocation (easy to forget)
  • No test execution integration — agents still needed to run cargo test separately
  • No contract file validation — only spec structural checks
  • No JSON output for CI/script consumption
  • Exit code behavior was unclear

Pattern 2: cargo test / just test as spec-validation proxy

Agents frequently used test runners as a proxy for spec verification, often paired with openspec validate:

just test && openspec validate --all --strict

Found in: 15+ log entries across 7 projects.

Pain points:

  • Two separate commands for one validation intent
  • No feedback that tests correspond to specs — only that tests pass
  • Projects without openspec validate integration just ran cargo test and assumed spec alignment

Pattern 3: Manual spec-to-test reading (no tool)

Agents read spec files and test files manually to verify correspondence:

"read spec" → "read test file" → "compare scenarios" → "fix" → "commit"

Found in: Occasional entries where agents reviewed scenarios alongside test files (2026-04-20 atril session, 2026-04-30 REPLy.jl).

Pain points:

  • Entirely manual — no automation
  • High cognitive load for contract wiring
  • Easy to miss scenarios or leave orphans

Pattern 4: ah check (current — espectacular)

After espectacular v0.2.0, the intended tool:

ah check                    # Fast static validation
ah check --changes <name>   # With staged change overlay
ah check --json             # Machine-readable

Deployment history:

  • 2026-06-18: First dogfooded in espectacular itself
  • 2026-07-08: v0.2.0 — all espectacular contracts wired, 0 findings
  • 2026-07-15: v0.2.2 — human-readable output (silent JSON before)
  • 2026-07-20: Deployed to all 13 openspec projects
  • 2026-07-21: Performance fix (--run-tests flag, fast default)

Gap (pre-deployment): Only 2/27 projects had ah check available. Agents didn’t use it because it wasn’t there to discover.


Pain Points (Evidence for espectacular-c9v)

#Pain PointBeforeAfterSeverity
1Two-step validationopenspec validate --strict + cargo testah check does bothHIGH
2No contract verificationopenspec validate only checked spec structureah check validates contracts + runs testsHIGH
3No CI integrationopenspec validate JSON output unclearah check --json has stable schemaMEDIUM
4Agent discoveryNo AGENTS.md block → agents didn’t know to validateah:managed block in AGENTS.mdHIGH
5Manual spec-test mappingAgents read spec + test files by handah check reports missing/orphan contractsMEDIUM
6Forgotten --strictEasy to validate without the right flagsah check defaults to comprehensiveLOW
7No test output in findingsopenspec validate had no test executionah check captures test output in findingsMEDIUM

Timeline

April 2026
  └─ openspec validate --strict  (sole tool, all projects)
  └─ just test / cargo test      (as spec proxy)

June 2026
  └─ ah check dogfooded in espectacular
  └─ openspec validate still used elsewhere

July 10
  └─ Last openspec validate usage (tRAGar project)
  └─ ah check v0.2.0 in espectacular

July 15
  └─ ah check v0.2.2 (human-readable output)

July 18
  └─ espectacular-892 ticket created (this audit)
  └─ Deployment begins

July 20
  └─ ah check deployed to all 13 openspec projects

July 21
  └─ Performance fix, --run-tests flag
  └─ This audit completed

Recommendations for Pi Extension (espectacular-c9v)

  1. Discovery is the primary gap. After deployment, agents still only use ah check if the ah:managed block is in their context window. A pi custom tool makes discovery unconditional.

  2. Pain point #1 (two-step validation) is the strongest motivator. Agents ran two commands (openspec validate + cargo test) where one should suffice. ah check already solves this — the pi extension just needs to expose it.

  3. Pain point #4 (agent discovery) is the blocker. The ah:managed block works when an agent reads it, but agents don’t always read AGENTS.md in every session. A pi tool bypasses this entirely.

  4. Manual spec-test mapping (#5) is the rarest but most expensive pattern — it wastes agent context on mechanical comparison.

Authoring Dual-Format Specs

espectacular supports the specodelic dual-format protocol as an opt-in authoring mode: a single spec.md can carry both the openspec grammar (## ADDED|MODIFIED|REMOVED Requirements + #### Scenario: headings) and the specodelic four-layer grammar (frontmatter intent, Constraints, Model, Properties), each linted by its own tool.

  • openspec half → validated by openspec validate <change-id> --strict
  • specodelic half → linted by spk lint <file>
  • espectacular’s gate (ah check) reads only the openspec half; the specodelic layers are inert decoration it happens to understand.

A worked example lives in openspec/changes/adopt-dual-format-specs/specs/gate/spec.md — this page explains its anatomy.

Anatomy of a dual-format file

---
id: spec                      ← 1. YAML frontmatter (intent layer)
kind: intent
statement: "WHEN … THE … SHALL …"
---

# <capability heading>

Prose (optional). Never parsed by the gate.

## Constraints                ← 2. Constraints table (invariant layer)

| id | kind | expr | traces_to |
|----|------|------|-----------|
| C-mirror-dedupe | invariant | two `#### Scenario:` headings … | |

## Model                      ← 3. Model (states + transitions)

### States
- `scanning`

### Transitions

| id | from | to | guard |
|----|------|----|-------|
| t-scan | scanning | deduplicating | [[spec.C-tables-inert]] |

## Properties                 ← 4. Properties table

| id | kind | derives_from | generator | predicate |
|----|------|--------------|-----------|-----------|
| P-dedupe | unit | [[spec.C-mirror-dedupe]] | … | … |

## MODIFIED Requirements      ← openspec half (unchanged grammar)

### Requirement: Scenario Discovery
…

#### Scenario: Reject duplicate scenario ids
- **GIVEN** …
- **WHEN** …
- **THEN** …

Section-sync mirror

In change deltas, the specodelic protocol mirrors requirement text into a trailing ## Requirements section so deployed-spec tooling sees a stable section spelling. The mirror must be textually identical to the delta section (specodelic section-sync).

The gate understands this: two #### Scenario: headings in the same file with the same slugified id and identical bodies are treated as one logical scenario and discovered exactly once. It emits no slug-collision finding for the mirror.

Table derivation

Scenario prose may be derived into structured tables (e.g. a Properties generator/predicate column quoting a scenario). This is safe by construction: the gate only discovers scenarios from lines that start with #### Scenario: . YAML frontmatter, Constraints rows, Model states and transitions, and Properties rows never yield scenarios — no matter what scenario-like text they quote.

Enforcement is mandated in this repo (opt-in for adopters)

espectacular deliberately does not make its product require the two-grammar format: plain openspec repos remain fully valid, and ah check treats both grammars as valid (C-adopters-unaffected).

This repository, however, dogfoods the mandate (change migrate-specs-to-dual-format, capability spec-authoring):

  • pre-commit — spk lint openspec (deep lint of dual-format files) plus a staged-file frontmatter mandate (spec-corpus-staged) that catches an archive strip at commit time;
  • corpus gate — just spec-lint fails while any non-archived spec.md lacks the specodelic half, and runs in CI;
  • CI — the spec-gate job installs specodelic from crates.io and runs the same mandate.

The migration recipe for converting a plain file lives in openspec/changes/migrate-specs-to-dual-format/design.md.

In-gate section-sync drift signal

Because mirrored sections must be textually identical, a same-id, different-body collision in a dual-format repo is not just a naming accident — it is section-sync drift: one of the two mirrored copies was edited without the other. ah check catches this as a slug-collision structural finding without running spk lint, which makes the gate a cheap first-line drift detector for dual-format repos. (spk lint remains the authoritative section-sync linter; the gate’s signal is a byproduct of collision detection, not a replacement.)

Linting both halves

# specodelic half
spk lint openspec/changes/<change-id>/specs/<capability>/spec.md

# openspec half
openspec validate <change-id> --strict

# gate (both grammars in scope)
ah check --changes <change-id>

All three must exit zero before the change is deployed; the gate additionally proves every discovered scenario has a contract.

Archive round-trip

openspec archive merges the openspec half of a delta into the deployed spec but strips the specodelic half (frontmatter and four-layer tables do not survive). The strip is not silent: just spec-lint (pre-commit staged mandate, corpus gate, and CI) fails until the half is re-derived.

Re-derivation recipe (not a blind copy — archive merges):

  1. Copy the frontmatter block from the archived delta (openspec/changes/archive/<change>/specs/<capability>/spec.md).
  2. Carry over the surviving constraint/property rows from the pre-archive deployed spec (git history).
  3. Add rows for any newly merged requirements.
  4. Re-run the gates: spk lint, openspec validate --all --strict, section sync, ah check.

An issue is filed against specodelic (specodelic#7) asking for the dual-format corpus to survive the archive round-trip natively; once fixed, the recipe becomes a no-op.

Scenario-to-property coverage (VERIFIES) and derived contracts

A Properties row is test intent; a scenario contract is executable intent. The VERIFIES bullet links the two. When a scenario body carries

#### Scenario: Ship invalid token
- **WHEN** a token fails validation
- **THEN** it is rejected
- **VERIFIES** [[spec.P-not-shipped]]

the gate (ah check) redirects the scenario at the property’s derived contract — the .toml that ah sync generated from the Properties row — and runs its tests. Coverage is declared once, in the spec, instead of restated in a hand-written contract.

ah sync — deriving contracts from Properties rows

ah sync          # create-or-refresh derived contracts
ah sync --check  # CI mode: report missing + stale contracts, write nothing

ah sync refuses to derive from a lint-dirty file (spk lint runs first — a file that fails specodelic invariants is never derived from) and fails with spk-unavailable when the binary is missing. It is idempotent: re-running after a spec edit refreshes the derived fields of already- generated contracts (that drift detection is the feature).

Field ownership (design D3)

FieldsOwnerRule
id, description, archetype, falsifiability_class, derived_fromsyncoverwritten on every sync; hand edits make the contract stale (contract-stale finding + ah sync --check exit 1)
tests, status, superseded_byhumannever overwritten by sync

derived_from = "<property-id>@<hash>" pins the exact Properties row the contract was derived from; editing any row cell (id, kind, derives_from, generator, predicate) changes the hash and marks the contract stale until the next sync.

Test naming rule (design D6)

A derived contract binds to a human-owned test named after the property id: property P-not-shipped → test function p_not_shipped, bound via tests.cargo.flags = ["p_not_shipped"]. The test body is yours; the name is the contract’s binding convention, so the gate can prove the binding exists structurally and execute it with ah check --run-tests.

Governance: contracts are committed artifacts

Derived contracts live beside hand-written contracts (.espectacular/<capability>/<id>.toml) and are committed — lockfile-like artifacts refreshed by sync, not disposable tool state. Only .espectacular/state/ and other runtime state stay uncommitted.

Advisory trace findings

ah lint reports the coverage convention’s gaps as advisory findings (never exit-non-zero): verifies-dangling (a VERIFIES bullet cites a property with no row), scenario-unlinked (a scenario without a bullet in a file that has Properties rows — legitimate under the D1 fallback to per-scenario contracts), and property-untraced (a row no scenario cites). They are the adoption TODO list, not gate failures.


id: gate kind: intent statement: “WHEN ah check validates a spec corpus THE gate SHALL discover scenarios, require one schema-valid sidecar contract each, execute declared tests with exit-code verdicts, emit stable JSON findings with agent-action fields, support deterministic change overlays and opt-in quality measurement plus an NR archetype and a conformance coverage matrix, never semantically evaluate test quality or scenario prose, derive one scenario contract per Properties row in a lint-clean dual-format spec, cover scenarios carrying a VERIFIES link via that property’s contract, and detect derived-contract drift against its source row hash.”


gate Specification

Constraints

idkindexprtraces_to
C-scenario-discoveryinvariantscenarios are discovered from #### Scenario: headings in spec.md files including dual-format files; identical mirrored headings deduplicate to one; same slugified id with different bodies emits a slug-collision finding and exits non-zero; frontmatter and structured table layers contribute no scenarios[[gate]]
C-sidecar-correspondenceinvarianteach discovered scenario in scope requires exactly one TOML sidecar contract; missing, orphaned, mismatched-id, or empty-test-set contracts emit the corresponding structural finding and fail the gate[[gate]]
C-contract-schemainvariantcontract metadata (id, description, archetype, status, authored_with) is validated before tests run; unknown status emits invalid-status and exits non-zero; superseded requires a non-empty superseded_by and still runs declared tests[[gate]]
C-runner-executioninvarianteach declared test command runs with its exit code as the verdict: unit tests map through config runners, shell tests run inline, timeouts are enforced, output tails are bounded, missing runners / invalid TOML / malformed test entries are structural findings, and a non-zero declared test fails the check[[gate]]
C-json-findingsinvariantah check emits stable JSON: success with empty findings, findings in stable order, actionable scenario context with verbatim body boundaries, checked scope, and command details for execution findings[[gate]]
C-overlay-scopeinvariantah check --changes <id> overlays selected changes on deployed specs: added scenarios, staged metadata updates, and signaled modified scenario text apply; unsignaled redefinition, supersession with missing replacement, conflicting overlays, and conflicting staged updates for one scenario are rejected; resolution is deterministic[[gate]]
C-nr-archetypeinvariantthe NR (Non-Regression) archetype is a valid contract archetype, runs in change overlay scope, and ah upgrade reports it as an archetype addition[[gate]]
C-scope-boundaryinvariantthe gate never semantically evaluates test quality or scenario prose — it does not inspect test internals and does not hash scenario prose[[gate]]
C-agent-actionsinvariantevery finding carries suggested_action and playbook_command, scenario_prose is verbatim and untruncated, findings sort deterministically, and the summary counts by kind[[gate]]
C-quality-measurementinvariantopt-in quality measurement capabilities run during ah check emitting measurement findings without failing the gate on below-threshold scores in v1; property/snapshot command failure and mutation tool execution failure fail the gate; mutation is off in pre-commit scope by default[[gate]]
C-quality-schemainvariantquality measurement config ([quality.mutation], property, snapshot) is not a test entry, while property and snapshot declarations are runnable test entries — baseline tests.<type> arrays stay runnable declarations[[gate]]
C-coverage-matrixinvarianta per-spec, per-archetype coverage matrix aggregates scenario contract status across all specs in scope, counts covered scenarios and archetype totals, reports missing contracts as uncovered, and is available machine-readable[[gate]]
C-apply-commandinvariantapply_command is set only when the finding’s suggested_action maps to a concrete mechanical shell command (e.g., enable_capability), and is null for findings requiring human review or code edits[[gate]]
C-property-contractinvariantin a dual-format spec whose specodelic half is spk lint-clean, each Properties row derives exactly one scenario contract whose id is the slugified property id[[gate]]
C-verifies-coversinvarianta scenario carrying a VERIFIES bullet linking to an existing property id is covered by that property’s derived contract and emits no no-tests-declared finding[[gate]]
C-unlinked-unchangedinvarianta scenario without a VERIFIES link keeps the per-scenario contract requirement and may still emit no-tests-declared[[gate]]
C-derived-staleinvarianta derived contract whose derived_from hash does not match the canonical serialization of its source property row emits a contract-stale structural finding[[gate]]
C-sync-ownershipinvariantah sync refreshes only derived fields (id, description, archetype, falsifiability_class, derived_from) and never overwrites human-owned fields (tests, status, superseded_by)[[gate]]
C-plain-unaffectedadvisoryplain openspec specs and dual-format specs without Properties rows discover, gate, and check byte-identically to before this change[[gate]]
C-batch-thresholdinvariantbindings of one runner type with structured-reporter support and pattern-only flags batch into a single OR-joined invocation when their count exceeds the fixed threshold (8), and never batch at or below it[[gate]]
C-batch-attributioninvarianta batched contract passes only when every structured-output test matched by its pattern reports passed; any matched test failed or skipped/todo emits a test-failing or no-tests-ran verdict for that contract[[gate]]
C-batch-matched-zeroinvarianta batched contract whose pattern matches no test in the structured output emits no-tests-ran regardless of the batched invocation exit code[[gate]]
C-batch-fallbackinvariantan unparseable, errored, or timed-out batched invocation emits a named fallback signal and re-runs its bindings per-contract with exit-code verdicts[[gate]]
C-batch-timeoutinvariantthe batched invocation runs under the maximum timeout_seconds across its batched entries and reports timed_out on expiry before falling back[[gate]]
C-unbatched-unchangedinvariantrunner types without structured-reporter support, bindings with JS-only regex constructs, and sets at or below the threshold keep per-binding exit-code execution byte-identical[[gate]]
C-range-diffinvariantwhen --base <ref> is given with --run-tests and no explicit file list, the changed-file set is git diff --name-only <base> <head>, with --head defaulting to HEAD[[gate]]
C-range-precedenceinvariantthe changed-file source precedence is total: --all-tests disables selection, --files wins over --base, --base wins over the worktree diff, and the worktree diff remains the only no-flag source[[gate]]
C-range-safeinvarianta range that yields no files or whose refs fail to resolve runs every declared contract; the gate never exits green having executed zero tests because of a degenerate range[[gate]]
C-range-unchangedinvariantwith no --base/--head, selection input, report shape, finding kinds, and exit codes are byte-identical to pre-change behavior[[gate]]

Model

States

  • discovering
  • validating
  • executing
  • attributed
  • reporting
  • deriving

Transitions

idfromtoguard
t-discoverdiscoveringdiscovering[[gate.C-scenario-discovery]] ([[gate.C-overlay-scope]] widens scope)
t-corresponddiscoveringvalidating[[gate.C-sidecar-correspondence]]
t-schemavalidatingvalidating[[gate.C-contract-schema]]
t-collectvalidatingvalidating[[gate.C-range-diff]] AND [[gate.C-range-precedence]] (the changed-file set resolves from files, range, or worktree diff per the total precedence)
t-executevalidatingexecuting[[gate.C-runner-execution]] ([[gate.C-nr-archetype]] and [[gate.C-quality-schema]] shape what runs)
t-qualityexecutingexecuting[[gate.C-quality-measurement]]
t-selectvalidatingvalidating[[gate.C-range-safe]] (a degenerate resolved set selects every capability, never green with zero tests executed)
t-attributeexecutingattributed[[gate.C-batch-attribution]]
t-report-batchedattributedreporting[[gate.C-batch-matched-zero]]
t-reportexecutingreporting[[gate.C-json-findings]] AND [[gate.C-agent-actions]] AND [[gate.C-coverage-matrix]] AND [[gate.C-apply-command]] AND [[gate.C-range-unchanged]]
t-boundreportingreporting[[gate.C-scope-boundary]]
t-derivediscoveringderiving[[gate.C-property-contract]]
t-matchderivingvalidating[[gate.C-verifies-covers]]
t-stalederivingreporting[[gate.C-derived-stale]]
t-ownderivingvalidating[[gate.C-sync-ownership]]
t-unlinkeddiscoveringvalidating[[gate.C-unlinked-unchanged]]

Properties

idkindderives_fromgeneratorpredicate
P-discoveryunit[[gate.C-scenario-discovery]]deployed and dual-format specs, mirrored deltas, and same-id/different-body collisionsone scenario per logical scenario; collisions fail with findings; structured layers inert
P-correspondenceunit[[gate.C-sidecar-correspondence]]scenarios with missing, orphaned, mismatched, and empty contractseach emits its structural finding and fails the gate
P-schemaunit[[gate.C-contract-schema]]contracts with valid, unknown, and superseded statusvalidation precedes execution; invalid status fails; superseded runs only with a replacement
P-executionunit[[gate.C-runner-execution]]unit, shell, timeout, bounded-output, structural-breakage, and failing-test contractsexit-code verdicts with bounded tails; structural breakage fails before running; non-zero fails the check
P-jsonunit[[gate.C-json-findings]]check runs from clean to finding-rich scopesstable ordering, verbatim bodies, scope and command details present
P-overlayunit[[gate.C-overlay-scope]]overlays applying additions, staged updates, signaled modifications, and each rejection caselegal overlays apply deterministically; every illegal case is rejected
P-nrunit[[gate.C-nr-archetype]]an NR contract in overlay scope and an ah upgrade runNR validates, runs in scope, and upgrades report the archetype addition
P-boundaryunit[[gate.C-scope-boundary]]a passing test with poor quality and prose mutationsverdicts and outputs are unchanged by test internals or prose content
P-agent-actionsunit[[gate.C-agent-actions]]any finding-producing check runevery finding has both agent fields; prose verbatim; deterministic sort; kind counts in summary
P-qualityunit[[gate.C-quality-measurement]]enabled mutation, declared property/snapshot, threshold crossings, tool failures, pre-commit scopemeasurement findings never fail on scores in v1; tool/command failures do; mutation off pre-commit
P-quality-schemaunit[[gate.C-quality-schema]]contracts with quality blocks and quality-declared runnable entriesquality config never counted as a test entry; property/snapshot run as declared tests
P-matrixunit[[gate.C-coverage-matrix]]multi-spec scopes with missing contractsmatrix counts coverage per spec and archetype, marks missing as uncovered, and is machine-readable
P-apply-commandunit[[gate.C-apply-command]]findings across suggested_action classesapply_command non-null only for mechanical actions
P-deriveunit[[gate.C-property-contract]]a lint-clean dual-format file with N property rowsthe contract set contains exactly N derived contracts with slugified ids
P-coversunit[[gate.C-verifies-covers]]a scenario carrying a VERIFIES link to an existing property idno-tests-declared is suppressed for that scenario
P-unlinkedunit[[gate.C-unlinked-unchanged]]a scenario without a VERIFIES linkthe gate requires its own contract exactly as before this change
P-staleunit[[gate.C-derived-stale]]a derived contract whose source row predicate is edited after synccontract-stale is emitted and ah sync –check exits non-zero without writing
P-ownershipunit[[gate.C-sync-ownership]]a derived contract with hand-filled tests and status, re-synced after a predicate editderived fields change; tests, status, and superseded_by are byte-identical
P-unaffectedunit[[gate.C-plain-unaffected]]a corpus of plain openspec specs and Properties-less dual-format specsdiscovery and check output is byte-identical to pre-change behavior
P-batch-countunit[[gate.C-batch-threshold]]a corpus with more than 8 vitest bindings sharing one runnerexactly one runner invocation executes for that runner type; a corpus with 8 or fewer keeps per-binding spawns
P-attributionunit[[gate.C-batch-attribution]]a batched contract whose pattern matches two tests where one failsthat contract emits test-failing while contracts whose matched tests all pass succeed
P-zerounit[[gate.C-batch-matched-zero]]a batched invocation whose JSON output contains no test matched by one contract’s patternthat contract emits no-tests-ran while the invocation’s exit code does not cover it
P-fallbackunit[[gate.C-batch-fallback]]a batched invocation whose structured output is truncateda named fallback signal is emitted and every affected binding re-runs per-contract
P-timeoutunit[[gate.C-batch-timeout]]batched entries declaring timeouts of 10 and 30 secondsthe invocation runs under a 30-second bound and a hang reports timed_out before fallback
P-unchangedunit[[gate.C-unbatched-unchanged]]a corpus of cargo and shell bindingscheck output is byte-identical to pre-change behavior
P-range-filesunit[[gate.C-range-diff]]a fixture repo where one capability’s spec changes after the base commitah check --run-tests --base <base-sha> runs only that capability’s contract; the other capability’s contract is absent from the selection report
P-range-precedenceunit[[gate.C-range-precedence]]the same fixture invoked with --files, with --all-tests, and with no flagsthe explicit file list wins over the range; --all-tests runs everything; the no-flag run uses the worktree diff
P-range-safeunit[[gate.C-range-safe]]the fixture invoked with --base HEAD and with --base not-a-refboth invocations run every declared contract and exit per findings, never green with zero tests executed
P-range-unchangedunit[[gate.C-range-unchanged]]the fixture invoked with no range flagscheck output is byte-identical to pre-change behavior

Non-Goals

  • Evaluating test quality, assertion strength, or scenario prose semantics — the gate only checks correspondence and runs declared commands (see C-scope-boundary)
  • Authoring or repairing the tests inside scenario contracts — test content is human/agent-owned and never generated by the gate or ah sync
  • Reimplementing specodelic lint, graph, or derivation rules — the gate consumes spk output verbatim and degrades silently when spk is unavailable
  • CI dashboards, trend storage, or historical drift tracking — ah report and ah check emit point-in-time snapshots only

Purpose

The behavioral verification gate: scenario discovery, sidecar correspondence, contract schema validation, test execution with exit-code verdicts, stable JSON findings, deterministic change overlays, opt-in quality measurement, the NR archetype, and the conformance coverage matrix. Since derive-contracts-from-specodelic, the gate also derives one scenario contract per Properties row of a lint-clean dual-format spec, covers VERIFIES-linked scenarios via that property’s contract, and detects derived-contract drift against the source row hash.

Requirements

Requirement: Scenario Discovery

The system SHALL discover OpenSpec scenarios from #### Scenario: headings in spec.md files, including dual-format files that additionally carry the specodelic four-layer grammar, deduplicating section-sync mirrors and ignoring structured layers.

Scenario: Discover deployed scenario

  • GIVEN openspec/specs/compiler/spec.md contains #### Scenario: Empty input rejected
  • WHEN ah check scans deployed specs
  • THEN it discovers a scenario with id empty-input-rejected
  • AND associates it with the compiler spec
  • VERIFIES [[gate.P-discovery]]

Scenario: Reject duplicate scenario ids

  • GIVEN two scenarios in the same spec slugify to the same id and have different bodies
  • WHEN ah check validates the spec
  • THEN it emits a structural finding for the slug collision
  • AND exits non-zero
  • VERIFIES [[gate.P-discovery]]

Scenario: Deduplicate mirrored delta sections

  • GIVEN a change delta carries identical #### Scenario: headings with identical bodies in both ## ADDED Requirements and the mirrored ## Requirements section
  • WHEN a user runs ah check --changes <change-id>
  • THEN each mirrored scenario is discovered exactly once
  • AND the gate emits no slug-collision finding for the mirror
  • VERIFIES [[gate.P-discovery]]

Scenario: Parse dual-format deployed spec

  • GIVEN a deployed spec under openspec/specs/ carries YAML frontmatter and specodelic tables in addition to its ## Requirements section
  • WHEN ah check scans deployed specs
  • THEN scenarios are discovered from the #### Scenario: headings exactly as before
  • AND frontmatter, constraint rows, model states and transitions, and property rows contribute no scenarios
  • VERIFIES [[gate.P-discovery]]

Scenario: Plain openspec specs remain valid

  • GIVEN a spec file contains no frontmatter and no specodelic tables
  • WHEN ah check scans the spec
  • THEN discovery behaves identically to before this change
  • VERIFIES [[gate.P-discovery]]

Requirement: Sidecar Contract Correspondence

The system SHALL require exactly one TOML sidecar contract for each discovered scenario in scope.

Scenario: Missing contract fails

  • GIVEN a scenario id empty-input-rejected exists under openspec/specs/compiler/spec.md
  • AND .espectacular/compiler/empty-input-rejected.toml does not exist
  • WHEN a user runs ah check
  • THEN the command emits a no-toml structural finding
  • AND exits non-zero
  • VERIFIES [[gate.P-correspondence]]

Scenario: Orphan contract fails

  • GIVEN .espectacular/compiler/empty-input-rejected.toml exists
  • AND no matching scenario exists under openspec/specs/compiler/spec.md
  • WHEN a user runs ah check
  • THEN the command emits an orphan-toml structural finding
  • AND exits non-zero
  • VERIFIES [[gate.P-correspondence]]

Scenario: Contract id mismatch fails

  • GIVEN .espectacular/compiler/empty-input-rejected.toml contains id = "different-id"
  • AND the matching scenario slug is empty-input-rejected
  • WHEN a user runs ah check
  • THEN the command emits an id-mismatch structural finding
  • AND exits non-zero
  • VERIFIES [[gate.P-correspondence]]

Scenario: Empty test set fails

  • GIVEN a scenario contract declares no tests
  • WHEN a user runs ah check
  • THEN the command emits a no-tests-declared structural finding
  • AND exits non-zero
  • VERIFIES [[gate.P-correspondence]]

Requirement: Contract Schema

The system SHALL validate per-scenario TOML contracts before running tests. Contracts MAY declare an optional falsifiability_class field with value safety or liveness; an invalid value is a structural finding, and a liveness-tagged contract whose test entries all lack timeout_seconds produces a warning that does not fail the gate.

Scenario: Validate scenario metadata

  • GIVEN a scenario contract contains id, description, archetype, status, and authored_with
  • WHEN a user runs ah check
  • THEN the command validates the metadata fields before executing tests
  • VERIFIES [[gate.P-correspondence]]

Scenario: Reject unknown status

  • GIVEN a scenario contract has status = "paused"
  • WHEN a user runs ah check
  • THEN the command emits an invalid-status structural finding
  • AND exits non-zero
  • VERIFIES [[gate.P-schema]]

Scenario: Validate superseded status

  • GIVEN a scenario contract has status = "superseded"
  • WHEN a user runs ah check
  • THEN the command requires a non-empty superseded_by value
  • AND still runs the scenario’s declared tests
  • VERIFIES [[gate.P-schema]]

Scenario: Accept contract without falsifiability_class

  • GIVEN a scenario contract declares no falsifiability_class field
  • WHEN a user runs ah check
  • THEN the contract validates and behaves exactly as it did before the field existed
  • AND no finding related to falsifiability_class is emitted
  • VERIFIES [[gate.P-schema]]

Scenario: Accept valid falsifiability_class values

  • GIVEN a scenario contract has falsifiability_class = "safety" (or "liveness")
  • WHEN a user runs ah check
  • THEN the contract validates and runs its declared tests normally
  • VERIFIES [[gate.P-schema]]

Scenario: Reject invalid falsifiability_class value

  • GIVEN a scenario contract has falsifiability_class = "eventual"
  • WHEN a user runs ah check
  • THEN the command emits an invalid-falsifiability-class structural finding
  • AND exits non-zero without running tests
  • VERIFIES [[gate.P-schema]]

Scenario: Warn on liveness contract without any test timeout

  • GIVEN a scenario contract has falsifiability_class = "liveness"
  • AND every declared test entry omits timeout_seconds
  • WHEN a user runs ah check
  • THEN the command emits a missing-liveness-timeout finding with severity = "warning"
  • AND the finding suggests adding explicit timeout semantics, since liveness claims are only falsifiable under bounded execution
  • VERIFIES [[gate.P-schema]]

Scenario: No warning when liveness contract declares a timeout

  • GIVEN a scenario contract has falsifiability_class = "liveness"
  • AND at least one declared test entry specifies timeout_seconds
  • WHEN a user runs ah check
  • THEN no missing-liveness-timeout finding is emitted
  • VERIFIES [[gate.P-schema]]

Scenario: No liveness warning when the contract declares no tests

  • GIVEN a scenario contract has falsifiability_class = "liveness"
  • AND the contract declares no test entries at all
  • WHEN a user runs ah check
  • THEN the command emits a no-tests-declared structural finding
  • AND no missing-liveness-timeout warning is emitted
  • VERIFIES [[gate.P-schema]]

Scenario: Warnings do not fail the gate

  • GIVEN ah check produces only missing-liveness-timeout warnings and no error-severity findings
  • WHEN the run completes
  • THEN the command exits zero
  • VERIFIES [[gate.P-schema]]

Requirement: Test Runner Execution

The system SHALL run every declared test binding and derive each binding’s execution verdict from its command’s exit code, or, when eligible bindings of one structured-output runner are batched into a single invocation, from that invocation’s structured per-test output.

Scenario: Run configured unit test

  • GIVEN .espectacular/config.toml maps unit to ["uv", "run", "pytest"]
  • AND a scenario contract declares [[tests.unit]] with flags = "tests/test_parser.py::test_empty_input"
  • WHEN a user runs ah check
  • THEN the command executes argv ["uv", "run", "pytest", "tests/test_parser.py::test_empty_input"] without a shell from the repository root
  • AND records the command exit code in JSON output
  • VERIFIES [[gate.P-execution]]

Scenario: Run shell test

  • GIVEN a scenario contract declares [[tests.shell]] with command = "ah --version | grep -q 'ah '"
  • WHEN a user runs ah check
  • THEN the command executes the shell command through /bin/sh -c from the repository root
  • AND records the command exit code in JSON output
  • VERIFIES [[gate.P-execution]]

Scenario: Enforce test timeout

  • GIVEN a declared test command runs longer than its configured timeout
  • WHEN a user runs ah check
  • THEN the command stops the test command
  • AND emits a test-failing execution finding with timed_out = true
  • AND exits non-zero
  • VERIFIES [[gate.P-execution]]

Scenario: Capture bounded output tails

  • GIVEN a declared test command writes more than 8 KiB to stdout and stderr
  • WHEN ah check emits JSON output
  • THEN the execution finding includes only the final 8 KiB of stdout
  • AND includes only the final 8 KiB of stderr
  • VERIFIES [[gate.P-execution]]

Scenario: Missing runner fails structurally

  • GIVEN a scenario contract declares [[tests.integration]]
  • AND .espectacular/config.toml does not define runners.integration
  • WHEN a user runs ah check
  • THEN the command emits a missing-runner structural finding
  • AND exits non-zero
  • VERIFIES [[gate.P-execution]]

Scenario: Invalid TOML syntax fails structurally

  • GIVEN a scenario contract file contains invalid TOML syntax
  • WHEN a user runs ah check
  • THEN the command emits a malformed-contract structural finding
  • AND exits non-zero
  • VERIFIES [[gate.P-execution]]

Scenario: Malformed test entry fails structurally

  • GIVEN a non-shell test entry omits flags
  • WHEN a user runs ah check
  • THEN the command emits a malformed-contract structural finding
  • AND exits non-zero
  • VERIFIES [[gate.P-execution]]

Scenario: Non-zero declared test fails check

  • GIVEN a declared test command exits non-zero
  • WHEN a user runs ah check
  • THEN the command emits a test-failing execution finding
  • AND exits non-zero
  • VERIFIES [[gate.P-execution]]

Scenario: Batch eligible bindings into one invocation

  • GIVEN more than 8 contracts declare bindings for one runner type with structured-reporter support and pattern-only flags
  • WHEN a user runs ah check --run-tests
  • THEN exactly one runner invocation executes for that runner type with an OR-joined pattern and the structured reporter
  • VERIFIES [[gate.P-batch-count]]

Scenario: Attribute batched contract from structured output

  • GIVEN a batched contract whose pattern matches two tests where one fails
  • WHEN a user runs ah check --run-tests
  • THEN that contract emits a test-failing execution finding
  • AND contracts whose matched tests all passed succeed
  • VERIFIES [[gate.P-attribution]]

Scenario: Matched-zero batched contract still fails

  • GIVEN a batched invocation whose structured output contains no test matched by one contract’s pattern
  • WHEN a user runs ah check --run-tests
  • THEN that contract emits a no-tests-ran execution finding
  • AND the batched invocation’s exit code does not cover it
  • VERIFIES [[gate.P-zero]]

Scenario: Batch fallback re-runs per binding

  • GIVEN a batched invocation whose structured output is truncated and unparseable
  • WHEN a user runs ah check --run-tests
  • THEN a named fallback signal is emitted in JSON output
  • AND every affected binding re-runs per-contract with exit-code verdicts
  • VERIFIES [[gate.P-fallback]]

Scenario: Batched timeout bound

  • GIVEN batched entries declare timeouts of 10 and 30 seconds
  • WHEN the batched invocation hangs beyond 30 seconds
  • THEN the invocation stops with timed_out = true
  • AND every affected binding re-runs per-contract with exit-code verdicts
  • VERIFIES [[gate.P-timeout]]

Scenario: Unbatched runners unchanged

  • GIVEN a corpus of cargo and shell bindings
  • WHEN a user runs ah check --run-tests
  • THEN each binding spawns per-contract with exit-code verdicts
  • AND the check output is byte-identical to pre-change behavior
  • VERIFIES [[gate.P-unchanged]]

Requirement: JSON Findings

The system SHALL emit stable JSON output for ah check results.

Scenario: Report success with empty findings

  • GIVEN every scenario in scope has a valid contract
  • AND every declared test command exits zero
  • WHEN a user runs ah check
  • THEN the command exits zero
  • AND emits JSON with findings = []
  • VERIFIES [[gate.P-json]]

Scenario: Report all findings in stable order

  • GIVEN multiple scenarios have findings
  • WHEN a user runs ah check
  • THEN the JSON output includes all findings
  • AND orders them by spec path and scenario id
  • VERIFIES [[gate.P-json]]

Scenario: Include actionable scenario context

  • GIVEN a scenario has a finding
  • WHEN ah check emits JSON output
  • THEN the finding includes the scenario id, spec path, scenario title, and scenario body markdown
  • VERIFIES [[gate.P-json]]

Scenario: Extract scenario body boundaries

  • GIVEN a scenario heading is followed by markdown body lines and then another #### heading
  • WHEN ah check emits JSON output for that scenario
  • THEN body_markdown contains only the lines after the scenario heading and before the next heading whose level is #### or higher
  • VERIFIES [[gate.P-json]]

Scenario: Include checked scope

  • WHEN ah check emits JSON output
  • THEN the top-level JSON includes whether deployed specs were checked
  • AND includes any selected OpenSpec changes
  • VERIFIES [[gate.P-json]]

Scenario: Include command details for execution findings

  • GIVEN a declared test command exits non-zero
  • WHEN ah check emits JSON output
  • THEN the finding includes the test type, command, exit code, timeout flag, stdout tail, and stderr tail when available
  • VERIFIES [[gate.P-json]]

Requirement: Change Overlay Scope

The system SHALL support checking selected OpenSpec changes as overlays on deployed specs.

Scenario: Check selected change overlay

  • GIVEN openspec/changes/add-parser/specs/compiler/spec.md adds a scenario
  • AND .espectacular/changes/add-parser/compiler/<scenario>.toml exists
  • WHEN a user runs ah check --changes add-parser
  • THEN the command validates the deployed compiler spec plus the add-parser scenario overlay
  • VERIFIES [[gate.P-overlay]]

Scenario: Apply staged metadata update for deployed scenario

  • GIVEN .espectacular/changes/add-parser/compiler/old-behavior.toml has status = "superseded"
  • AND deployed spec compiler contains scenario old-behavior
  • WHEN a user runs ah check --changes add-parser
  • THEN the command validates the staged contract as the active contract for old-behavior in the overlay
  • VERIFIES [[gate.P-overlay]]

Scenario: Apply modified scenario text from change overlay

  • GIVEN a change delta defines scenario old-behavior for spec compiler, which exists in the deployed specs, with different body text
  • AND the change stages a contract at .espectacular/changes/<change>/compiler/old-behavior.toml
  • WHEN a user runs ah check --changes <change>
  • THEN the overlay scenario text replaces the deployed scenario text in scope
  • AND no overlay-conflict finding is emitted
  • VERIFIES [[gate.P-overlay]]

Scenario: Reject unsignaled scenario redefinition

  • GIVEN a change delta defines scenario old-behavior for spec compiler, which exists in the deployed specs, with different body text
  • AND the change stages no contract for old-behavior
  • WHEN a user runs ah check --changes <change>
  • THEN the command emits an overlay-conflict structural finding
  • AND exits non-zero
  • VERIFIES [[gate.P-overlay]]

Scenario: Reject supersession with missing replacement

  • GIVEN .espectacular/changes/add-parser/compiler/old-behavior.toml has status = "superseded"
  • AND superseded_by = "new-behavior"
  • AND no scenario new-behavior exists in deployed specs or the selected change overlay
  • WHEN a user runs ah check --changes add-parser
  • THEN the command emits a structural finding for the missing replacement scenario
  • AND exits non-zero
  • VERIFIES [[gate.P-overlay]]

Scenario: Reject conflicting overlays

  • GIVEN two selected changes define the same new scenario id for the same spec
  • WHEN a user runs ah check --changes first --changes second
  • THEN the command emits a structural finding for the conflict
  • AND exits non-zero
  • VERIFIES [[gate.P-overlay]]

Scenario: Reject conflicting staged updates for one deployed scenario

  • GIVEN two selected changes both stage metadata updates for the same deployed scenario id in the same spec
  • WHEN a user runs ah check --changes first --changes second
  • THEN the command emits an overlay-conflict structural finding
  • AND exits non-zero
  • VERIFIES [[gate.P-overlay]]

Scenario: Overlay resolution is deterministic

  • GIVEN selected changes do not conflict
  • WHEN a user runs ah check --changes zeta --changes alpha
  • THEN the command resolves selected changes in sorted change-id order
  • AND produces the same validation scope as ah check --changes alpha --changes zeta
  • VERIFIES [[gate.P-overlay]]

Requirement: Non-Regression Archetype

The system SHALL support an NR (Non-Regression) archetype for contracts that assert existing behavior is preserved during change proposals.

Scenario: NR contract is valid

  • GIVEN a scenario contract has archetype = "NR"
  • WHEN a user runs ah check
  • THEN the gate accepts NR as a valid archetype value
  • AND validates and runs the contract’s declared tests identically to other archetypes
  • VERIFIES [[gate.P-nr]]

Scenario: NR contract runs in change overlay scope

  • GIVEN a change proposal modifies a capability
  • AND an existing scenario is covered by a contract with archetype = "NR"
  • WHEN a user runs ah check --changes <change-id>
  • THEN the NR contract is validated as part of the overlay scope
  • AND a failing NR test exits non-zero
  • VERIFIES [[gate.P-nr]]

Scenario: ah upgrade reports NR as archetype addition

  • GIVEN .espectacular/config.toml pins a tool version that predates NR support
  • WHEN a user runs ah upgrade
  • THEN the command reports NR as a newly available archetype before updating the configured tool version
  • VERIFIES [[gate.P-nr]]

Requirement: Deterministic Scope Boundary

The system SHALL avoid semantic evaluation of test quality or scenario prose.

Scenario: Do not inspect test internals

  • GIVEN a declared test command exists and exits zero
  • WHEN a user runs ah check
  • THEN the command treats the test as passing
  • AND does not inspect assertions, fixtures, mocks, or setup code
  • VERIFIES [[gate.P-boundary]]

Scenario: Do not hash scenario prose

  • GIVEN the body text under an existing scenario heading changes
  • WHEN a user runs ah check
  • THEN the command does not fail solely because the prose changed
  • VERIFIES [[gate.P-boundary]]

Requirement: JSON finding schema includes agent-action fields

The system SHALL include agent-action fields on every finding in the JSON output.

Scenario: Every finding carries suggested_action

  • GIVEN ah check produces any finding
  • WHEN the JSON output is inspected
  • THEN every finding object contains a suggested_action field with a value from the documented enum
  • VERIFIES [[gate.P-agent-actions]]

Scenario: Every finding carries playbook_command

  • GIVEN ah check produces any finding
  • WHEN the JSON output is inspected
  • THEN every finding object contains a playbook_command field with a valid ah explain <topic> invocation
  • VERIFIES [[gate.P-agent-actions]]

Scenario: scenario_prose is verbatim and untruncated

  • GIVEN a finding references a scenario
  • WHEN the JSON output is inspected
  • THEN the scenario_prose field contains the full markdown body of the scenario heading, verbatim, without truncation
  • VERIFIES [[gate.P-agent-actions]]

Scenario: Findings are sorted deterministically

  • GIVEN ah check produces multiple findings
  • WHEN the JSON output is inspected
  • THEN the findings array is sorted by (spec_path, scenario_id, kind) in ascending lexicographic order
  • VERIFIES [[gate.P-agent-actions]]

Scenario: Summary counts by kind

  • GIVEN ah check produces findings of multiple kinds
  • WHEN the JSON output is inspected
  • THEN the envelope summary.counts_by_kind object contains the count of each finding kind present
  • VERIFIES [[gate.P-agent-actions]]

Scenario: Quality findings carry no scenario prose and never fail the gate

  • GIVEN ah check produces only a quality finding whose mutation tool has no owning scenario
  • WHEN the JSON output is inspected
  • THEN the quality finding carries its message and action fields with scenario_prose = null
  • AND no scenario body is fabricated for it
  • AND the run exits zero — a scenario-less quality finding fails nothing
  • VERIFIES [[gate.P-agent-actions]]

Requirement: Quality measurement capabilities

The system SHALL support opt-in quality measurement capabilities that run during ah check and emit measurement findings without failing the gate.

Scenario: Mutation testing runs when enabled

  • GIVEN a contract declares [quality.mutation] enabled = true
  • AND a mutation tool is configured in .espectacular/config.toml
  • WHEN a user runs ah check --mutation
  • THEN the gate runs the mutation tool against the contract’s declared tests
  • AND emits a quality-mutation info finding with the measured score
  • AND exits zero when the score is below any configured threshold
  • VERIFIES [[gate.P-quality]]

Scenario: Property-based testing runs when declared

  • GIVEN a contract declares a tests.property entry
  • WHEN a user runs ah check
  • THEN the gate runs the property test command
  • AND emits a quality-property finding with the run result
  • VERIFIES [[gate.P-quality]]

Scenario: Snapshot testing runs when declared

  • GIVEN a contract declares a tests.snapshot entry
  • WHEN a user runs ah check
  • THEN the gate runs the snapshot test command
  • AND emits a quality-snapshot finding with the run result
  • VERIFIES [[gate.P-quality]]

Scenario: Quality scores below threshold do not fail the gate in v1

  • GIVEN a quality measurement capability completes successfully and produces a score below threshold
  • WHEN a user runs ah check
  • THEN the finding severity is warning or info
  • AND the overall exit status is zero
  • VERIFIES [[gate.P-quality]]

Scenario: Property or snapshot command failure fails the gate

  • GIVEN a contract declares [[tests.property]] or [[tests.snapshot]]
  • AND the declared command exits non-zero or times out
  • WHEN a user runs ah check
  • THEN the command emits a test-failing execution finding
  • AND the overall exit status is non-zero
  • VERIFIES [[gate.P-quality]]

Scenario: Mutation tool execution failure fails the gate

  • GIVEN mutation measurement is enabled and the mutation tool command exits non-zero before producing a measurement
  • WHEN a user runs ah check --mutation
  • THEN the command emits a test-failing execution finding
  • AND the overall exit status is non-zero
  • VERIFIES [[gate.P-quality]]

Scenario: Mutation is off in pre-commit scope by default

  • GIVEN mutation testing is configured
  • AND ah check is invoked without an explicit --mutation flag
  • WHEN the command runs in pre-commit mode
  • THEN mutation testing is skipped
  • VERIFIES [[gate.P-quality]]

Requirement: Quality contract schema

The system SHALL represent quality measurements without changing the baseline rule that tests.<type> entries are arrays of runnable test declarations.

Scenario: Mutation configuration is not a test entry

  • GIVEN mutation measurement is enabled for a scenario contract
  • WHEN the contract is validated
  • THEN mutation settings are read from a [quality.mutation] table
  • AND tests.mutation as a boolean is rejected as a malformed contract
  • VERIFIES [[gate.P-quality-schema]]

Scenario: Property and snapshot are runnable test entries

  • GIVEN a scenario contract declares [[tests.property]] or [[tests.snapshot]]
  • WHEN the contract is validated
  • THEN each entry follows the same runnable test-entry shape as other tests.<type> arrays
  • VERIFIES [[gate.P-quality-schema]]

Requirement: Conformance coverage matrix

The system SHALL compute a per-spec, per-archetype coverage matrix aggregating scenario contract status across all specs in scope.

Scenario: Matrix counts covered scenarios

  • GIVEN openspec/specs/ contains multiple specs, each with scenarios that have contracts
  • WHEN a user runs ah report
  • THEN the command emits a matrix row for each spec with columns for each archetype
  • AND each cell contains covered, missing, and failing counts
  • VERIFIES [[gate.P-matrix]]

Scenario: Matrix includes archetype totals

  • GIVEN ah report runs against deployed specs
  • WHEN the output is inspected
  • THEN the matrix includes a totals row summing counts across all specs
  • VERIFIES [[gate.P-matrix]]

Scenario: Missing contracts appear as uncovered

  • GIVEN a deployed scenario has no sidecar contract
  • WHEN ah report runs
  • THEN the scenario is counted as missing for its spec row
  • AND the archetype column is unassigned
  • VERIFIES [[gate.P-matrix]]

Scenario: Machine-readable matrix output

  • WHEN a user runs ah report --json
  • THEN the command emits a JSON object with a matrix array
  • AND each row contains spec, archetype, covered, missing, and failing integer fields
  • VERIFIES [[gate.P-matrix]]

Requirement: apply_command is conditionally present

The system SHALL set apply_command only when the finding’s suggested_action maps to a concrete, mechanical shell command; it SHALL be null for findings that require non-mechanical human action.

Scenario: apply_command is set for enable_capability findings

  • GIVEN ah check or ah doctor produces a finding with suggested_action = enable_capability
  • WHEN the JSON output is inspected
  • THEN apply_command contains the ah doctor --enable <capability> invocation
  • VERIFIES [[gate.P-apply-command]]

Scenario: apply_command is null for human_review_required findings

  • GIVEN ah check produces a finding with suggested_action = human_review_required
  • WHEN the JSON output is inspected
  • THEN apply_command is null or absent
  • VERIFIES [[gate.P-apply-command]]

Scenario: apply_command is null for edit_code_not_scenario findings

  • GIVEN ah check produces a finding with suggested_action = edit_code_not_scenario
  • WHEN the JSON output is inspected
  • THEN apply_command is null or absent
  • VERIFIES [[gate.P-apply-command]]

Scenario: apply_command is null for run_ah_sync findings

  • GIVEN ah check produces a contract-stale finding with suggested_action = run_ah_sync
  • WHEN the JSON output is inspected
  • THEN apply_command is null or absent — refreshing derived fields writes TOML, and a command that mutates contract files is refused the mechanical fast-path
  • VERIFIES [[gate.P-apply-command]]

Scenario: review_and_apply findings carry no apply_command

  • GIVEN ah check produces a structural finding with suggested_action = review_and_apply (e.g. a missing sidecar contract)
  • WHEN the JSON output is inspected
  • THEN apply_command is null or absent — correspondence repair requires judgment, and a fabricated one-liner would fail the mechanical-only contract
  • AND no shell invocation is fabricated for it
  • VERIFIES [[gate.P-apply-command]]

Requirement: Property-Derived Contracts

The system SHALL derive one scenario contract per Properties row in a dual-format spec whose specodelic half is lint-clean, using the slugified property id as the contract id, and SHALL treat a scenario carrying a **VERIFIES** [[gate.P-...]] link as covered by that property’s contract.

Scenario: Derive contracts from properties

  • GIVEN a dual-format spec is spk lint-clean and carries three Properties rows
  • WHEN ah sync runs
  • THEN exactly three derived contracts exist with slugified property ids
  • AND each records derived_from = "<property-id>@<hash>"
  • VERIFIES [[gate.P-derive]]

Scenario: Cover linked scenario

  • GIVEN a scenario carries - **VERIFIES** [[gate.P-not-shipped]] and the property’s derived contract exists
  • WHEN ah check validates the spec
  • THEN the scenario emits no no-tests-declared finding
  • VERIFIES [[gate.P-covers]]

Scenario: Unlinked scenario unchanged

  • GIVEN a scenario carries no VERIFIES link
  • WHEN ah check validates the spec
  • THEN the scenario requires its own contract exactly as before this change
  • VERIFIES [[gate.P-unlinked]]

Scenario: Plain specs unaffected

  • GIVEN a corpus of plain openspec specs and dual-format specs without Properties rows
  • WHEN discovery and check run over the corpus
  • THEN their output is byte-identical to pre-change behavior
  • VERIFIES [[gate.P-unaffected]]

Scenario: Refuse derivation from lint-dirty spec

  • GIVEN a dual-format spec whose specodelic half has a spk lint violation (e.g. a duplicate row id)
  • WHEN ah sync runs
  • THEN the command refuses the file with a named refusal citing the specodelic rule
  • AND no contracts are derived or written for that spec
  • VERIFIES [[gate.P-derive]]

Requirement: Derived Contract Drift

The system SHALL record derived_from = "<property-id>@<hash>" in each derived contract, emit a contract-stale structural finding when the hash no longer matches the source row’s canonical serialization, and refresh only derived fields during ah sync — never tests, status, or superseded_by.

Scenario: Detect stale contract

  • GIVEN a property row’s predicate is edited after its contract was derived
  • WHEN ah check validates the spec
  • THEN a contract-stale structural finding is emitted
  • AND ah sync --check exits non-zero without writing
  • VERIFIES [[gate.P-stale]]

Scenario: Refresh preserves owned fields

  • GIVEN a derived contract has hand-filled tests and status
  • WHEN the source predicate is edited and ah sync re-runs
  • THEN derived fields are updated to match the row
  • AND tests, status, and superseded_by are byte-identical to before
  • VERIFIES [[gate.P-ownership]]

Requirement: Range-Based Test Selection

The system SHALL derive the ah check --run-tests changed-file set from the commit range git diff --name-only <base> <head> when --base is given, and SHALL run every declared contract when the range yields no files or a ref fails to resolve.

Scenario: Select contracts from a commit range

  • GIVEN a fixture repo with two capabilities whose contracts declare tests
  • AND a base commit after which only one capability’s spec changed
  • WHEN a user runs ah check --run-tests --base <base-sha>
  • THEN only the changed capability’s contract tests run
  • AND the other capability’s contract is absent from the selection report
  • VERIFIES [[gate.P-range-files]]

Scenario: Head defaults to HEAD

  • GIVEN the same fixture repo and base commit
  • WHEN a user runs ah check --run-tests --base <base-sha> without --head
  • THEN the selection is identical to ah check --run-tests --base <base-sha> --head HEAD
  • VERIFIES [[gate.P-range-files]]

Scenario: Explicit files win over the range

  • GIVEN both --files <paths> and --base <ref> are given
  • WHEN a user runs ah check --run-tests
  • THEN the explicit file list supplies the changed-file set and the range is ignored
  • VERIFIES [[gate.P-range-precedence]]

Scenario: All-tests overrides the range

  • GIVEN both --all-tests and --base <ref> are given
  • WHEN a user runs ah check --run-tests
  • THEN every declared contract runs regardless of the range
  • VERIFIES [[gate.P-range-precedence]]

Scenario: Empty range runs everything

  • GIVEN --base resolves to the same commit as --head, so the range diff is empty
  • WHEN a user runs ah check --run-tests --base HEAD
  • THEN every declared contract runs
  • AND the run exits per its findings, never green with zero tests executed
  • VERIFIES [[gate.P-range-safe]]

Scenario: Unresolvable ref runs everything

  • GIVEN --base not-a-ref fails to resolve, so git diff exits non-zero
  • WHEN a user runs ah check --run-tests --base not-a-ref
  • THEN every declared contract runs
  • AND the command does not crash and exits per its findings
  • VERIFIES [[gate.P-range-safe]]

Scenario: No range flags keep worktree behavior

  • GIVEN no --base or --head is given
  • WHEN a user runs ah check --run-tests
  • THEN the changed-file set comes from the worktree diff exactly as before
  • AND the selection report, finding kinds, and exit codes are unchanged
  • VERIFIES [[gate.P-range-unchanged]]

Scenario: Range flags are inert without run-tests

  • GIVEN --base <ref> is given without --run-tests
  • WHEN a user runs ah check --base <ref>
  • THEN the structural gate validates the full deployed scope exactly as without the flag
  • AND no contract tests run and no selection report is produced
  • VERIFIES [[gate.P-range-precedence]]

id: cli kind: intent statement: “WHEN a user drives the ah CLI THE system SHALL provide the command surface — idempotent init with hook integration, the deterministic check gate, doctor with capability enablement, archetype docs, append-only scenario lifecycle, archive companion, upgrade drift reporting, explain playbooks, conformance reporting, recommendation findings, and static spec lint — each exiting deterministically.”


cli Specification

Constraints

idkindexprtraces_to
C-cli-nameinvariantthe standalone command-line interface is exposed as ah[[cli]]
C-init-commandinvariantah init is idempotent and prepares a repository for spec-test correspondence checks: it refuses without OpenSpec, stubs existing deployed scenarios, installs a supported pre-commit integration preferring lefthook before prek and falling back to prek, and reports when no hook framework is present[[cli]]
C-check-commandinvariantah check is the deterministic gate command, operating on deployed specs and accepting an OpenSpec change overlay[[cli]]
C-doctor-commandinvariantah doctor diagnoses project setup and correspondence wiring[[cli]]
C-type-commandsinvariantah type exposes built-in archetype guidance: listing archetypes and showing per-archetype details[[cli]]
C-scenario-lifecycleinvariantscenario lifecycle commands author append-only: creating a scenario in a change rejects without a target requirement; superseding rejects when the replacement is missing[[cli]]
C-archive-companioninvariantah archive <change> moves staged scenario contracts after OpenSpec archive; it refuses to run before OpenSpec archive and refuses on collision[[cli]]
C-upgrade-commandinvariantah upgrade makes tool-version drift explicit by reporting compatibility changes[[cli]]
C-doctor-enableinvariantah doctor --enable <capability> for a detected inactive capability writes exactly one config table for it and prints the path and table name written; enabling an unknown capability is an error; enabling an already-active capability is a no-op[[cli]]
C-explain-subcommandinvariantah explain <topic> prints playbook guidance for finding kinds, suggested actions, and general topics, supports JSON output and listing, and errors on unknown topics[[cli]]
C-report-commandinvariantah report displays a conformance coverage matrix across deployed specs and archetype tiers (modeled on the OpenTelemetry per-language compliance matrix); it exits zero when coverage is complete, non-zero when scenarios lack contracts, and supports JSON output[[cli]]
C-recommendation-findingsinvariantah doctor emits recommendation findings for capabilities that are available but not yet configured; each carries its enable command and is a finding kind, not a log line[[cli]]
C-lint-commandinvariantah lint statically analyzes OpenSpec scenario files for quality findings without modifying files or running tests, operating on deployed specs and change overlays, scoping to a single check category, supporting JSON output, and exiting zero with empty findings on a clean spec[[cli]]

Model

States

  • bare
  • prepared
  • gated
  • advised

Transitions

idfromtoguard
t-initbareprepared[[cli.C-init-command]]
t-checkpreparedgated[[cli.C-check-command]] ([[cli.C-lint-command]] widens quality scope)
t-doctorpreparedadvised[[cli.C-doctor-command]] ([[cli.C-doctor-enable]] and [[cli.C-recommendation-findings]] refine it)
t-lifecyclepreparedprepared[[cli.C-scenario-lifecycle]] AND [[cli.C-archive-companion]] AND [[cli.C-upgrade-command]]
t-guidanceadvisedadvised[[cli.C-type-commands]] AND [[cli.C-explain-subcommand]] AND [[cli.C-report-command]]

Properties

idkindderives_fromgeneratorpredicate
P-nameunit[[cli.C-cli-name]]invoking the binaryit answers to ah with help
P-initunit[[cli.C-init-command]]repositories with and without OpenSpec, deployed scenarios, lefthook, prek, and neither hook frameworkidempotent preparation; refusal without OpenSpec; scenario stubs; hook preference order; honest missing-framework report
P-checkunit[[cli.C-check-command]]clean scopes and change overlaysdeterministic gate behavior on both scopes
P-doctorunit[[cli.C-doctor-command]]healthy and broken installationsdiagnoses setup and correspondence wiring
P-typeunit[[cli.C-type-commands]]ah type list and detail invocationsall archetypes listed; details render per archetype
P-lifecycleunit[[cli.C-scenario-lifecycle]]create-with and create-without target requirement, supersede with and without replacementlegal authoring succeeds append-only; both illegal cases rejected
P-archiveunit[[cli.C-archive-companion]]archive runs before OpenSpec archive, after it, and on collisionstaged contracts move only post-archive and refuse collisions
P-upgradeunit[[cli.C-upgrade-command]]version-drifted tool referencescompatibility changes reported explicitly
P-enableunit[[cli.C-doctor-enable]]enabled capabilities: pytest, cargo, vitest, mutation, property, snapshot, unknown, already-activeone config table per enable with reported path/table; unknown errors; active is a no-op
P-explainunit[[cli.C-explain-subcommand]]finding-kind, suggested-action, and general topics with JSON, list, and unknown-topic variantsguidance printed per topic class; JSON valid; list complete; unknown errors
P-reportunit[[cli.C-report-command]]scopes with complete coverage vs missing contractszero exit on completeness, non-zero on gaps, JSON output available
P-recommendationunit[[cli.C-recommendation-findings]]doctor runs over projects with unconfigured available capabilitiesrecommendation findings carry enable commands and are finding kinds
P-lintunit[[cli.C-lint-command]]clean specs, dirty specs, overlays, single categories, JSON outputquality findings without file modification or test execution; clean exits zero with empty findings

Purpose

TBD - created by archiving change add-spec-assertions. Update Purpose after archive.

Non-Goals

  • Defining contract TOML content or test-entry semantics — those live in the gate spec; the CLI only routes commands to it
  • Framework-specific test invocation logic — adapters own detection and dispatch (see the adapters spec)
  • Interactive prompts or wizards — every command is non-interactive and CI-safe
  • Replacing OpenSpec’s own CLI — ah commands complement openspec commands and never parse or rewrite the change/proposal format

Requirements

Requirement: CLI Command Name

The system SHALL expose the standalone command-line interface as ah.

Scenario: Invoke help

  • WHEN a user runs ah --help
  • THEN the CLI displays available ah commands
  • VERIFIES [[cli.P-name]]

Scenario: Unknown subcommand fails with a suggestion

  • WHEN a user runs a subcommand name ah does not implement
  • THEN the command fails with a non-zero exit and a Did you mean suggestion naming the closest known command
  • VERIFIES [[cli.P-name]]

Requirement: Project Initialization

The system SHALL provide an idempotent ah init command that prepares a repository for spec-test correspondence checks.

Scenario: Initialize project files

  • GIVEN a repository contains an openspec/ directory
  • WHEN a user runs ah init
  • THEN the command creates .espectacular/config.toml when it is missing
  • AND writes .espectacular/AGENTS.md
  • AND creates top-level AGENTS.md and CLAUDE.md when they are absent
  • AND refreshes managed ah blocks in top-level instruction files
  • VERIFIES [[cli.P-init]]

Scenario: Refuse initialization without OpenSpec

  • GIVEN a repository does not contain an openspec/ directory
  • WHEN a user runs ah init
  • THEN the command fails without creating .espectacular/
  • VERIFIES [[cli.P-init]]

Scenario: Stub existing deployed scenarios

  • GIVEN deployed OpenSpec scenarios exist under openspec/specs/
  • WHEN a user runs ah init
  • THEN the command creates matching .espectacular/<spec>/<scenario>.toml stubs for scenarios without contracts
  • AND the stubs declare no tests until the user or AI supplies them
  • VERIFIES [[cli.P-init]]

Scenario: Install supported pre-commit integration

  • GIVEN the repository uses lefthook
  • WHEN a user runs ah init
  • THEN the command installs or refreshes a managed pre-commit integration that runs ah check
  • VERIFIES [[cli.P-init]]

Scenario: Prefer lefthook before prek

  • GIVEN the repository has both lefthook and prek configured
  • WHEN a user runs ah init
  • THEN the command installs the managed pre-commit integration through lefthook
  • VERIFIES [[cli.P-init]]

Scenario: Fall back to prek

  • GIVEN the repository uses prek
  • AND does not use lefthook
  • WHEN a user runs ah init
  • THEN the command installs or refreshes a managed pre-commit integration through prek
  • VERIFIES [[cli.P-init]]

Scenario: Report missing hook framework

  • GIVEN the repository does not use lefthook or prek
  • WHEN a user runs ah init
  • THEN the command reports a concern that the user or AI must set up pre-commit integration
  • AND does not write a raw .git/hooks/pre-commit fallback
  • VERIFIES [[cli.P-init]]

Requirement: Correspondence Check Command

The system SHALL provide ah check as the deterministic gate command.

Scenario: Check deployed specs

  • WHEN a user runs ah check
  • THEN the command validates deployed specs under openspec/specs/
  • AND validates matching contracts under .espectacular/<spec>/
  • AND emits JSON output
  • VERIFIES [[cli.P-check]]

Scenario: Check an OpenSpec change overlay

  • WHEN a user runs ah check --changes add-parser
  • THEN the command validates deployed specs plus the add-parser change overlay
  • AND validates staged contracts under .espectacular/changes/add-parser/
  • AND includes the selected change in the JSON scope
  • VERIFIES [[cli.P-check]]

Scenario: Reject a selected change that does not exist

  • GIVEN no openspec/changes/missing-change/specs/ directory exists
  • WHEN a user runs ah check --changes missing-change
  • THEN the command fails with a diagnostic naming the missing change path
  • VERIFIES [[cli.P-check]]

Requirement: Health Check Command

The system SHALL provide ah doctor for installation health checks.

Scenario: Diagnose project setup

  • WHEN a user runs ah doctor
  • THEN the command validates .espectacular/config.toml
  • AND checks managed instruction blocks
  • AND checks supported hook integration
  • AND reports tool-version compatibility concerns
  • VERIFIES [[cli.P-doctor]]

Scenario: Diagnose correspondence wiring

  • WHEN a user runs ah doctor
  • THEN the command reports slug collisions as errors
  • AND reports orphan contracts as errors
  • AND reports unknown archetype names as warnings
  • VERIFIES [[cli.P-doctor]]

Requirement: Archetype Documentation Commands

The system SHALL expose built-in archetype guidance through ah type commands.

Scenario: List archetypes

  • WHEN a user runs ah type
  • THEN the command lists all known archetypes with one-line descriptions
  • AND includes PF, SA, BP, CE, and NR
  • VERIFIES [[cli.P-type]]

Scenario: Show archetype details

  • WHEN a user runs ah type PF
  • THEN the command prints the full built-in documentation for the PF archetype
  • VERIFIES [[cli.P-type]]

Scenario: Reject an unknown archetype code

  • WHEN a user runs ah type NOPE
  • THEN the command exits non-zero and names the known archetype codes
  • AND a near-miss input suggests the closest known code
  • VERIFIES [[cli.P-type]]

Requirement: Scenario Lifecycle Commands

The system SHALL provide commands for append-only scenario authoring.

Scenario: Create scenario in a change

  • GIVEN openspec/changes/add-parser/specs/compiler/spec.md contains ### Requirement: Parser Input Validation
  • WHEN a user runs ah scenario new add-parser compiler --requirement "Parser Input Validation" "Empty input rejected"
  • THEN the command appends a scenario heading under that requirement
  • AND writes placeholder WHEN and THEN lines under the scenario heading
  • AND creates .espectacular/changes/add-parser/compiler/empty-input-rejected.toml with id, empty description, empty archetype, status = "active", empty superseded_by, and authored_with
  • VERIFIES [[cli.P-lifecycle]]

Scenario: Reject scenario creation without target requirement

  • GIVEN openspec/changes/add-parser/specs/compiler/spec.md does not contain ### Requirement: Parser Input Validation
  • WHEN a user runs ah scenario new add-parser compiler --requirement "Parser Input Validation" "Empty input rejected"
  • THEN the command fails without creating or modifying files
  • VERIFIES [[cli.P-lifecycle]]

Scenario: Supersede a scenario

  • GIVEN scenario new-behavior exists in the deployed-plus-add-parser overlay for spec compiler
  • WHEN a user runs ah scenario supersede compiler old-behavior --with=new-behavior --in-change=add-parser
  • THEN the command stages .espectacular/changes/add-parser/compiler/old-behavior.toml
  • AND marks the staged contract as superseded
  • AND records new-behavior as the replacement scenario id
  • VERIFIES [[cli.P-lifecycle]]

Scenario: Reject supersession with missing replacement

  • GIVEN scenario new-behavior does not exist in the deployed-plus-add-parser overlay for spec compiler
  • WHEN a user runs ah scenario supersede compiler old-behavior --with=new-behavior --in-change=add-parser
  • THEN the command fails without creating or modifying files
  • VERIFIES [[cli.P-lifecycle]]

Requirement: Archive Companion Command

The system SHALL provide ah archive <change> to move staged scenario contracts after OpenSpec archive.

Scenario: Archive staged contracts

  • GIVEN openspec archive add-parser has applied the OpenSpec change
  • AND every staged contract id exists in deployed openspec/specs/
  • WHEN a user runs ah archive add-parser
  • THEN the command moves new contracts from .espectacular/changes/add-parser/<spec>/ to .espectacular/<spec>/
  • VERIFIES [[cli.P-archive]]

Scenario: Refuse archive before OpenSpec archive

  • GIVEN .espectacular/changes/add-parser/compiler/new-behavior.toml exists
  • AND deployed openspec/specs/compiler/spec.md does not contain scenario new-behavior
  • WHEN a user runs ah archive add-parser
  • THEN the command fails without moving staged contracts
  • VERIFIES [[cli.P-archive]]

Scenario: Refuse archive collision

  • GIVEN .espectacular/compiler/foo.toml already exists
  • AND .espectacular/changes/add-parser/compiler/foo.toml is not a superseded metadata update for foo
  • WHEN a user runs ah archive add-parser
  • THEN the command fails without overwriting the deployed contract
  • VERIFIES [[cli.P-archive]]

Requirement: Upgrade Command

The system SHALL provide ah upgrade to make tool-version drift explicit.

Scenario: Report compatibility changes

  • GIVEN .espectacular/config.toml pins an older tool version than the installed ah
  • WHEN a user runs ah upgrade
  • THEN the command reports config schema version changes, execution default changes, archetype additions, and archetype deprecations before updating the configured tool version
  • AND does not rewrite existing scenario contract authored_with values
  • VERIFIES [[cli.P-upgrade]]

Scenario: Signal drift to CI with a non-zero exit

  • GIVEN .espectacular/config.toml pins an older tool version than the installed ah
  • WHEN a user runs ah upgrade
  • THEN the command updates the config and exits non-zero so CI can detect the compatibility change
  • AND an up-to-date config exits zero instead
  • VERIFIES [[cli.P-upgrade]]

Requirement: Doctor enable flag

When a user runs ah doctor --enable <capability> for a detected inactive capability, the system SHALL write exactly one config table for that capability and SHALL print the path and table name written.

Scenario: Enable pytest adapter

  • GIVEN pytest is detected by ah doctor
  • WHEN a user runs ah doctor --enable pytest
  • THEN the command writes [runners.pytest] command = ["pytest"] to .espectacular/config.toml
  • AND prints .espectacular/config.toml and [runners.pytest]
  • VERIFIES [[cli.P-enable]]

Scenario: Enable cargo adapter

  • GIVEN cargo is detected by ah doctor
  • WHEN a user runs ah doctor --enable cargo
  • THEN the command writes [runners.cargo] command = ["cargo", "test"] to .espectacular/config.toml
  • AND prints .espectacular/config.toml and [runners.cargo]
  • VERIFIES [[cli.P-enable]]

Scenario: Enable vitest adapter

  • GIVEN vitest is detected by ah doctor
  • WHEN a user runs ah doctor --enable vitest
  • THEN the command writes [runners.vitest] command = ["vitest", "run"] to .espectacular/config.toml
  • AND prints .espectacular/config.toml and [runners.vitest]
  • VERIFIES [[cli.P-enable]]

Scenario: Enable mutation capability

  • GIVEN a mutation testing tool is detected
  • WHEN a user runs ah doctor --enable mutation
  • THEN the command writes [capabilities.mutation] enabled = true to .espectacular/config.toml
  • AND prints .espectacular/config.toml and [capabilities.mutation]
  • VERIFIES [[cli.P-enable]]

Scenario: Enable property capability

  • GIVEN a property-based testing framework is detected
  • WHEN a user runs ah doctor --enable property
  • THEN the command writes [capabilities.property] enabled = true to .espectacular/config.toml
  • AND prints .espectacular/config.toml and [capabilities.property]
  • VERIFIES [[cli.P-enable]]

Scenario: Enable snapshot capability

  • GIVEN a snapshot testing framework is detected
  • WHEN a user runs ah doctor --enable snapshot
  • THEN the command writes [capabilities.snapshot] enabled = true to .espectacular/config.toml
  • AND prints .espectacular/config.toml and [capabilities.snapshot]
  • VERIFIES [[cli.P-enable]]

Scenario: Enable unknown capability is an error

  • WHEN a user runs ah doctor --enable nonexistent
  • THEN the command exits non-zero
  • AND prints unrecognized capability: nonexistent
  • VERIFIES [[cli.P-enable]]

Scenario: Enable already-active capability is a no-op

  • GIVEN a capability is already present in .espectacular/config.toml
  • WHEN a user runs ah doctor --enable <capability>
  • THEN the command reports it is already enabled and makes no changes
  • VERIFIES [[cli.P-enable]]

Requirement: Explain subcommand

The system SHALL provide an ah explain <topic> subcommand that prints playbook guidance for a finding kind or suggested action.

Scenario: Explain a finding kind

  • WHEN a user runs ah explain no-toml
  • THEN the command prints markdown guidance for the no-toml finding kind
  • VERIFIES [[cli.P-explain]]

Scenario: Explain a suggested action

  • WHEN a user runs ah explain run_ah_scenario_new
  • THEN the command prints markdown guidance for the run_ah_scenario_new suggested action
  • VERIFIES [[cli.P-explain]]

Scenario: Explain a general topic

  • WHEN a user runs ah explain workflow
  • THEN the command prints markdown guidance for the general workflow topic
  • VERIFIES [[cli.P-explain]]

Scenario: Explain with JSON output

  • WHEN a user runs ah explain no-toml --json
  • THEN the command emits a JSON object with fields: topic, summary, when, do, human_approval, related_topics, hints
  • AND each hints item contains kind and message string fields
  • VERIFIES [[cli.P-explain]]

Scenario: List all topics

  • WHEN a user runs ah explain --list
  • THEN the command prints all available topic identifiers, one per line
  • VERIFIES [[cli.P-explain]]

Scenario: Unknown topic is an error

  • WHEN a user runs ah explain no-such-topic
  • THEN the command exits non-zero
  • AND prints either Run ah explain --list or the sorted list of available topic identifiers
  • VERIFIES [[cli.P-explain]]

Requirement: Coverage report command

The system SHALL provide ah report to display a conformance coverage matrix across all deployed specs and archetype tiers, modeled on the OpenTelemetry per-language compliance matrix pattern.

Scenario: Report coverage by spec and archetype

  • WHEN a user runs ah report
  • THEN the command prints a table showing each spec as a row and each archetype as a column
  • AND each cell shows covered/missing/failing counts
  • VERIFIES [[cli.P-report]]

Scenario: Report exits zero when coverage is complete

  • GIVEN every deployed scenario has a valid, passing contract
  • WHEN a user runs ah report
  • THEN the command exits zero
  • VERIFIES [[cli.P-report]]

Scenario: Report exits non-zero when scenarios are missing contracts

  • GIVEN at least one deployed scenario has no sidecar contract
  • WHEN a user runs ah report
  • THEN the command exits non-zero
  • VERIFIES [[cli.P-report]]

Scenario: Report JSON output

  • WHEN a user runs ah report --json
  • THEN the command emits a JSON conformance matrix consumable by CI dashboards and agent harnesses
  • VERIFIES [[cli.P-report]]

Requirement: Recommendation findings

The system SHALL emit recommendation findings when ah doctor detects capabilities that are available but not yet configured.

Scenario: Recommendation finding carries enable command

  • GIVEN ah doctor detects an available framework not yet configured
  • WHEN the output is inspected (JSON or text)
  • THEN a recommendation finding is present with suggested_action = enable_capability
  • AND apply_command contains the ah doctor --enable <capability> invocation
  • VERIFIES [[cli.P-recommendation]]

Scenario: Recommendation finding is a finding kind, not a log line

  • GIVEN ah doctor detects an available but unconfigured framework
  • WHEN the output is requested as JSON (ah doctor --json)
  • THEN the finding appears in the findings array with kind = recommendation
  • AND it carries a playbook_command field
  • VERIFIES [[cli.P-recommendation]]

Scenario: Configured frameworks emit no recommendation

  • GIVEN every available framework is already configured in .espectacular/config.toml
  • WHEN ah doctor --json runs
  • THEN no recommendation finding is emitted for a configured framework — a duplicate enable suggestion would be an invalid nudge
  • AND the framework is reported as a detection instead
  • VERIFIES [[cli.P-recommendation]]

Requirement: Spec Lint Command

The system SHALL provide ah lint to statically analyze OpenSpec scenario files for quality findings without modifying files or running tests.

Scenario: Lint deployed specs

  • WHEN a user runs ah lint
  • THEN the command analyzes all spec files under openspec/specs/
  • AND emits lint findings to stdout
  • AND exits zero when only warning-severity findings are present
  • VERIFIES [[cli.P-lint]]

Scenario: Lint a change overlay

  • WHEN a user runs ah lint --changes add-parser
  • THEN the command analyzes deployed specs plus the add-parser change spec overlay
  • VERIFIES [[cli.P-lint]]

Scenario: Lint a single check category

  • WHEN a user runs ah lint --check vague-qualifier
  • THEN the command runs only the vague-qualifier check and skips all others
  • VERIFIES [[cli.P-lint]]

Scenario: Lint JSON output

  • WHEN a user runs ah lint --json
  • THEN the command emits a JSON object in the same schema as ah check --json
  • AND each finding includes kind, severity, spec_path, scenario_id, message, and suggestion
  • VERIFIES [[cli.P-lint]]

Scenario: Clean spec exits zero with empty findings

  • GIVEN all analyzed spec files pass all lint checks
  • WHEN a user runs ah lint
  • THEN the command exits zero
  • AND emits no findings
  • VERIFIES [[cli.P-lint]]

Scenario: Reject an unknown check category

  • WHEN a user runs ah lint --check no-such-check
  • THEN the command fails with a non-zero exit and lists the valid check kinds
  • AND no spec files are analyzed
  • VERIFIES [[cli.P-lint]]

id: adapters kind: intent statement: “WHEN contracts declare test commands THE adapter layer SHALL detect frameworks through a configured-manifest-environment-source precedence chain, dispatch per test type via bundled pytest, cargo, and vitest adapters normalizing output into the finding schema, emit missing-adapter findings when nothing matches, and wire custom runners through a strict JSON envelope.”


adapters Specification

Constraints

idkindexprtraces_to
C-detection-precedenceinvariantadapter detection runs through the precedence chain configured > manifest > environment > source_import before any adapter invocation, with manifest evidence winning per matching test type (cross-language evidence is not a conflict), and the selection reported by ah doctor --json / ah check --json as adapter, test_type, and detection_source ∈ {manifest, environment, source_import, configured}[[adapters]]
C-pytest-adapterinvariantthe bundled pytest adapter detects pytest via pyproject.toml (reporting the detected version), runs the declared command, and normalizes output: zero exit emits no test-failing finding; non-zero emits test-failing with bounded stdout/stderr tails; JSON ImportError, missing-fixture, and collection failures remain test-failing with execution context test.type = pytest-import-error, pytest-fixture-error, pytest-collection-error[[adapters]]
C-cargo-adapterinvariantthe bundled cargo adapter detects cargo via Cargo.toml, runs the declared command, and normalizes output: zero exit emits no test-failing finding; non-zero emits test-failing with bounded stdout/stderr tails[[adapters]]
C-vitest-adapterinvariantthe bundled vitest adapter detects vitest via package.json dependencies (reporting the detected version), runs the declared command, and normalizes output like the cargo adapter[[adapters]]
C-missing-adapterinvarianta contract that declares a test command with no adapter configured or detected for the test type emits a missing-adapter finding carrying kind, message, suggested_action, and playbook_command, directing the user to ah doctor, and is distinct from no-tests-declared[[adapters]]
C-custom-runner-envelopeinvariant[runners.custom.<name>] config blocks wire arbitrary shell commands via the JSON envelope of schemas/custom-runner.schema.json (findings conform to schemas/check-output.schema.json): the envelope requires exit_code (integer), passed (boolean), findings (array); empty findings with zero exit is a pass; envelope failure (passed = false or non-empty findings) overrides process success; process failure overrides envelope success; non-zero exit without a valid envelope emits test-failing with raw tails; no custom runner is invoked without explicit config[[adapters]]

Model

States

  • detecting
  • running
  • normalizing
  • reported

Transitions

idfromtoguard
t-detectdetectingrunning[[adapters.C-detection-precedence]]
t-runrunningnormalizinga bundled adapter ([[adapters.C-pytest-adapter]], [[adapters.C-cargo-adapter]], or [[adapters.C-vitest-adapter]]) or [[adapters.C-custom-runner-envelope]]
t-normalizenormalizingreportedbundled-adapter normalization or envelope processing rules
t-missingdetectingreported[[adapters.C-missing-adapter]]
t-no-runnerdetectingdetecting[[adapters.C-custom-runner-envelope]] (no invocation without explicit config)

Properties

idkindderives_fromgeneratorpredicate
P-precedenceunit[[adapters.C-detection-precedence]]projects with configured, manifest-declared, environment-installed, and source-imported frameworks in overlapping combinationsthe strongest surviving signal per test type wins; cross-language evidence never conflicts; reports carry adapter/test_type/detection_source
P-pytestunit[[adapters.C-pytest-adapter]]pytest runs exiting zero and non-zero, plus JSON outputs with import, fixture, and collection errorspass normalization and test-failing classification with bounded tails and correct test.type context
P-cargounit[[adapters.C-cargo-adapter]]cargo test runs exiting zero and non-zeropass normalization and test-failing with bounded tails
P-vitestunit[[adapters.C-vitest-adapter]]vitest runs exiting zero and non-zeropass normalization and test-failing with bounded tails
P-missing-adapterunit[[adapters.C-missing-adapter]]a contract declaring a test with no adapter anywheremissing-adapter finding with all four fields, distinct from no-tests-declared
P-envelopeunit[[adapters.C-custom-runner-envelope]]runners spanning the five envelope outcomeseach outcome maps to the mandated emission; no invocation occurs without config

Purpose

TBD - created by archiving change add-quality-measurement-and-adapters. Update Purpose after archive.

Non-Goals

  • Bundling adapters beyond the shipped pytest, cargo test, and vitest set — the custom runner plugin protocol is the extension path for everything else
  • Owning test-framework behavior, versions, or output formats — upstream tool concerns the adapters only normalize
  • Measuring test quality (mutation, coverage, flakiness) — that is the gate spec’s quality-measurement domain

Requirements

Requirement: Adapter detection precedence

The system SHALL detect framework availability through a defined precedence chain before invoking any adapter, SHALL dispatch adapters per declared contract test type rather than selecting one global adapter for the whole repository, and SHALL record the selected detection source in doctor and check output.

Scenario: Explicit config takes precedence

  • GIVEN .espectacular/config.toml selects an adapter for a declared contract test type
  • WHEN adapter detection runs
  • THEN the configured adapter is treated as the strongest signal
  • AND the command reports detection_source = configured
  • VERIFIES [[adapters.P-precedence]]

Scenario: Manifest declaration is second

  • GIVEN no explicit adapter config exists
  • AND a project declares a test framework in its language manifest (e.g., pyproject.toml, Cargo.toml, package.json)
  • WHEN adapter detection runs
  • THEN the manifest declaration overrides environment and source signals
  • VERIFIES [[adapters.P-precedence]]

Scenario: Environment detection is third

  • GIVEN a framework is not declared in a manifest but is installed in the environment
  • WHEN adapter detection runs
  • THEN the environment presence is used to confirm availability
  • VERIFIES [[adapters.P-precedence]]

Scenario: Source import is weakest signal

  • GIVEN a framework is not configured, not in the manifest, and not in the environment, but is imported in a test file
  • WHEN adapter detection runs
  • THEN the source import is recognized as the weakest positive signal
  • VERIFIES [[adapters.P-precedence]]

Scenario: Manifest signal wins for the matching adapter

  • GIVEN a project manifest declares pytest as the Python test framework
  • AND the environment also has vitest installed
  • WHEN adapter detection runs for a Python contract test type
  • THEN pytest is selected for that Python test invocation because manifest takes precedence over environment
  • AND the vitest environment presence remains available for TypeScript test invocations and is not treated as a conflict
  • VERIFIES [[adapters.P-precedence]]

Scenario: Detection source is reported

  • GIVEN adapter detection selects a framework through manifest, environment, or source-import evidence
  • WHEN ah doctor --json or ah check --json reports the selected adapter
  • THEN the report includes adapter, test_type, and detection_source
  • AND detection_source is one of manifest, environment, source_import, or configured
  • VERIFIES [[adapters.P-precedence]]

Scenario: No detection signal fails to select an adapter

  • GIVEN a project with no configured adapter, no manifest declaration, no installed framework, and no test-file imports
  • WHEN adapter detection runs
  • THEN no adapter is selected and no detection source is fabricated
  • AND the contract test falls to the missing-adapter path instead of invoking a guessed runner
  • VERIFIES [[adapters.P-precedence]]

Requirement: Python pytest adapter

The system SHALL provide a bundled pytest adapter that detects pytest, runs the declared test command, and normalizes output into the finding schema.

Scenario: Pytest adapter detects via pyproject.toml

  • GIVEN a project contains pyproject.toml with pytest in [tool.pytest.ini_options] or as a dependency
  • WHEN the pytest adapter runs detection
  • THEN it reports pytest as available with the detected version
  • VERIFIES [[adapters.P-pytest]]

Scenario: Pytest adapter normalizes zero exit to pass

  • GIVEN a contract declares a pytest test command
  • WHEN the adapter runs the command and pytest exits zero
  • THEN the adapter emits no test-failing finding for that contract
  • VERIFIES [[adapters.P-pytest]]

Scenario: Pytest adapter normalizes non-zero exit to test-failing

  • GIVEN a contract declares a pytest test command
  • WHEN the adapter runs the command and pytest exits non-zero
  • THEN the adapter emits a test-failing finding with bounded stdout/stderr tails
  • VERIFIES [[adapters.P-pytest]]

Scenario: Pytest adapter classifies import errors

  • GIVEN pytest emits JSON output containing an ImportError
  • WHEN the adapter normalizes the failing result
  • THEN the finding remains test-failing
  • AND the execution context reports test.type = pytest-import-error
  • VERIFIES [[adapters.P-pytest]]

Scenario: Pytest adapter classifies fixture failures

  • GIVEN pytest emits JSON output containing a missing fixture failure
  • WHEN the adapter normalizes the failing result
  • THEN the finding remains test-failing
  • AND the execution context reports test.type = pytest-fixture-error
  • VERIFIES [[adapters.P-pytest]]

Scenario: Pytest adapter classifies collection failures

  • GIVEN pytest emits JSON output containing a collection error
  • WHEN the adapter normalizes the failing result
  • THEN the finding remains test-failing
  • AND the execution context reports test.type = pytest-collection-error
  • VERIFIES [[adapters.P-pytest]]

Requirement: Rust cargo test adapter

The system SHALL provide a bundled cargo test adapter that detects cargo, runs the declared test command, and normalizes output into the finding schema.

Scenario: Cargo adapter detects via Cargo.toml

  • GIVEN a project contains Cargo.toml
  • WHEN the cargo adapter runs detection
  • THEN it reports cargo test as available
  • VERIFIES [[adapters.P-cargo]]

Scenario: Cargo adapter normalizes zero exit to pass

  • GIVEN a contract declares a cargo test command
  • WHEN the adapter runs the command and cargo exits zero
  • THEN the adapter emits no test-failing finding for that contract
  • VERIFIES [[adapters.P-cargo]]

Scenario: Cargo adapter normalizes non-zero exit to test-failing

  • GIVEN a contract declares a cargo test command
  • WHEN the adapter runs the command and cargo exits non-zero
  • THEN the adapter emits a test-failing finding with bounded stdout/stderr tails
  • VERIFIES [[adapters.P-cargo]]

Requirement: TypeScript vitest adapter

The system SHALL provide a bundled vitest adapter that detects vitest, runs the declared test command, and normalizes output into the finding schema.

Scenario: Vitest adapter detects via package.json

  • GIVEN a project contains package.json with vitest in dependencies or devDependencies
  • WHEN the vitest adapter runs detection
  • THEN it reports vitest as available with the detected version
  • VERIFIES [[adapters.P-vitest]]

Scenario: Vitest adapter normalizes zero exit to pass

  • GIVEN a contract declares a vitest test command
  • WHEN the adapter runs the command and vitest exits zero
  • THEN the adapter emits no test-failing finding for that contract
  • VERIFIES [[adapters.P-vitest]]

Scenario: Vitest adapter normalizes non-zero exit to test-failing

  • GIVEN a contract declares a vitest test command
  • WHEN the adapter runs the command and vitest exits non-zero
  • THEN the adapter emits a test-failing finding with bounded stdout/stderr tails
  • VERIFIES [[adapters.P-vitest]]

Requirement: No-adapter-configured path

When a contract declares a test command but no adapter is configured or detected for the declared test type, the system SHALL emit a missing-adapter finding with kind, message, suggested_action, and playbook_command fields.

Scenario: Missing adapter emits missing-adapter finding

  • GIVEN a contract declares a test command
  • AND no adapter is configured in .espectacular/config.toml for the declared test type
  • AND adapter detection finds no matching framework
  • WHEN ah check runs
  • THEN a missing-adapter finding is emitted with a message directing the user to run ah doctor
  • AND the finding is distinct from no-tests-declared because the contract did declare a test
  • VERIFIES [[adapters.P-missing-adapter]]

Scenario: Configured adapter suppresses missing-adapter

  • GIVEN a contract declares a test command
  • AND an adapter is configured in .espectacular/config.toml for the declared test type
  • WHEN ah check runs
  • THEN no missing-adapter finding is emitted for that contract
  • AND the run fails only when the declared test itself fails
  • VERIFIES [[adapters.P-precedence]]

Requirement: Custom runner plugin protocol

The system SHALL support [runners.custom.<name>] config blocks that wire arbitrary shell commands into the adapter layer via a documented JSON envelope defined in schemas/custom-runner.schema.json.

Note: The envelope schema (schemas/custom-runner.schema.json) specifies the top-level structure the shell command must emit. Individual findings within the findings array conform to the full finding schema (schemas/check-output.schema.json), not to the envelope schema.

Scenario: Custom runner emits required envelope fields

  • GIVEN a custom runner is configured and invoked
  • WHEN the runner’s stdout is parsed
  • THEN the envelope contains at minimum: exit_code (integer), passed (boolean), findings (array)
  • AND each finding in the array conforms to the full finding schema
  • VERIFIES [[adapters.P-envelope]]

Scenario: Empty findings array with zero exit is a pass

  • GIVEN a custom runner exits zero
  • AND the envelope findings array is empty
  • WHEN the adapter processes the result
  • THEN no test-failing finding is emitted for that contract
  • VERIFIES [[adapters.P-envelope]]

Scenario: Envelope failure overrides process success

  • GIVEN a custom runner exits zero
  • AND stdout contains a valid envelope with passed = false or non-empty findings
  • WHEN the adapter processes the result
  • THEN the adapter emits the envelope findings
  • AND treats the contract as not passing
  • VERIFIES [[adapters.P-envelope]]

Scenario: Process failure overrides envelope success

  • GIVEN a custom runner exits non-zero
  • AND stdout contains a valid envelope with passed = true and an empty findings array
  • WHEN the adapter processes the result
  • THEN a test-failing finding is emitted with the raw stdout/stderr tails
  • AND the process exit code is preserved in the finding
  • VERIFIES [[adapters.P-envelope]]

Scenario: Custom runner non-zero exit without valid envelope is an error finding

  • GIVEN a custom runner exits non-zero
  • AND stdout is not a valid envelope
  • WHEN the adapter processes the result
  • THEN a test-failing finding is emitted with the raw stdout/stderr tails
  • VERIFIES [[adapters.P-envelope]]

Scenario: Custom runner is not invoked without explicit config

  • GIVEN no [runners.custom.<name>] block exists in config
  • WHEN ah check runs
  • THEN no custom runner is invoked
  • VERIFIES [[adapters.P-envelope]]

id: explain kind: intent statement: “WHEN a user runs ah explain THE playbook SHALL serve compile-enforced topics for every FindingKind, SuggestedAction, quality finding kind, and compiled-in adapter capability, with stable sorted listing, machine-readable JSON, and non-zero exit for unknown topics.”


explain Specification

Constraints

idkindexprtraces_to
C-compile-enforcedinvariantthe build fails if any FindingKind or SuggestedAction enum variant lacks a playbook topic body, and succeeds enumerating all topics when complete[[explain]]
C-topic-coverageinvariantah explain provides topics for every FindingKind value, every SuggestedAction value, and a set of general topics[[explain]]
C-json-schemainvariantah explain <topic> --json emits a valid JSON object with topic, summary, when, do (string array), human_approval (boolean), related_topics (string array), and hints (objects with kind and message), for every valid topic[[explain]]
C-listing-stableinvariantah explain --list prints all topic identifiers one per line, sorted alphabetically, identical across runs[[explain]]
C-unknown-topicinvariantan unknown topic exits non-zero with either the sorted topic list or a pointer to ah explain --list[[explain]]
C-quality-kindsinvarianttopics exist for the quality finding kinds quality-mutation, quality-property, quality-snapshot, and are subject to compile enforcement like all FindingKind values[[explain]]
C-adapter-topicsinvariantprogressive-enablement capability topics are included when their adapter modules are compiled in; two adapter modules registering the same topic identifier fail the build[[explain]]

Model

States

  • compiling
  • serving
  • errored

Transitions

idfromtoguard
t-enforcecompilingcompiling[[explain.C-compile-enforced]] AND [[explain.C-adapter-topics]] (duplicate registration)
t-servecompilingserving[[explain.C-topic-coverage]] AND [[explain.C-quality-kinds]]
t-jsonservingserving[[explain.C-json-schema]]
t-listservingserving[[explain.C-listing-stable]]
t-unknownservingerrored[[explain.C-unknown-topic]]

Properties

idkindderives_fromgeneratorpredicate
P-enforcedunit[[explain.C-compile-enforced]]a build with one variant stripped of its topic body vs the complete setthe stripped build fails naming the missing topic; the complete build succeeds and --list enumerates every variant
P-coverageunit[[explain.C-topic-coverage]]every FindingKind and SuggestedAction value plus a general topiceach resolves via ah explain <topic> printing guidance and exiting zero
P-jsonunit[[explain.C-json-schema]]every valid topic with --jsonoutput is a valid object with exactly the required fields and typed hints entries
P-listingunit[[explain.C-listing-stable]]two successive --list runsoutputs are byte-identical and alphabetically sorted
P-unknownunit[[explain.C-unknown-topic]]ah explain no-such-topicexits non-zero with the topic list or the --list pointer
P-qualityunit[[explain.C-quality-kinds]]ah explain quality-mutation, quality-property, quality-snapshoteach prints kind-specific guidance (mutation meaning/enablement) and exits zero
P-adapterunit[[explain.C-adapter-topics]]a build with the pytest adapter compiled in vs two modules registering one topic idthe first serves ah explain pytest enablement guidance; the second fails the build naming the conflict

Purpose

TBD - created by archiving change add-quality-measurement-and-adapters. Update Purpose after archive.

Non-Goals

  • Serving playbook content from runtime or external files — topics are embedded in the binary at compile time
  • Authoring playbooks for upstream tools’ own findings (genesis, specodelic) — only espectacular’s kinds and actions
  • Localizing or theming playbook text — English-only output

Requirements

Requirement: Playbook is compile-enforced

The system SHALL fail to build if any FindingKind or SuggestedAction enum variant lacks a corresponding ah explain topic body.

Scenario: Missing topic body is a compile error

  • GIVEN a FindingKind or SuggestedAction variant has no associated playbook body
  • WHEN the project is built with cargo build
  • THEN the build fails with an error identifying the missing topic
  • VERIFIES [[explain.P-enforced]]

Scenario: All variants have topics at build time

  • GIVEN all enum variants have associated playbook bodies
  • WHEN the project is built
  • THEN the build succeeds and ah explain --list enumerates them all
  • VERIFIES [[explain.P-coverage]]

Requirement: Topic coverage

The system SHALL provide ah explain topics for every FindingKind value, every SuggestedAction value, and a set of general topics.

Scenario: Finding kind topic exists

  • WHEN a user runs ah explain no-toml
  • THEN the command prints guidance for the no-toml finding kind and exits zero
  • VERIFIES [[explain.P-coverage]]

Scenario: Suggested action topic exists

  • WHEN a user runs ah explain run_ah_scenario_new
  • THEN the command prints guidance for the run_ah_scenario_new action and exits zero
  • VERIFIES [[explain.P-coverage]]

Scenario: General topic exists

  • WHEN a user runs ah explain workflow
  • THEN the command prints general workflow guidance and exits zero
  • VERIFIES [[explain.P-coverage]]

Scenario: Topic with no kind or action is refused

  • WHEN a user runs ah explain with a topic identifier that corresponds to no FindingKind, SuggestedAction, or general topic
  • THEN the command exits non-zero and prints no guidance for it
  • VERIFIES [[explain.P-unknown]]

Requirement: Structured JSON output

The system SHALL support --json output for ah explain that emits a machine-readable object.

Scenario: JSON output has required fields

  • WHEN a user runs ah explain no-toml --json
  • THEN the output is a valid JSON object containing: topic (string), summary (string), when (string), do (array of strings), human_approval (boolean), related_topics (array of strings), hints (array of objects)
  • AND each hints item contains kind (string) and message (string)
  • VERIFIES [[explain.P-json]]

Scenario: JSON output is valid for every topic

  • GIVEN any valid topic identifier
  • WHEN ah explain <topic> --json is run
  • THEN the output passes JSON schema validation
  • VERIFIES [[explain.P-json]]

Scenario: JSON output is not emitted for an unknown topic

  • WHEN a user runs ah explain no-such-topic --json
  • THEN the command exits non-zero and emits a plain-text error, never a malformed JSON object
  • VERIFIES [[explain.P-unknown]]

Requirement: Topic listing

The system SHALL enumerate all available topics on demand.

Scenario: List enumerates all topics

  • WHEN a user runs ah explain --list
  • THEN the command prints all topic identifiers, one per line, and exits zero
  • VERIFIES [[explain.P-listing]]

Scenario: List is stable across runs

  • WHEN ah explain --list is run twice in succession
  • THEN the output is identical (topics are sorted alphabetically)
  • VERIFIES [[explain.P-listing]]

Scenario: Invalid extra argument does not corrupt the listing

  • WHEN ah explain --list is run with a stray positional argument
  • THEN the command still prints the full sorted topic list and exits zero
  • VERIFIES [[explain.P-listing]]

Requirement: Unknown topic handling

When a user requests an unknown ah explain topic, the system SHALL exit non-zero and print either Run ah explain --list or the sorted list of available topic identifiers.

Scenario: Unknown topic exits non-zero

  • WHEN a user runs ah explain no-such-topic
  • THEN the command exits non-zero
  • AND the error message lists available topics or directs the user to ah explain --list
  • VERIFIES [[explain.P-unknown]]

Requirement: Quality finding kind topics

The system SHALL provide ah explain topics for every quality finding kind introduced by this change: quality-mutation, quality-property, quality-snapshot. These are FindingKind values and are therefore subject to the compile-enforcement requirement.

Scenario: quality-mutation topic exists

  • WHEN a user runs ah explain quality-mutation
  • THEN the command prints guidance explaining what the mutation score means, how to enable mutation testing, and when the finding appears
  • VERIFIES [[explain.P-quality]]

Scenario: quality-property topic exists

  • WHEN a user runs ah explain quality-property
  • THEN the command prints guidance for the quality-property finding kind and exits zero
  • VERIFIES [[explain.P-quality]]

Scenario: quality-snapshot topic exists

  • WHEN a user runs ah explain quality-snapshot
  • THEN the command prints guidance for the quality-snapshot finding kind and exits zero
  • VERIFIES [[explain.P-quality]]

Scenario: Unknown quality topic is refused

  • WHEN a user runs ah explain quality-nonexistent
  • THEN the command exits non-zero and prints no quality guidance
  • VERIFIES [[explain.P-unknown]]

Requirement: Adapter topics ship with adapters

The system SHALL include ah explain topics for progressive-enablement capabilities when their adapter modules are compiled in.

Scenario: Pytest adapter contributes topic

  • GIVEN the pytest adapter is compiled into the binary
  • WHEN a user runs ah explain pytest
  • THEN the command prints guidance for enabling and using the pytest adapter
  • VERIFIES [[explain.P-adapter]]

Scenario: Duplicate topic registration is a compile error

  • GIVEN two adapter modules attempt to register the same topic identifier
  • WHEN the project is built
  • THEN the build fails identifying the conflicting topic name
  • VERIFIES [[explain.P-enforced]]

id: cli.core kind: intent statement: “WHEN the tool adopts genesis::guide or a user runs ah sync THE CLI SHALL build the scaffold via Guide::builder, return Output or use ErrorSink with errors surfaced via suggestion footers, create or refresh property-derived contracts for the current scope, exit non-zero when spk lint fails or spk is unavailable, and support a check mode that writes nothing and fails on drift.”


cli-core spec delta: adopt genesis::guide

Constraints

idkindexprtraces_to
C-guide-builderinvariantthe tool’s main.rs CLI setup uses genesis::guide::Guide::builder() when the tool adopts genesis::guide (adoption is the maintainer’s decision — this spec fixes the contract, not the timing), with cargo test passing[[cli.core]]
C-output-handlersadvisorycommand handlers SHOULD return Output<T> or use ErrorSink[[cli.core]]
C-errorsink-selfhealadvisoryon a command error, ErrorSink SHOULD print the error with a suggestion footer and write to the error scratch (for --from-last-error)[[cli.core]]
C-sync-refusalinvariantah sync exits non-zero without writing when spk lint fails on any target file or when the spk binary is missing, reporting spk-unavailable for the latter[[cli.core]]
C-sync-checkinvariantah sync –check writes nothing, exits zero when all derived contracts exist and are fresh, and exits non-zero on drift or missing contracts[[cli.core]]
C-sync-registrationinvariantah sync is registered in the Command enum with help text and completions like every other subcommand[[cli.core]]
C-sync-scopeadvisorywithout an explicit scope, ah sync operates on the same default scope as ah check[[cli.core]]

Model

States

  • ad-hoc-cli
  • guide-built
  • linting
  • parsing
  • written
  • checked

Transitions

idfromtoguard
t-buildad-hoc-cliguide-built[[cli.core.C-guide-builder]]
t-refuselintinglinting[[cli.core.C-sync-refusal]]
t-parselintingparsing[[cli.core.C-sync-refusal]]
t-writeparsingwritten[[cli.core.C-sync-registration]]
t-checkparsingchecked[[cli.core.C-sync-check]]

Properties

idkindderives_fromgeneratorpredicate
P-guide-builderunit[[cli.core.C-guide-builder]]the adopted main.rsCLI setup is constructed via Guide::builder() and tests pass
P-handlersunit[[cli.core.C-output-handlers]]each command handlerhandlers return Output<T> or use ErrorSink where the advisory is honored
P-selfhealunit[[cli.core.C-errorsink-selfheal]]a failing command runthe error prints with a suggestion footer and lands in the error scratch readable by --from-last-error
P-refuseunit[[cli.core.C-sync-refusal]]a scope containing a lint-dirty dual-format file, and a scope with no spk binary on PATHsync exits non-zero with a reason in both cases and writes nothing
P-checkunit[[cli.core.C-sync-check]]a scope with fresh contracts, drifted contracts, and missing contractscheck mode exits zero, non-zero, and non-zero respectively; no file is modified in any case
P-registerunit[[cli.core.C-sync-registration]]ah –help and shell completion generationsync appears with help text and completes like other subcommands
P-scopeunit[[cli.core.C-sync-scope]]sync invoked with and without an explicit scopethe default scope matches ah check’s default scope

Purpose

Adopt genesis::guide for the CLI scaffold: Guide::builder() replaces ad-hoc setup, handlers standardize on Output<T>/ErrorSink, and errors become self-healing via suggestion footers and the error scratch.

Give adopters a single command that turns lint-clean Properties rows into gate-readable contracts, safely refreshable in CI via --check.

Non-Goals

  • Changing ah check scope resolution, findings, or exit-code semantics — owned by the gate spec
  • Defining contract file format or derived_from semantics — owned by the gate spec’s Contract Schema and Property-Derived Contracts requirements
  • Authoring or repairing spec text: ah sync derives contracts from existing lint-clean rows, it never edits the specs themselves
  • Vendoring or wrapping spk behavior: lint rules and their semantics live upstream in specodelic

Requirements

Requirement: CLI scaffold uses Guide

The tool’s main.rs CLI setup SHALL use genesis::guide::Guide::builder() when the tool adopts genesis::guide. Adoption is determined by the tool’s maintainer — this spec defines the contract for how to do it.

Scenario: Guide builder replaces ad-hoc setup

  • GIVEN the tool adopts genesis::guide
  • WHEN main.rs is updated to use Guide::builder(...)
  • THEN command handlers SHOULD return Output<T> or use ErrorSink
  • AND cargo test SHALL pass
  • VERIFIES [[cli.core.P-guide-builder]]

Scenario: ErrorSink for self-healing errors

  • GIVEN the tool adopts genesis::guide
  • WHEN a command returns an error
  • THEN ErrorSink SHOULD print the error with a suggestion footer
  • AND it SHOULD write to the error scratch (for --from-last-error)
  • VERIFIES [[cli.core.P-handlers]]
  • VERIFIES [[cli.core.P-selfheal]]

Requirement: Sync Command

ah sync SHALL create or refresh property-derived contracts for the current scope, SHALL exit non-zero without writing when spk lint fails on any target file or when spk is unavailable, and SHALL support --check, which writes nothing and exits non-zero on drift or missing contracts.

Scenario: Sync creates derived contracts

  • GIVEN the default scope contains a lint-clean dual-format spec with Properties rows
  • WHEN ah sync runs
  • THEN a derived contract exists per property row with derived_from recorded
  • VERIFIES [[cli.core.P-scope]]

Scenario: Sync is registered like every subcommand

  • GIVEN the tool’s CLI is built
  • WHEN a user runs ah --help or generates shell completions
  • THEN sync appears with help text and completes like the other subcommands
  • VERIFIES [[cli.core.P-register]]

Scenario: Sync refuses on lint failure

  • GIVEN the scope contains a dual-format spec that fails spk lint
  • WHEN ah sync runs
  • THEN it exits non-zero, names the offending file, and writes nothing
  • VERIFIES [[cli.core.P-refuse]]

Scenario: Missing spk reports spk-unavailable

  • GIVEN spk is not on PATH
  • WHEN ah sync runs
  • THEN it exits non-zero with an spk-unavailable reason
  • VERIFIES [[cli.core.P-refuse]]

Scenario: Check mode is CI-safe

  • GIVEN a scope with one drifted and one missing derived contract
  • WHEN ah sync --check runs
  • THEN it exits non-zero, reports both, and modifies no files
  • VERIFIES [[cli.core.P-check]]

id: config kind: intent statement: “WHEN the tool adopts genesis::config THE tool SHALL implement ConfigFile, delegate all config file I/O to genesis, register its config with ConfigRegistry at startup, and remove dead config code without new clippy warnings.”


config spec delta: adopt genesis::config

Constraints

idkindexprtraces_to
C-configfile-implinvariantthe tool’s config struct implements genesis::config::ConfigFile and all config file I/O (read, write, parse) delegates to genesis, with cargo test passing[[config]]
C-startup-registrationinvariantat startup the tool registers its config struct with ConfigRegistry[[config]]
C-configstore-advisoryadvisoryconfig discovery and validation SHOULD use ConfigStore[[config]]
C-dead-code-removedinvariantafter adoption the old config parsing code is removed, cargo clippy introduces no new warnings, and cargo test passes[[config]]

Model

States

  • own-config
  • genesis-config
  • registered

Transitions

idfromtoguard
t-adoptown-configgenesis-config[[config.C-configfile-impl]]
t-registergenesis-configregistered[[config.C-startup-registration]]
t-pruneregisteredregistered[[config.C-dead-code-removed]]

Properties

idkindderives_fromgeneratorpredicate
P-configfileunit[[config.C-configfile-impl]]the adopted config structit implements ConfigFile; every read, write, and parse call routes through genesis; tests pass
P-registrationunit[[config.C-startup-registration]]a startup runthe config struct is registered with ConfigRegistry before config is first read
P-configstoreunit[[config.C-configstore-advisory]]config discovery and validation pathsthey use ConfigStore where the advisory is honored
P-pruneunit[[config.C-dead-code-removed]]the post-adoption treeno old config parsing code remains; cargo clippy adds no new warnings and cargo test passes

Purpose

Adopt genesis::config as the shared config substrate: the tool’s config implements ConfigFile, all file I/O delegates to genesis, startup registers with ConfigRegistry, and dead local config code is removed cleanly.

Non-Goals

  • Defining the tool’s config schema or default values — each tool owns its own config content
  • genesis::config internals — the substrate’s behavior is an upstream concern
  • Remote or secret-management config sources — local file I/O only

Requirements

Requirement: Shared config management

The tool SHALL adopt genesis::config for shared config management.

Scenario: config struct implements ConfigFile

  • GIVEN the tool needs config management
  • WHEN the tool adopts genesis::config
  • THEN the tool’s config SHALL implement genesis::config::ConfigFile
  • AND all config file I/O (read, write, parse) SHALL delegate to genesis
  • AND cargo test SHALL pass
  • VERIFIES [[config.P-configfile]]
  • VERIFIES [[config.P-configstore]]

Scenario: config registered at startup

  • GIVEN the tool starts up
  • WHEN it initializes
  • THEN it SHALL register its config struct with ConfigRegistry
  • AND it SHOULD use ConfigStore for config discovery and validation
  • VERIFIES [[config.P-registration]]

Scenario: dead config code removed

  • GIVEN the tool has adopted genesis::config
  • WHEN the old config parsing code is removed
  • THEN cargo clippy SHALL introduce no new warnings
  • AND cargo test SHALL pass
  • VERIFIES [[config.P-prune]]

Scenario: Config I/O bypassing genesis is rejected

  • GIVEN the tool has adopted genesis::config
  • WHEN a config read, write, or parse call routes anywhere other than genesis
  • THEN the delegation property is violated and the change is rejected
  • AND cargo test SHALL fail
  • VERIFIES [[config.P-configfile]]

id: lint kind: intent statement: “WHEN ah lint inspects specification files THE linter SHALL flag vague qualifiers, imperative UI steps, conjunctive step bloat, missing negative scenarios, missing non-goals, unresolved ambiguities, and entangled presentation concerns, emit findings in the shared check finding schema with warning severity by default, bridge specodelic findings for dual-format files without reimplementing specodelic rules, never skip a kind-intent file silently, relay spk graph typing violations and dangling references as spk.graph.* warnings, and report scenario-to-property trace gaps as advisory findings that never affect the exit code.”


lint Specification

Constraints

idkindexprtraces_to
C-vague-qualifierinvariantrequirement bodies and scenario steps containing unbound qualitative terms without an adjacent numeric measurement emit a vague-qualifier finding suggesting a measurable condition; a qualifier with an adjacent numeric bound emits none[[lint]]
C-imperative-stepinvariantWHEN/THEN steps describing UI mechanics (explicit click, URL navigation) emit an imperative-step finding suggesting business-action phrasing; declarative business steps emit none[[lint]]
C-conjunctive-bloatinvariantscenarios chaining more than the configured maximum of AND-linked steps emit a conjunctive-bloat finding suggesting a split; a scenario at exactly the limit is accepted; the maximum is configurable via [lint] max_and_steps in .espectacular/config.toml[[lint]]
C-missing-negativeinvarianta requirement with no scenario exercising an error, rejection, or boundary condition emits a missing-negative-scenario finding suggesting a failure-mode scenario; one negative scenario suppresses it[[lint]]
C-missing-non-goalsinvarianta spec capability file lacking a Non-Goals/Non Goals/non-goals/Out of Scope heading emits a missing-non-goals finding for that file[[lint]]
C-unresolved-ambiguityinvariantrequirement or scenario text containing a bracketed NEEDS CLARIFICATION marker emits an unresolved-ambiguity finding including the marker as context; text without the marker emits none[[lint]]
C-entangled-specinvarianta scenario with contract archetype PF or SA whose text references presentation-layer primitives (buttons, modals, colors, CSS selectors, component names, client routes) emits an entangled-spec finding suggesting the domain event or state; the same text in a BP scenario and domain-legitimate lookalike terms are not flagged[[lint]]
C-finding-schemainvariantlint findings use the same stable JSON envelope as ah check: each carries kind, severity, spec_path, message, suggested_action, playbook_command, plus scenario_id/scenario_title when scenario-specific; findings default to severity = "warning"; a malformed spec file emits a severity = "error" finding and exits non-zero[[lint]]
C-dual-bridgeinvariantfor dual-format files (id: spec frontmatter) ah lint invokes spk lint and relays each specodelic finding as kind = "spk.<rule_id>" with severity = "warning"; plain openspec files cause no specodelic invocation and identical pre-bridge output; a missing spk binary emits one advisory spk-unavailable finding with unchanged exit code; a non-envelope spk failure emits one advisory finding and linting continues[[lint]]
C-no-silent-skipinvarianta kind-intent file whose frontmatter id does not match its filename stem emits an spk-frontmatter-mismatch finding instead of being skipped silently[[lint]]
C-graph-relayinvariantspk graph typing violations and dangling references for dual-format files are relayed as spk.graph.* warning findings[[lint]]
C-trace-advisoryadvisoryverifies-dangling, scenario-unlinked, and property-untraced are reported at advisory severity and never affect the lint exit code[[lint]]
C-plain-untouchedadvisoryplain openspec files never trigger an spk invocation and produce no specodelic findings[[lint]]

Model

States

  • scanning
  • bridging
  • reported
  • relaying
  • tracing

Transitions

idfromtoguard
t-textual-rulesscanningscanning[[lint.C-vague-qualifier]], [[lint.C-imperative-step]], [[lint.C-conjunctive-bloat]], [[lint.C-missing-negative]], [[lint.C-missing-non-goals]], [[lint.C-unresolved-ambiguity]], or [[lint.C-entangled-spec]]
t-bridgescanningbridging[[lint.C-dual-bridge]] (dual-format file present)
t-inertscanningscanning[[lint.C-dual-bridge]] (plain file — no invocation)
t-schemabridgingreported[[lint.C-finding-schema]]
t-degradebridgingreported[[lint.C-dual-bridge]] (missing binary or failed invocation — advisory only)
t-relaybridgingrelaying[[lint.C-no-silent-skip]]
t-graphrelayingtracing[[lint.C-graph-relay]]
t-tracetracingreported[[lint.C-graph-relay]]
t-plainrelayingreported[[lint.C-no-silent-skip]]

Properties

idkindderives_fromgeneratorpredicate
P-vagueunit[[lint.C-vague-qualifier]]bodies with unbound qualifiers vs numerically bounded qualifiers, in requirements and THEN stepsfindings exactly in the unbound cases, with measurable-condition suggestions
P-imperativeunit[[lint.C-imperative-step]]WHEN steps with click/URL mechanics vs declarative business stepsfindings exactly in the mechanical cases, with business-action suggestions
P-bloatunit[[lint.C-conjunctive-bloat]]scenarios at limit-1, limit, and limit+1 AND-steps under default and custom maximumsfinding only above the configured limit; exact limit accepted
P-negativeunit[[lint.C-missing-negative]]requirements with only happy-path scenarios vs one negative scenariofinding exactly in the happy-path-only case
P-non-goalsunit[[lint.C-missing-non-goals]]spec files with and without any non-goals heading variantfinding exactly when no variant is present
P-ambiguityunit[[lint.C-unresolved-ambiguity]]bodies with and without bracketed NEEDS CLARIFICATION markersfinding with marker context exactly when present
P-entangledunit[[lint.C-entangled-spec]]PF/SA scenarios with presentation primitives vs BP scenarios and domain lookalikesfindings exactly in the PF/SA primitive cases
P-schemaunit[[lint.C-finding-schema]]lint runs over well-formed and malformed specsshared finding fields throughout, warning default, malformed → error + non-zero exit
P-bridgeunit[[lint.C-dual-bridge]]dual files with spk findings, plain files, missing binary, and failing invocationrelaying with spk. kind prefix; inert for plain files; advisory-only degradation in both failure modes
P-mismatchunit[[lint.C-no-silent-skip]]a kind-intent file whose frontmatter id differs from its filename stemlint emits spk-frontmatter-mismatch naming the file and the expected id
P-graphunit[[lint.C-graph-relay]]a dual-format corpus containing a typing violation and a dangling referenceboth are relayed as spk.graph.* warnings
P-traceunit[[lint.C-trace-advisory]]a dual-format corpus with a dangling VERIFIES link, an unlinked scenario, and an untraced propertyall three appear as advisory findings; the exit code is zero
P-plainunit[[lint.C-plain-untouched]]a corpus of plain openspec spec fileslint output contains no specodelic findings and no spk invocation occurs

Purpose

The spec linter: textual spec-shape findings (vague qualifiers, imperative steps, conjunctive bloat, missing negative scenarios and non-goals, unresolved ambiguities, entangled specs), the shared stable finding schema, and the dual-format specodelic bridge — complete since derive-contracts-from-specodelic: no silent skips, spk graph relay, and advisory scenario-to-property trace checks.

Requirements

Requirement: Vague Qualifier Detection

The system SHALL flag requirements and scenario steps that contain unbound qualitative terms without an adjacent numeric measurement.

Scenario: Flag unbound qualifier in requirement

  • GIVEN a requirement body contains the unbound qualifier “fast” with no numeric bound (a line like “p95 < 200 ms” would pass)
  • WHEN ah lint runs
  • THEN the command emits a vague-qualifier finding for that requirement
  • AND the finding suggests adding a measurable response condition (e.g., “within 200 ms”)
  • VERIFIES [[lint.P-vague]]

Scenario: Do not flag bounded qualifier

  • GIVEN a requirement body contains “fast (p95 < 200 ms)”
  • WHEN ah lint runs
  • THEN no vague-qualifier finding is emitted for that requirement
  • AND no error-severity finding is emitted — a numeric bound satisfies the check
  • VERIFIES [[lint.P-vague]]

Scenario: Flag qualifier in scenario step

  • GIVEN a THEN step contains “the response is user-friendly” and the step line carries no numeric bound (a step like “served under 200 ms” would pass)
  • WHEN ah lint runs
  • THEN the command emits a vague-qualifier finding for that scenario step
  • VERIFIES [[lint.P-vague]]

Requirement: Imperative Step Detection

The system SHALL flag WHEN and THEN steps that describe UI mechanics rather than business intent, coupling the spec to implementation details.

Scenario: Flag explicit click step

  • GIVEN a WHEN step contains “clicks the Submit button”
  • WHEN ah lint runs
  • THEN the command emits an imperative-step finding
  • AND the finding suggests rephrasing to describe the business action (e.g., “submits the form”)
  • VERIFIES [[lint.P-imperative]]

Scenario: Flag URL navigation step

  • GIVEN a WHEN step contains “navigates to /dashboard/settings”
  • WHEN ah lint runs
  • THEN the command emits an imperative-step finding
  • VERIFIES [[lint.P-imperative]]

Scenario: Do not flag declarative business steps

  • GIVEN a WHEN step contains “the user submits a payment”
  • WHEN ah lint runs
  • THEN no imperative-step finding is emitted
  • AND no error-severity finding is emitted — business phrasing passes cleanly
  • VERIFIES [[lint.P-imperative]]

Requirement: Conjunctive Step Bloat Detection

The system SHALL flag scenarios that chain more than the configured maximum number of AND-linked steps, indicating a scenario that tests multiple behaviors at once.

Scenario: Flag overlong scenario

  • GIVEN a scenario has eight AND-linked steps
  • AND the configured maximum is five
  • WHEN ah lint runs
  • THEN the command emits a conjunctive-bloat finding
  • AND the finding suggests splitting the scenario into focused single-behavior scenarios
  • VERIFIES [[lint.P-bloat]]

Scenario: Accept scenario within limit

  • GIVEN a scenario has four AND-linked steps
  • AND the configured maximum is five
  • WHEN ah lint runs
  • THEN no conjunctive-bloat finding is emitted for that scenario
  • VERIFIES [[lint.P-bloat]]

Scenario: Scenario exactly at limit is accepted

  • GIVEN a scenario has five AND-linked steps
  • AND the configured maximum is five
  • WHEN ah lint runs
  • THEN no conjunctive-bloat finding is emitted for that scenario — the limit boundary does not fail
  • VERIFIES [[lint.P-bloat]]

Scenario: Default maximum is configurable

  • GIVEN .espectacular/config.toml sets [lint] max_and_steps = 3
  • WHEN ah lint runs
  • THEN the command uses 3 as the maximum AND-step count
  • VERIFIES [[lint.P-bloat]]

Requirement: Missing Negative Scenario Detection

The system SHALL flag requirements that have no scenario exercising an error condition, rejection, or boundary violation, indicating incomplete behavioral specification.

Scenario: Flag requirement with only happy-path scenarios

  • GIVEN a requirement has two scenarios, both describing successful outcomes
  • AND no scenario contains rejection, error, or boundary language
  • WHEN ah lint runs
  • THEN the command emits a missing-negative-scenario finding
  • AND the finding suggests adding a scenario for the corresponding failure mode
  • VERIFIES [[lint.P-negative]]

Scenario: Accept requirement with at least one negative scenario

  • GIVEN a requirement has a scenario whose THEN step contains “the command exits non-zero”
  • WHEN ah lint runs
  • THEN no missing-negative-scenario finding is emitted for that requirement
  • VERIFIES [[lint.P-negative]]

Requirement: Missing Non-Goals Detection

The system SHALL flag spec capability files that lack an explicit Non-Goals or Out-of-Scope section, since their absence enables silent scope creep during AI-assisted implementation.

Scenario: Flag spec without non-goals section

  • GIVEN a spec.md file contains no heading matching Non-Goals, Non Goals, non-goals, or Out of Scope
  • WHEN ah lint runs
  • THEN the command emits a missing-non-goals finding for that spec file
  • VERIFIES [[lint.P-non-goals]]

Scenario: Accept spec with non-goals section

  • GIVEN a spec.md file contains a ## Non-Goals heading
  • WHEN ah lint runs
  • THEN no missing-non-goals finding is emitted for that file — an explicit exclusion section is never rejected
  • VERIFIES [[lint.P-non-goals]]

Requirement: Unresolved Ambiguity Detection

The system SHALL flag requirements and scenarios that contain the bracketed NEEDS CLARIFICATION marker, indicating authoring-time decisions deferred but not yet resolved.

Scenario: Flag needs-clarification marker

  • GIVEN a requirement body contains a bracketed NEEDS CLARIFICATION marker (e.g. an unresolved question about which auth provider applies, wrapped in square brackets)
  • WHEN ah lint runs
  • THEN the command emits an unresolved-ambiguity finding
  • AND the finding includes the marker text as context
  • VERIFIES [[lint.P-ambiguity]]

Scenario: Accept requirement with no ambiguity markers

  • GIVEN a requirement body contains no bracketed NEEDS CLARIFICATION marker, and no line pairs those two words inside square brackets
  • WHEN ah lint runs
  • THEN no unresolved-ambiguity finding is emitted for that requirement
  • AND resolved requirements emit no error — only deferred decisions are flagged
  • VERIFIES [[lint.P-ambiguity]]

Requirement: Entangled Specification Detection

The system SHALL flag scenarios whose contract archetype is PF or SA but whose text references presentation-layer primitives (buttons, modals, colors, CSS selectors, component names, client routes), since the archetype declares the scenario belongs to a layer where UI mechanics must not appear. The same text in a BP scenario is not flagged, as transport mechanics are legitimate at a boundary seam.

Scenario: Flag UI primitive in PF scenario

  • GIVEN a scenario with contract archetype PF whose THEN step contains “a green notification banner is displayed”
  • WHEN ah lint runs
  • THEN the command emits an entangled-spec finding
  • AND the finding suggests specifying the domain event or state instead of the visual primitive
  • VERIFIES [[lint.P-entangled]]

Scenario: Do not flag transport mechanics in BP scenario

  • GIVEN a scenario with contract archetype BP whose WHEN step contains “navigates to /dashboard/settings”
  • WHEN ah lint runs
  • THEN no entangled-spec finding is emitted for that scenario
  • VERIFIES [[lint.P-entangled]]

Scenario: Do not flag domain language resembling UI terms

  • GIVEN a scenario with contract archetype SA whose text contains “the event routing layer delivers the message”
  • WHEN ah lint runs
  • THEN no entangled-spec finding is emitted, as the matcher list excludes domain-legitimate uses of superficially similar terms
  • AND no error-severity finding is emitted for the lookalike either
  • VERIFIES [[lint.P-entangled]]

Requirement: Lint Finding Schema

The system SHALL emit lint findings using the same stable JSON envelope as ah check, enabling agent harnesses to consume both without separate parsing logic.

Scenario: Lint findings share check finding fields

  • GIVEN ah lint --json produces findings
  • WHEN the JSON output is inspected
  • THEN each finding contains kind, severity, spec_path, message, suggested_action, and playbook_command
  • AND findings that reference a specific scenario also contain scenario_id and scenario_title
  • VERIFIES [[lint.P-schema]]

Scenario: Lint findings are warning severity by default

  • GIVEN ah lint produces warning-severity findings for any lint check category
  • WHEN the JSON output is inspected
  • THEN every finding has severity = "warning"
  • VERIFIES [[lint.P-schema]]

Scenario: Malformed spec file is error severity

  • GIVEN a spec file contains invalid Markdown that prevents scenario parsing
  • WHEN ah lint runs
  • THEN the command emits a finding with severity = "error"
  • AND exits non-zero
  • VERIFIES [[lint.P-schema]]

Requirement: Dual-Format Lint Bridge

The system SHALL relay specodelic lint findings for dual-format spec files into the shared lint finding schema by invoking spk lint, without reimplementing specodelic rules.

Scenario: Relay specodelic findings for dual-format files

  • GIVEN a spec file carries id: spec frontmatter (dual-format)
  • AND spk lint reports findings for that file
  • WHEN ah lint runs
  • THEN each specodelic finding appears in the output with kind = "spk.<rule_id>"
  • AND every relayed finding has severity = "warning"
  • VERIFIES [[lint.P-bridge]]

Scenario: Bridge is inert for plain openspec files

  • GIVEN a spec file contains no dual-format frontmatter
  • WHEN ah lint runs
  • THEN no specodelic invocation occurs and the output is identical to pre-bridge behavior
  • VERIFIES [[lint.P-bridge]]

Scenario: Missing spk binary is advisory only

  • GIVEN a dual-format spec file
  • AND the spk binary is not available on PATH
  • WHEN ah lint runs
  • THEN the command emits a single advisory spk-unavailable finding
  • AND the exit code is unchanged relative to a successful warning-only lint run
  • VERIFIES [[lint.P-bridge]]

Scenario: Specodelic invocation failure does not hard-fail

  • GIVEN a dual-format spec file
  • AND spk lint exits with a non-envelope error or unparseable output
  • WHEN ah lint runs
  • THEN the command emits a single advisory finding describing the failure
  • AND continues linting the remaining spec files
  • VERIFIES [[lint.P-bridge]]

Requirement: Specodelic Relay Completeness

The linter SHALL emit an spk-frontmatter-mismatch finding for every kind: intent file whose frontmatter id does not match its filename stem instead of skipping it silently, and SHALL relay spk graph typing violations and dangling references as spk.graph.* warning findings.

Scenario: Mismatched id is reported

  • GIVEN a kind: intent file whose frontmatter id differs from its filename stem
  • WHEN ah lint runs over it
  • THEN an spk-frontmatter-mismatch finding names the file and the expected id
  • AND no specodelic file is skipped without a finding
  • VERIFIES [[lint.P-mismatch]]

Scenario: Graph findings relayed

  • GIVEN a dual-format corpus containing a typing violation and a dangling reference
  • WHEN ah lint runs over the corpus
  • THEN both are relayed as spk.graph.* warnings
  • VERIFIES [[lint.P-graph]]

Scenario: Clean dual-format file unchanged

  • GIVEN a dual-format file that passes spk lint and spk graph
  • WHEN ah lint runs over it
  • THEN no specodelic findings are emitted for the file

Requirement: Scenario Property Trace

The linter SHALL report advisory trace findings — verifies-dangling, scenario-unlinked, and property-untraced — for dual-format files with Properties rows, without affecting the exit code.

  • GIVEN a scenario carries **VERIFIES** [[lint.P-missing]] and no such property exists
  • WHEN ah lint runs over the file
  • THEN a verifies-dangling advisory finding is emitted

Scenario: Unlinked scenario and untraced property

  • GIVEN a dual-format file with Properties rows where one scenario has no VERIFIES link and one property is cited by no scenario
  • WHEN ah lint runs over the file
  • THEN scenario-unlinked and property-untraced advisory findings are emitted
  • AND the exit code is zero
  • VERIFIES [[lint.P-trace]]

Scenario: Plain files untouched

  • GIVEN a corpus of plain openspec spec files
  • WHEN ah lint runs over the corpus
  • THEN no specodelic findings are emitted and no spk invocation occurs
  • AND no error-severity trace finding is emitted — plain files never fail the trace gate
  • VERIFIES [[lint.P-plain]]

id: spec.authoring kind: intent statement: “WHEN any spec file in this repository is added, edited, or deployed by archive THE corpus SHALL stay lint-clean under specodelic dual format, with the mandate enforced in the repo hook chain and CI and the specodelic half re-derived from the archived delta whenever archive strips it.”


spec-authoring corpus mandate

Dogfooding change: every spec file in this repository — deployed capability specs and change deltas alike — carries the specodelic four-layer grammar in addition to its openspec grammar. This is repo practice, not product behavior: espectacular remains opt-in for adopters (C-opt-in, deployed gate spec), and this mandate never reaches ah check’s product logic.

This delta is itself authored in dual format, extending the pilot from adopt-dual-format-specs to the whole corpus.

Constraints

idkindexprtraces_to
C-corpus-dualinvariantevery spec.md under openspec/specs/ and under active openspec/changes/*/specs/ carries YAML frontmatter (id: spec, kind: intent, EARS statement) and the Constraints, Model, and Properties tables[[spec.authoring]]
C-lint-cleaninvariantspk lint exits zero over the corpus, and the repo hook chain plus CI run it on every change to a spec file[[spec.authoring]]
C-open-half-stableinvariantmigrating a file leaves its openspec requirement and scenario text textually identical (section-sync mirror and ah check stay green)[[spec.authoring]]
C-adopters-unaffectedadvisoryno espectacular product behavior requires dual format; plain openspec repos remain fully valid[[spec.authoring]]
C-archive-rederiveinvariantwhen openspec archive merges new requirements into a deployed spec and strips its specodelic half, the half is re-derived — frontmatter copied from the archived delta, surviving constraint/property rows carried over from the pre-archive version, rows added for newly merged requirements — and the gates re-run[[spec.authoring]]

Model

States

  • plain
  • dual
  • linted

Transitions

idfromtoguard
t-migrateplaindual[[spec.authoring.C-corpus-dual]]
t-lintduallinted[[spec.authoring.C-lint-clean]]
t-stablelintedlinted[[spec.authoring.C-open-half-stable]]
t-striplinteddual[[spec.authoring.C-archive-rederive]]

Properties

idkindderives_fromgeneratorpredicate
P-corpusunit[[spec.authoring.C-corpus-dual]]any spec.md under openspec/the file parses as dual format: frontmatter plus all three tables present
P-lintunit[[spec.authoring.C-lint-clean]]the full corpus after any edit to a spec filespk lint exits zero
P-open-halfunit[[spec.authoring.C-open-half-stable]]the openspec half of a migrated file diffed against its pre-migration git blobthe diff is empty outside frontmatter and the three tables
P-adopterunit[[spec.authoring.C-adopters-unaffected]]a plain openspec spec file with no frontmatterah check discovers scenarios and imposes no dual-format finding
P-rederiveunit[[spec.authoring.C-archive-rederive]]a deployed spec whose specodelic half was stripped by an archive that also merged new requirementsafter re-derivation, constraint and property rows cover both pre-existing and newly merged requirements, and spk lint exits zero

Purpose

Make the entire spec corpus of this repository dual-format and machine-linted, flipping this repo’s own enforcement from opt-in to mandated while preserving espectacular’s opt-in promise to adopters.

Non-Goals

  • Making any espectacular product behavior depend on dual format — adopters’ plain openspec repos remain fully valid (C-adopters-unaffected)
  • Automating archive-time re-derivation inside the tool: re-deriving the stripped specodelic half is a contributor step after openspec archive, not tool behavior
  • Retroactively migrating archived change deltas — the mandate covers deployed specs and active changes only
  • Extending the specodelic grammar itself — upstream concern (specodelic)

Requirements

Requirement: Dual-Format Spec Corpus

The repository SHALL keep every spec file under openspec/ in specodelic dual format, lint-clean under spk lint, with the mandate enforced by the repo hook chain and CI, and SHALL re-derive the specodelic half after openspec archive merges new requirements into a deployed spec and strips it.

Scenario: Plain spec file rejected

  • GIVEN a spec.md under openspec/ carries no YAML frontmatter and no specodelic tables
  • WHEN the repo hook chain or CI lints the corpus
  • THEN spk lint emits a finding for the file
  • AND the change cannot be committed
  • VERIFIES [[spec.authoring.P-adopter]]

Scenario: Migration keeps the openspec half identical

  • GIVEN a spec file is migrated to dual format
  • WHEN its openspec requirement and scenario text is diffed against the pre-migration version
  • THEN the diff shows no change outside the frontmatter and the Constraints, Model, and Properties tables
  • AND ah check output is unchanged
  • VERIFIES [[spec.authoring.P-open-half]]

Scenario: Archive strip is re-derived

  • GIVEN openspec archive merged a delta’s requirements into a deployed spec and stripped its specodelic half
  • WHEN the contributor re-derives the half — frontmatter from the archived delta, surviving constraint and property rows from the pre-archive version, rows added for the newly merged requirements — and re-runs the gates
  • THEN spk lint exits zero on the deployed spec again
  • AND the constraints and properties cover both pre-existing and newly merged requirements
  • AND the corpus-wide lint stays green
  • VERIFIES [[spec.authoring.P-rederive]]

Scenario: Corpus parses as dual format

  • GIVEN any non-archived spec.md under openspec/
  • WHEN the corpus gate (just spec-lint) runs
  • THEN every file parses as dual format: frontmatter plus Constraints, Model, and Properties tables present
  • VERIFIES [[spec.authoring.P-corpus]]

Scenario: Corpus is lint clean

  • GIVEN the full spec corpus after any edit to a spec file
  • WHEN spk lint runs over the non-archived corpus
  • THEN it exits zero with no findings
  • VERIFIES [[spec.authoring.P-lint]]