packages feed

tasty-bdd 0.1.0.1 → 0.1.0.2

raw patch · 49 files changed

+4866/−503 lines, 49 filesdep −aesondep −aeson-qqdep −microlens-thdep ~HUnitdep ~exceptionsdep ~freePVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies removed: aeson, aeson-qq, microlens-th, pretty, pretty-show, qm-interpolated-string, regex-posix, transformers

Dependency ranges changed: HUnit, exceptions, free, microlens, mtl, tagged, tasty, tasty-expected-failure, tasty-fail-fast, tasty-hunit, temporary, text, tree-diff

API changes (from Hackage documentation)

- Test.BDD.LanguageFree: instance GHC.Base.Functor (Test.BDD.LanguageFree.GivenFree m)
- Test.BDD.LanguageFree: instance GHC.Base.Functor (Test.BDD.LanguageFree.ThenFree m t)
- Test.Tasty.Bdd: instance (Control.Monad.Catch.MonadCatch m, Data.Typeable.Internal.Typeable m) => Test.Tasty.Core.IsTest (Test.Tasty.Bdd.FreeBDDCase m)
- Test.Tasty.Bdd: instance (Data.Typeable.Internal.Typeable t, Test.Tasty.Bdd.TestableMonad m) => Test.Tasty.Core.IsTest (Test.BDD.Language.BDDTest m t ())
- Test.Tasty.Bdd: instance GHC.Exception.Type.Exception Test.Tasty.Bdd.EqualityDoesntHold
- Test.Tasty.Bdd: instance GHC.Show.Show Test.Tasty.Bdd.EqualityDoesntHold
+ Test.BDD.LanguageFree: instance GHC.Internal.Base.Functor (Test.BDD.LanguageFree.GivenFree m)
+ Test.BDD.LanguageFree: instance GHC.Internal.Base.Functor (Test.BDD.LanguageFree.ThenFree m t)
+ Test.Tasty.Bdd: instance (Control.Monad.Catch.MonadCatch m, GHC.Internal.Data.Typeable.Internal.Typeable m) => Test.Tasty.Core.IsTest (Test.Tasty.Bdd.FreeBDDCase m)
+ Test.Tasty.Bdd: instance (GHC.Internal.Data.Typeable.Internal.Typeable t, Test.Tasty.Bdd.TestableMonad m) => Test.Tasty.Core.IsTest (Test.BDD.Language.BDDTest m t ())
+ Test.Tasty.Bdd: instance GHC.Internal.Exception.Type.Exception Test.Tasty.Bdd.EqualityDoesntHold
+ Test.Tasty.Bdd: instance GHC.Internal.Show.Show Test.Tasty.Bdd.EqualityDoesntHold
- Test.BDD.Language: BDDTest :: [t -> m q] -> [TestContext m] -> m t -> BDDTest m t q
+ Test.BDD.Language: BDDTest :: [t -> m q] -> [TestContext m] -> m t -> BDDTest (m :: Type -> Type) t q
- Test.BDD.Language: TestContext :: m r -> (r -> m ()) -> TestContext m
+ Test.BDD.Language: TestContext :: m r -> (r -> m ()) -> TestContext (m :: Type -> Type)
- Test.BDD.Language: [End] :: Language m t q 'Testing
+ Test.BDD.Language: [End] :: forall (m :: Type -> Type) t q. Language m t q 'Testing
- Test.BDD.Language: [GivenAndAfter] :: m r -> (r -> m ()) -> Language m t q 'Preparing -> Language m t q 'Preparing
+ Test.BDD.Language: [GivenAndAfter] :: forall (m :: Type -> Type) r t q. m r -> (r -> m ()) -> Language m t q 'Preparing -> Language m t q 'Preparing
- Test.BDD.Language: [Given] :: m () -> Language m t q 'Preparing -> Language m t q 'Preparing
+ Test.BDD.Language: [Given] :: forall (m :: Type -> Type) t q. m () -> Language m t q 'Preparing -> Language m t q 'Preparing
- Test.BDD.Language: [Then] :: (t -> m q) -> Language m t q 'Testing -> Language m t q 'Testing
+ Test.BDD.Language: [Then] :: forall t (m :: Type -> Type) q. (t -> m q) -> Language m t q 'Testing -> Language m t q 'Testing
- Test.BDD.Language: [When] :: m t -> Language m t q 'Testing -> Language m t q 'Preparing
+ Test.BDD.Language: [When] :: forall (m :: Type -> Type) t q. m t -> Language m t q 'Testing -> Language m t q 'Preparing
- Test.BDD.Language: [_context] :: BDDTest m t q -> [TestContext m]
+ Test.BDD.Language: [_context] :: BDDTest (m :: Type -> Type) t q -> [TestContext m]
- Test.BDD.Language: [_tests] :: BDDTest m t q -> [t -> m q]
+ Test.BDD.Language: [_tests] :: BDDTest (m :: Type -> Type) t q -> [t -> m q]
- Test.BDD.Language: [_when] :: BDDTest m t q -> m t
+ Test.BDD.Language: [_when] :: BDDTest (m :: Type -> Type) t q -> m t
- Test.BDD.Language: context :: forall m_adM3 t_adM4 q_adM5. Lens' (BDDTest m_adM3 t_adM4 q_adM5) [TestContext m_adM3]
+ Test.BDD.Language: context :: forall f (m :: Type -> Type) t q. Functor f => ([TestContext m] -> f [TestContext m]) -> BDDTest m t q -> f (BDDTest m t q)
- Test.BDD.Language: data BDDTest m t q
+ Test.BDD.Language: data BDDTest (m :: Type -> Type) t q
- Test.BDD.Language: data Language m t q a
+ Test.BDD.Language: data Language (m :: Type -> Type) t q (a :: Phase)
- Test.BDD.Language: data TestContext m
+ Test.BDD.Language: data TestContext (m :: Type -> Type)
- Test.BDD.Language: interpret :: Monad m => Language m t q a -> BDDTest m t q
+ Test.BDD.Language: interpret :: forall (m :: Type -> Type) t q (a :: Phase). Monad m => Language m t q a -> BDDTest m t q
- Test.BDD.Language: tests :: forall m_adM3 t_adM4 q_adM5 q_ae3n. Lens (BDDTest m_adM3 t_adM4 q_adM5) (BDDTest m_adM3 t_adM4 q_ae3n) [t_adM4 -> m_adM3 q_adM5] [t_adM4 -> m_adM3 q_ae3n]
+ Test.BDD.Language: tests :: Functor f => ([t -> m q1] -> f [t -> m q2]) -> BDDTest m t q1 -> f (BDDTest m t q2)
- Test.BDD.Language: type BDDPreparing m t q = Language m t q 'Preparing
+ Test.BDD.Language: type BDDPreparing (m :: Type -> Type) t q = Language m t q 'Preparing
- Test.BDD.Language: type BDDTesting m t q = Language m t q 'Testing
+ Test.BDD.Language: type BDDTesting (m :: Type -> Type) t q = Language m t q 'Testing
- Test.BDD.Language: when :: forall m_adM3 t_adM4 q_adM5. Lens' (BDDTest m_adM3 t_adM4 q_adM5) (m_adM3 t_adM4)
+ Test.BDD.Language: when :: Functor f => (m t -> f (m t)) -> BDDTest m t q -> f (BDDTest m t q)
- Test.BDD.LanguageFree: Failed :: SomeException -> m () -> BDDResult m
+ Test.BDD.LanguageFree: Failed :: SomeException -> m () -> BDDResult (m :: Type -> Type)
- Test.BDD.LanguageFree: Succeded :: m () -> BDDResult m
+ Test.BDD.LanguageFree: Succeded :: m () -> BDDResult (m :: Type -> Type)
- Test.BDD.LanguageFree: data BDDResult m
+ Test.BDD.LanguageFree: data BDDResult (m :: Type -> Type)
- Test.BDD.LanguageFree: data GivenFree m a
+ Test.BDD.LanguageFree: data GivenFree (m :: Type -> Type) a
- Test.BDD.LanguageFree: data ThenFree m t a
+ Test.BDD.LanguageFree: data ThenFree (m :: Type -> Type) t a
- Test.BDD.LanguageFree: type FreeBDD m x = Free (GivenFree m) x
+ Test.BDD.LanguageFree: type FreeBDD (m :: Type -> Type) x = Free GivenFree m x
- Test.Tasty.Bdd: [End] :: Language m t q 'Testing
+ Test.Tasty.Bdd: [End] :: forall (m :: Type -> Type) t q. Language m t q 'Testing
- Test.Tasty.Bdd: [GivenAndAfter] :: m r -> (r -> m ()) -> Language m t q 'Preparing -> Language m t q 'Preparing
+ Test.Tasty.Bdd: [GivenAndAfter] :: forall (m :: Type -> Type) r t q. m r -> (r -> m ()) -> Language m t q 'Preparing -> Language m t q 'Preparing
- Test.Tasty.Bdd: [Given] :: m () -> Language m t q 'Preparing -> Language m t q 'Preparing
+ Test.Tasty.Bdd: [Given] :: forall (m :: Type -> Type) t q. m () -> Language m t q 'Preparing -> Language m t q 'Preparing
- Test.Tasty.Bdd: [Then] :: (t -> m q) -> Language m t q 'Testing -> Language m t q 'Testing
+ Test.Tasty.Bdd: [Then] :: forall t (m :: Type -> Type) q. (t -> m q) -> Language m t q 'Testing -> Language m t q 'Testing
- Test.Tasty.Bdd: [When] :: m t -> Language m t q 'Testing -> Language m t q 'Preparing
+ Test.Tasty.Bdd: [When] :: forall (m :: Type -> Type) t q. m t -> Language m t q 'Testing -> Language m t q 'Preparing
- Test.Tasty.Bdd: class (MonadCatch m, MonadIO m, Monad m, Typeable m) => TestableMonad m
+ Test.Tasty.Bdd: class (MonadCatch m, MonadIO m, Monad m, Typeable m) => TestableMonad (m :: Type -> Type)
- Test.Tasty.Bdd: data Language m t q a
+ Test.Tasty.Bdd: data Language (m :: Type -> Type) t q (a :: Phase)
- Test.Tasty.Bdd: testBehavior :: (MonadIO m, TestableMonad m, Typeable t) => String -> BDDPreparing m t () -> TestTree
+ Test.Tasty.Bdd: testBehavior :: forall (m :: Type -> Type) t. (MonadIO m, TestableMonad m, Typeable t) => String -> BDDPreparing m t () -> TestTree
- Test.Tasty.Bdd: testBehaviorIO :: (Typeable t, MonadIO m, TestableMonad m) => String -> IO (BDDPreparing m t ()) -> TestTree
+ Test.Tasty.Bdd: testBehaviorIO :: forall t (m :: Type -> Type). (Typeable t, MonadIO m, TestableMonad m) => String -> IO (BDDPreparing m t ()) -> TestTree
- Test.Tasty.Bdd: type BDDPreparing m t q = Language m t q 'Preparing
+ Test.Tasty.Bdd: type BDDPreparing (m :: Type -> Type) t q = Language m t q 'Preparing
- Test.Tasty.Bdd: type BDDTesting m t q = Language m t q 'Testing
+ Test.Tasty.Bdd: type BDDTesting (m :: Type -> Type) t q = Language m t q 'Testing

Files

+ .github/actionlint.yaml view
@@ -0,0 +1,2 @@+self-hosted-runner:+  labels: [nixos]
+ .github/pull_request_template.md view
@@ -0,0 +1,11 @@+<!-- Describe behavior for a reader who has only this repository. -->++## What changed++## Why++## How to verify++<!-- Use commands from this repository or link the CI run. -->++## Limits
+ .github/workflows/ci.yml view
@@ -0,0 +1,59 @@+name: CI+on:+  push:+    branches: [master, 'chore/**']+  pull_request:+    branches: [master]+  workflow_dispatch:+permissions:+  contents: read+concurrency:+  group: ci-${{ github.ref }}+  cancel-in-progress: true+jobs:+  build-gate:+    name: Build Gate+    runs-on: ${{ github.repository_owner == 'lambdasistemi' && 'nixos' || 'ubuntu-latest' }}+    steps:+      - uses: actions/checkout@v6+      - name: Install Nix before organization transfer+        if: github.repository_owner != 'lambdasistemi'+        uses: paolino/dev-assets/setup-nix@0328b73b71788bb83848fe407dd04136a52c3697+        with:+          cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }}+      - uses: cachix/cachix-action@v17+        if: github.repository_owner == 'lambdasistemi'+        with:+          name: paolino+          authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}+      - run: nix flake check --accept-flake-config --no-eval-cache+      - run: nix build --accept-flake-config .#devShells.x86_64-linux.default.inputDerivation+  checks:+    name: CI ${{ matrix.check }}+    needs: build-gate+    runs-on: ${{ github.repository_owner == 'lambdasistemi' && 'nixos' || 'ubuntu-latest' }}+    strategy:+      fail-fast: false+      matrix:+        check: [unit, format-check, hlint, cabal-check, workflow-check, docs, sdist-rebuild, api-compat, hackage-quality]+    steps:+      - uses: actions/checkout@v6+      - name: Install Nix before organization transfer+        if: github.repository_owner != 'lambdasistemi'+        uses: paolino/dev-assets/setup-nix@0328b73b71788bb83848fe407dd04136a52c3697+        with:+          cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }}+      - run: nix run --accept-flake-config .#${{ matrix.check }}+  dev-shell:+    name: CI dev-shell+    needs: build-gate+    runs-on: ${{ github.repository_owner == 'lambdasistemi' && 'nixos' || 'ubuntu-latest' }}+    steps:+      - uses: actions/checkout@v6+      - name: Install Nix before organization transfer+        if: github.repository_owner != 'lambdasistemi'+        uses: paolino/dev-assets/setup-nix@0328b73b71788bb83848fe407dd04136a52c3697+        with:+          cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }}+      - run: nix develop --accept-flake-config --quiet -c just build+      - run: nix develop --accept-flake-config --quiet -c just unit
+ .github/workflows/deploy-docs.yml view
@@ -0,0 +1,48 @@+name: Deploy documentation+on:+  push:+    branches: [master, 'chore/ghc-9.12-modernization']+  workflow_dispatch:+permissions:+  contents: read+  pages: write+  id-token: write+concurrency:+  group: pages+  cancel-in-progress: false+jobs:+  build-gate:+    name: Docs Build Gate+    runs-on: ${{ github.repository_owner == 'lambdasistemi' && 'nixos' || 'ubuntu-latest' }}+    steps:+      - uses: actions/checkout@v6+      - name: Install Nix before organization transfer+        if: github.repository_owner != 'lambdasistemi'+        uses: paolino/dev-assets/setup-nix@0328b73b71788bb83848fe407dd04136a52c3697+        with:+          cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }}+      - run: nix build --accept-flake-config .#docs -o result-docs+      - name: Stage the site and bind it to the source commit+        run: |+          mkdir -p site+          cp -R -L result-docs/. site/+          chmod -R u+w site+          echo "$GITHUB_SHA" > site/revision.txt+      - uses: actions/upload-pages-artifact@v5+        with:+          path: site+  deploy:+    if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/chore/ghc-9.12-modernization'+    needs: build-gate+    runs-on: ${{ github.repository_owner == 'lambdasistemi' && 'nixos' || 'ubuntu-latest' }}+    environment:+      name: github-pages+      url: ${{ steps.deployment.outputs.page_url }}+    steps:+      - uses: actions/checkout@v6+      - id: deployment+        uses: actions/deploy-pages@v5+      - name: Verify the deployed revision and documentation bytes+        env:+          PAGES_URL: ${{ steps.deployment.outputs.page_url }}+        run: nix run --accept-flake-config .#pages-smoke -- "$PAGES_URL" "$GITHUB_SHA"
+ .github/workflows/release.yml view
@@ -0,0 +1,26 @@+name: Prepare release+on:+  workflow_dispatch:+permissions:+  contents: read+jobs:+  build-gate:+    name: Release Build Gate+    runs-on: ${{ github.repository_owner == 'lambdasistemi' && 'nixos' || 'ubuntu-latest' }}+    steps:+      - uses: actions/checkout@v6+      - name: Install Nix before organization transfer+        if: github.repository_owner != 'lambdasistemi'+        uses: paolino/dev-assets/setup-nix@0328b73b71788bb83848fe407dd04136a52c3697+        with:+          cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }}+      - run: nix flake check --accept-flake-config --no-eval-cache+      - run: nix build --accept-flake-config .#hackage-release -o result-release+      - uses: actions/upload-artifact@v4+        with:+          name: source-distribution-for-review+          path: |+            result-release/*.tar.gz+            result-release/SHA256SUMS+            result-release/haddock.log+          if-no-files-found: error
+ .specify/memory/constitution.md view
@@ -0,0 +1,20 @@+# tasty-bdd constitution++Version: 1.1.0. Ratified: 2026-09-11. Amended: 2026-09-12.++## Core principles++1. Documentation, specifications, vision and acceptance criteria outrank implementation. Code can be regenerated from a good record; the record cannot be regenerated from code. Every change ships its user-facing documentation and executable acceptance evidence together.+2. Preserve the published BDD API, providers, ordering and teardown behavior during tooling modernization. Changes to those promises need an explicit product decision. Retain the published `Succeded` constructor spelling for compatibility.+3. Use a locked Nix flake and GHC 9.12.3. Local and CI checks execute the same tools. Build, tests, formatting, lint, package metadata and documentation checks must fail when their subject is broken.+4. Keep the Cabal package description authoritative; do not maintain generated hpack and handwritten Cabal descriptions concurrently. Keep package metadata suitable for Hackage.+5. Keep source provenance: preserve the GitLab commits that follow GitHub and match the published library source. Repository ownership and package publication are separate operations.+6. Every library package and component, published or unpublished, must embed its current generated API reference in the live project documentation using its language’s appropriate tool. For this Haskell project, use Haddock. CI derives and verifies the embedded API from the same source revision and checks the served pages; independently maintained API copies are not acceptable. A Hackage release reuses that rendering.++## Development workflow++Specifications describe user outcomes before implementation. Plans record choices and tasks track evidence. Use focused conventional commits on a branch and a reviewed pull request; no direct default-branch changes, force pushes or merges with failing required checks. Test development changes at `-O0`; packaged CI builds use `-O2`. Enable strict warnings in CI through a manual Cabal flag, not for downstream users by default.++## Governance++A maintainer approves changes to this constitution with a rationale and corresponding spec updates. Repository transfer is an operator action described in a reviewed runbook. No workflow in this modernization publishes to Hackage; a future release requires a separate version and publication decision. Never reuse an already published version for an upload. No new API or provider decisions are implied by a toolchain update.
+ .specify/scripts/bash/check-prerequisites.sh view
@@ -0,0 +1,190 @@+#!/usr/bin/env bash++# Consolidated prerequisite checking script+#+# This script provides unified prerequisite checking for Spec-Driven Development workflow.+# It replaces the functionality previously spread across multiple scripts.+#+# Usage: ./check-prerequisites.sh [OPTIONS]+#+# OPTIONS:+#   --json              Output in JSON format+#   --require-tasks     Require tasks.md to exist (for implementation phase)+#   --include-tasks     Include tasks.md in AVAILABLE_DOCS list+#   --paths-only        Only output path variables (no validation)+#   --help, -h          Show help message+#+# OUTPUTS:+#   JSON mode: {"FEATURE_DIR":"...", "AVAILABLE_DOCS":["..."]}+#   Text mode: FEATURE_DIR:... \n AVAILABLE_DOCS: \n ✓/✗ file.md+#   Paths only: REPO_ROOT: ... \n BRANCH: ... \n FEATURE_DIR: ... etc.++set -e++# Parse command line arguments+JSON_MODE=false+REQUIRE_TASKS=false+INCLUDE_TASKS=false+PATHS_ONLY=false++for arg in "$@"; do+    case "$arg" in+        --json)+            JSON_MODE=true+            ;;+        --require-tasks)+            REQUIRE_TASKS=true+            ;;+        --include-tasks)+            INCLUDE_TASKS=true+            ;;+        --paths-only)+            PATHS_ONLY=true+            ;;+        --help|-h)+            cat << 'EOF'+Usage: check-prerequisites.sh [OPTIONS]++Consolidated prerequisite checking for Spec-Driven Development workflow.++OPTIONS:+  --json              Output in JSON format+  --require-tasks     Require tasks.md to exist (for implementation phase)+  --include-tasks     Include tasks.md in AVAILABLE_DOCS list+  --paths-only        Only output path variables (no prerequisite validation)+  --help, -h          Show this help message++EXAMPLES:+  # Check task prerequisites (plan.md required)+  ./check-prerequisites.sh --json++  # Check implementation prerequisites (plan.md + tasks.md required)+  ./check-prerequisites.sh --json --require-tasks --include-tasks++  # Get feature paths only (no validation)+  ./check-prerequisites.sh --paths-only++EOF+            exit 0+            ;;+        *)+            echo "ERROR: Unknown option '$arg'. Use --help for usage information." >&2+            exit 1+            ;;+    esac+done++# Source common functions+SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"+source "$SCRIPT_DIR/common.sh"++# Get feature paths and validate branch+_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }+eval "$_paths_output"+unset _paths_output+check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1++# If paths-only mode, output paths and exit (support JSON + paths-only combined)+if $PATHS_ONLY; then+    if $JSON_MODE; then+        # Minimal JSON paths payload (no validation performed)+        if has_jq; then+            jq -cn \+                --arg repo_root "$REPO_ROOT" \+                --arg branch "$CURRENT_BRANCH" \+                --arg feature_dir "$FEATURE_DIR" \+                --arg feature_spec "$FEATURE_SPEC" \+                --arg impl_plan "$IMPL_PLAN" \+                --arg tasks "$TASKS" \+                '{REPO_ROOT:$repo_root,BRANCH:$branch,FEATURE_DIR:$feature_dir,FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,TASKS:$tasks}'+        else+            printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \+                "$(json_escape "$REPO_ROOT")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$TASKS")"+        fi+    else+        echo "REPO_ROOT: $REPO_ROOT"+        echo "BRANCH: $CURRENT_BRANCH"+        echo "FEATURE_DIR: $FEATURE_DIR"+        echo "FEATURE_SPEC: $FEATURE_SPEC"+        echo "IMPL_PLAN: $IMPL_PLAN"+        echo "TASKS: $TASKS"+    fi+    exit 0+fi++# Validate required directories and files+if [[ ! -d "$FEATURE_DIR" ]]; then+    echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2+    echo "Run /speckit.specify first to create the feature structure." >&2+    exit 1+fi++if [[ ! -f "$IMPL_PLAN" ]]; then+    echo "ERROR: plan.md not found in $FEATURE_DIR" >&2+    echo "Run /speckit.plan first to create the implementation plan." >&2+    exit 1+fi++# Check for tasks.md if required+if $REQUIRE_TASKS && [[ ! -f "$TASKS" ]]; then+    echo "ERROR: tasks.md not found in $FEATURE_DIR" >&2+    echo "Run /speckit.tasks first to create the task list." >&2+    exit 1+fi++# Build list of available documents+docs=()++# Always check these optional docs+[[ -f "$RESEARCH" ]] && docs+=("research.md")+[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md")++# Check contracts directory (only if it exists and has files)+if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then+    docs+=("contracts/")+fi++[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md")++# Include tasks.md if requested and it exists+if $INCLUDE_TASKS && [[ -f "$TASKS" ]]; then+    docs+=("tasks.md")+fi++# Output results+if $JSON_MODE; then+    # Build JSON array of documents+    if has_jq; then+        if [[ ${#docs[@]} -eq 0 ]]; then+            json_docs="[]"+        else+            json_docs=$(printf '%s\n' "${docs[@]}" | jq -R . | jq -s .)+        fi+        jq -cn \+            --arg feature_dir "$FEATURE_DIR" \+            --argjson docs "$json_docs" \+            '{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs}'+    else+        if [[ ${#docs[@]} -eq 0 ]]; then+            json_docs="[]"+        else+            json_docs=$(for d in "${docs[@]}"; do printf '"%s",' "$(json_escape "$d")"; done)+            json_docs="[${json_docs%,}]"+        fi+        printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$(json_escape "$FEATURE_DIR")" "$json_docs"+    fi+else+    # Text output+    echo "FEATURE_DIR:$FEATURE_DIR"+    echo "AVAILABLE_DOCS:"++    # Show status of each potential document+    check_file "$RESEARCH" "research.md"+    check_file "$DATA_MODEL" "data-model.md"+    check_dir "$CONTRACTS_DIR" "contracts/"+    check_file "$QUICKSTART" "quickstart.md"++    if $INCLUDE_TASKS; then+        check_file "$TASKS" "tasks.md"+    fi+fi
+ .specify/scripts/bash/common.sh view
@@ -0,0 +1,329 @@+#!/usr/bin/env bash+# Common functions and variables for all scripts++# Find repository root by searching upward for .specify directory+# This is the primary marker for spec-kit projects+find_specify_root() {+    local dir="${1:-$(pwd)}"+    # Normalize to absolute path to prevent infinite loop with relative paths+    # Use -- to handle paths starting with - (e.g., -P, -L)+    dir="$(cd -- "$dir" 2>/dev/null && pwd)" || return 1+    local prev_dir=""+    while true; do+        if [ -d "$dir/.specify" ]; then+            echo "$dir"+            return 0+        fi+        # Stop if we've reached filesystem root or dirname stops changing+        if [ "$dir" = "/" ] || [ "$dir" = "$prev_dir" ]; then+            break+        fi+        prev_dir="$dir"+        dir="$(dirname "$dir")"+    done+    return 1+}++# Get repository root, prioritizing .specify directory over git+# This prevents using a parent git repo when spec-kit is initialized in a subdirectory+get_repo_root() {+    # First, look for .specify directory (spec-kit's own marker)+    local specify_root+    if specify_root=$(find_specify_root); then+        echo "$specify_root"+        return+    fi++    # Fallback to git if no .specify found+    if git rev-parse --show-toplevel >/dev/null 2>&1; then+        git rev-parse --show-toplevel+        return+    fi++    # Final fallback to script location for non-git repos+    local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"+    (cd "$script_dir/../../.." && pwd)+}++# Get current branch, with fallback for non-git repositories+get_current_branch() {+    # First check if SPECIFY_FEATURE environment variable is set+    if [[ -n "${SPECIFY_FEATURE:-}" ]]; then+        echo "$SPECIFY_FEATURE"+        return+    fi++    # Then check git if available at the spec-kit root (not parent)+    local repo_root=$(get_repo_root)+    if has_git; then+        git -C "$repo_root" rev-parse --abbrev-ref HEAD+        return+    fi++    # For non-git repos, try to find the latest feature directory+    local specs_dir="$repo_root/specs"++    if [[ -d "$specs_dir" ]]; then+        local latest_feature=""+        local highest=0+        local latest_timestamp=""++        for dir in "$specs_dir"/*; do+            if [[ -d "$dir" ]]; then+                local dirname=$(basename "$dir")+                if [[ "$dirname" =~ ^([0-9]{8}-[0-9]{6})- ]]; then+                    # Timestamp-based branch: compare lexicographically+                    local ts="${BASH_REMATCH[1]}"+                    if [[ "$ts" > "$latest_timestamp" ]]; then+                        latest_timestamp="$ts"+                        latest_feature=$dirname+                    fi+                elif [[ "$dirname" =~ ^([0-9]{3})- ]]; then+                    local number=${BASH_REMATCH[1]}+                    number=$((10#$number))+                    if [[ "$number" -gt "$highest" ]]; then+                        highest=$number+                        # Only update if no timestamp branch found yet+                        if [[ -z "$latest_timestamp" ]]; then+                            latest_feature=$dirname+                        fi+                    fi+                fi+            fi+        done++        if [[ -n "$latest_feature" ]]; then+            echo "$latest_feature"+            return+        fi+    fi++    echo "main"  # Final fallback+}++# Check if we have git available at the spec-kit root level+# Returns true only if git is installed and the repo root is inside a git work tree+# Handles both regular repos (.git directory) and worktrees/submodules (.git file)+has_git() {+    # First check if git command is available (before calling get_repo_root which may use git)+    command -v git >/dev/null 2>&1 || return 1+    local repo_root=$(get_repo_root)+    # Check if .git exists (directory or file for worktrees/submodules)+    [ -e "$repo_root/.git" ] || return 1+    # Verify it's actually a valid git work tree+    git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1+}++check_feature_branch() {+    local branch="$1"+    local has_git_repo="$2"++    # For non-git repos, we can't enforce branch naming but still provide output+    if [[ "$has_git_repo" != "true" ]]; then+        echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2+        return 0+    fi++    if [[ ! "$branch" =~ ^[0-9]{3}- ]] && [[ ! "$branch" =~ ^[0-9]{8}-[0-9]{6}- ]]; then+        echo "ERROR: Not on a feature branch. Current branch: $branch" >&2+        echo "Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name" >&2+        return 1+    fi++    return 0+}++get_feature_dir() { echo "$1/specs/$2"; }++# Find feature directory by numeric prefix instead of exact branch match+# This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature)+find_feature_dir_by_prefix() {+    local repo_root="$1"+    local branch_name="$2"+    local specs_dir="$repo_root/specs"++    # Extract prefix from branch (e.g., "004" from "004-whatever" or "20260319-143022" from timestamp branches)+    local prefix=""+    if [[ "$branch_name" =~ ^([0-9]{8}-[0-9]{6})- ]]; then+        prefix="${BASH_REMATCH[1]}"+    elif [[ "$branch_name" =~ ^([0-9]{3})- ]]; then+        prefix="${BASH_REMATCH[1]}"+    else+        # If branch doesn't have a recognized prefix, fall back to exact match+        echo "$specs_dir/$branch_name"+        return+    fi++    # Search for directories in specs/ that start with this prefix+    local matches=()+    if [[ -d "$specs_dir" ]]; then+        for dir in "$specs_dir"/"$prefix"-*; do+            if [[ -d "$dir" ]]; then+                matches+=("$(basename "$dir")")+            fi+        done+    fi++    # Handle results+    if [[ ${#matches[@]} -eq 0 ]]; then+        # No match found - return the branch name path (will fail later with clear error)+        echo "$specs_dir/$branch_name"+    elif [[ ${#matches[@]} -eq 1 ]]; then+        # Exactly one match - perfect!+        echo "$specs_dir/${matches[0]}"+    else+        # Multiple matches - this shouldn't happen with proper naming convention+        echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2+        echo "Please ensure only one spec directory exists per prefix." >&2+        return 1+    fi+}++get_feature_paths() {+    local repo_root=$(get_repo_root)+    local current_branch=$(get_current_branch)+    local has_git_repo="false"++    if has_git; then+        has_git_repo="true"+    fi++    # Use prefix-based lookup to support multiple branches per spec+    local feature_dir+    if ! feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch"); then+        echo "ERROR: Failed to resolve feature directory" >&2+        return 1+    fi++    # Use printf '%q' to safely quote values, preventing shell injection+    # via crafted branch names or paths containing special characters+    printf 'REPO_ROOT=%q\n' "$repo_root"+    printf 'CURRENT_BRANCH=%q\n' "$current_branch"+    printf 'HAS_GIT=%q\n' "$has_git_repo"+    printf 'FEATURE_DIR=%q\n' "$feature_dir"+    printf 'FEATURE_SPEC=%q\n' "$feature_dir/spec.md"+    printf 'IMPL_PLAN=%q\n' "$feature_dir/plan.md"+    printf 'TASKS=%q\n' "$feature_dir/tasks.md"+    printf 'RESEARCH=%q\n' "$feature_dir/research.md"+    printf 'DATA_MODEL=%q\n' "$feature_dir/data-model.md"+    printf 'QUICKSTART=%q\n' "$feature_dir/quickstart.md"+    printf 'CONTRACTS_DIR=%q\n' "$feature_dir/contracts"+}++# Check if jq is available for safe JSON construction+has_jq() {+    command -v jq >/dev/null 2>&1+}++# Escape a string for safe embedding in a JSON value (fallback when jq is unavailable).+# Handles backslash, double-quote, and JSON-required control character escapes (RFC 8259).+json_escape() {+    local s="$1"+    s="${s//\\/\\\\}"+    s="${s//\"/\\\"}"+    s="${s//$'\n'/\\n}"+    s="${s//$'\t'/\\t}"+    s="${s//$'\r'/\\r}"+    s="${s//$'\b'/\\b}"+    s="${s//$'\f'/\\f}"+    # Escape any remaining U+0001-U+001F control characters as \uXXXX.+    # (U+0000/NUL cannot appear in bash strings and is excluded.)+    # LC_ALL=C ensures ${#s} counts bytes and ${s:$i:1} yields single bytes,+    # so multi-byte UTF-8 sequences (first byte >= 0xC0) pass through intact.+    local LC_ALL=C+    local i char code+    for (( i=0; i<${#s}; i++ )); do+        char="${s:$i:1}"+        printf -v code '%d' "'$char" 2>/dev/null || code=256+        if (( code >= 1 && code <= 31 )); then+            printf '\\u%04x' "$code"+        else+            printf '%s' "$char"+        fi+    done+}++check_file() { [[ -f "$1" ]] && echo "  ✓ $2" || echo "  ✗ $2"; }+check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo "  ✓ $2" || echo "  ✗ $2"; }++# Resolve a template name to a file path using the priority stack:+#   1. .specify/templates/overrides/+#   2. .specify/presets/<preset-id>/templates/ (sorted by priority from .registry)+#   3. .specify/extensions/<ext-id>/templates/+#   4. .specify/templates/ (core)+resolve_template() {+    local template_name="$1"+    local repo_root="$2"+    local base="$repo_root/.specify/templates"++    # Priority 1: Project overrides+    local override="$base/overrides/${template_name}.md"+    [ -f "$override" ] && echo "$override" && return 0++    # Priority 2: Installed presets (sorted by priority from .registry)+    local presets_dir="$repo_root/.specify/presets"+    if [ -d "$presets_dir" ]; then+        local registry_file="$presets_dir/.registry"+        if [ -f "$registry_file" ] && command -v python3 >/dev/null 2>&1; then+            # Read preset IDs sorted by priority (lower number = higher precedence).+            # The python3 call is wrapped in an if-condition so that set -e does not+            # abort the function when python3 exits non-zero (e.g. invalid JSON).+            local sorted_presets=""+            if sorted_presets=$(SPECKIT_REGISTRY="$registry_file" python3 -c "+import json, sys, os+try:+    with open(os.environ['SPECKIT_REGISTRY']) as f:+        data = json.load(f)+    presets = data.get('presets', {})+    for pid, meta in sorted(presets.items(), key=lambda x: x[1].get('priority', 10)):+        print(pid)+except Exception:+    sys.exit(1)+" 2>/dev/null); then+                if [ -n "$sorted_presets" ]; then+                    # python3 succeeded and returned preset IDs — search in priority order+                    while IFS= read -r preset_id; do+                        local candidate="$presets_dir/$preset_id/templates/${template_name}.md"+                        [ -f "$candidate" ] && echo "$candidate" && return 0+                    done <<< "$sorted_presets"+                fi+                # python3 succeeded but registry has no presets — nothing to search+            else+                # python3 failed (missing, or registry parse error) — fall back to unordered directory scan+                for preset in "$presets_dir"/*/; do+                    [ -d "$preset" ] || continue+                    local candidate="$preset/templates/${template_name}.md"+                    [ -f "$candidate" ] && echo "$candidate" && return 0+                done+            fi+        else+            # Fallback: alphabetical directory order (no python3 available)+            for preset in "$presets_dir"/*/; do+                [ -d "$preset" ] || continue+                local candidate="$preset/templates/${template_name}.md"+                [ -f "$candidate" ] && echo "$candidate" && return 0+            done+        fi+    fi++    # Priority 3: Extension-provided templates+    local ext_dir="$repo_root/.specify/extensions"+    if [ -d "$ext_dir" ]; then+        for ext in "$ext_dir"/*/; do+            [ -d "$ext" ] || continue+            # Skip hidden directories (e.g. .backup, .cache)+            case "$(basename "$ext")" in .*) continue;; esac+            local candidate="$ext/templates/${template_name}.md"+            [ -f "$candidate" ] && echo "$candidate" && return 0+        done+    fi++    # Priority 4: Core templates+    local core="$base/${template_name}.md"+    [ -f "$core" ] && echo "$core" && return 0++    # Template not found in any location.+    # Return 1 so callers can distinguish "not found" from "found".+    # Callers running under set -e should use: TEMPLATE=$(resolve_template ...) || true+    return 1+}
+ .specify/scripts/bash/create-new-feature.sh view
@@ -0,0 +1,335 @@+#!/usr/bin/env bash++set -e++JSON_MODE=false+SHORT_NAME=""+BRANCH_NUMBER=""+USE_TIMESTAMP=false+ARGS=()+i=1+while [ $i -le $# ]; do+    arg="${!i}"+    case "$arg" in+        --json)+            JSON_MODE=true+            ;;+        --short-name)+            if [ $((i + 1)) -gt $# ]; then+                echo 'Error: --short-name requires a value' >&2+                exit 1+            fi+            i=$((i + 1))+            next_arg="${!i}"+            # Check if the next argument is another option (starts with --)+            if [[ "$next_arg" == --* ]]; then+                echo 'Error: --short-name requires a value' >&2+                exit 1+            fi+            SHORT_NAME="$next_arg"+            ;;+        --number)+            if [ $((i + 1)) -gt $# ]; then+                echo 'Error: --number requires a value' >&2+                exit 1+            fi+            i=$((i + 1))+            next_arg="${!i}"+            if [[ "$next_arg" == --* ]]; then+                echo 'Error: --number requires a value' >&2+                exit 1+            fi+            BRANCH_NUMBER="$next_arg"+            ;;+        --timestamp)+            USE_TIMESTAMP=true+            ;;+        --help|-h)+            echo "Usage: $0 [--json] [--short-name <name>] [--number N] [--timestamp] <feature_description>"+            echo ""+            echo "Options:"+            echo "  --json              Output in JSON format"+            echo "  --short-name <name> Provide a custom short name (2-4 words) for the branch"+            echo "  --number N          Specify branch number manually (overrides auto-detection)"+            echo "  --timestamp         Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"+            echo "  --help, -h          Show this help message"+            echo ""+            echo "Examples:"+            echo "  $0 'Add user authentication system' --short-name 'user-auth'"+            echo "  $0 'Implement OAuth2 integration for API' --number 5"+            echo "  $0 --timestamp --short-name 'user-auth' 'Add user authentication'"+            exit 0+            ;;+        *)+            ARGS+=("$arg")+            ;;+    esac+    i=$((i + 1))+done++FEATURE_DESCRIPTION="${ARGS[*]}"+if [ -z "$FEATURE_DESCRIPTION" ]; then+    echo "Usage: $0 [--json] [--short-name <name>] [--number N] [--timestamp] <feature_description>" >&2+    exit 1+fi++# Trim whitespace and validate description is not empty (e.g., user passed only whitespace)+FEATURE_DESCRIPTION=$(echo "$FEATURE_DESCRIPTION" | xargs)+if [ -z "$FEATURE_DESCRIPTION" ]; then+    echo "Error: Feature description cannot be empty or contain only whitespace" >&2+    exit 1+fi++# Function to get highest number from specs directory+get_highest_from_specs() {+    local specs_dir="$1"+    local highest=0++    if [ -d "$specs_dir" ]; then+        for dir in "$specs_dir"/*; do+            [ -d "$dir" ] || continue+            dirname=$(basename "$dir")+            # Only match sequential prefixes (###-*), skip timestamp dirs+            if echo "$dirname" | grep -q '^[0-9]\{3\}-'; then+                number=$(echo "$dirname" | grep -o '^[0-9]\{3\}')+                number=$((10#$number))+                if [ "$number" -gt "$highest" ]; then+                    highest=$number+                fi+            fi+        done+    fi++    echo "$highest"+}++# Function to get highest number from git branches+get_highest_from_branches() {+    local highest=0++    # Get all branches (local and remote)+    branches=$(git branch -a 2>/dev/null || echo "")++    if [ -n "$branches" ]; then+        while IFS= read -r branch; do+            # Clean branch name: remove leading markers and remote prefixes+            clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||')++            # Extract feature number if branch matches pattern ###-*+            if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then+                number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0")+                number=$((10#$number))+                if [ "$number" -gt "$highest" ]; then+                    highest=$number+                fi+            fi+        done <<< "$branches"+    fi++    echo "$highest"+}++# Function to check existing branches (local and remote) and return next available number+check_existing_branches() {+    local specs_dir="$1"++    # Fetch all remotes to get latest branch info (suppress errors if no remotes)+    git fetch --all --prune >/dev/null 2>&1 || true++    # Get highest number from ALL branches (not just matching short name)+    local highest_branch=$(get_highest_from_branches)++    # Get highest number from ALL specs (not just matching short name)+    local highest_spec=$(get_highest_from_specs "$specs_dir")++    # Take the maximum of both+    local max_num=$highest_branch+    if [ "$highest_spec" -gt "$max_num" ]; then+        max_num=$highest_spec+    fi++    # Return next number+    echo $((max_num + 1))+}++# Function to clean and format a branch name+clean_branch_name() {+    local name="$1"+    echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'+}++# Resolve repository root using common.sh functions which prioritize .specify over git+SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"+source "$SCRIPT_DIR/common.sh"++REPO_ROOT=$(get_repo_root)++# Check if git is available at this repo root (not a parent)+if has_git; then+    HAS_GIT=true+else+    HAS_GIT=false+fi++cd "$REPO_ROOT"++SPECS_DIR="$REPO_ROOT/specs"+mkdir -p "$SPECS_DIR"++# Function to generate branch name with stop word filtering and length filtering+generate_branch_name() {+    local description="$1"++    # Common stop words to filter out+    local stop_words="^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$"++    # Convert to lowercase and split into words+    local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')++    # Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)+    local meaningful_words=()+    for word in $clean_name; do+        # Skip empty words+        [ -z "$word" ] && continue++        # Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms)+        if ! echo "$word" | grep -qiE "$stop_words"; then+            if [ ${#word} -ge 3 ]; then+                meaningful_words+=("$word")+            elif echo "$description" | grep -q "\b${word^^}\b"; then+                # Keep short words if they appear as uppercase in original (likely acronyms)+                meaningful_words+=("$word")+            fi+        fi+    done++    # If we have meaningful words, use first 3-4 of them+    if [ ${#meaningful_words[@]} -gt 0 ]; then+        local max_words=3+        if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi++        local result=""+        local count=0+        for word in "${meaningful_words[@]}"; do+            if [ $count -ge $max_words ]; then break; fi+            if [ -n "$result" ]; then result="$result-"; fi+            result="$result$word"+            count=$((count + 1))+        done+        echo "$result"+    else+        # Fallback to original logic if no meaningful words found+        local cleaned=$(clean_branch_name "$description")+        echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'+    fi+}++# Generate branch name+if [ -n "$SHORT_NAME" ]; then+    # Use provided short name, just clean it up+    BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME")+else+    # Generate from description with smart filtering+    BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")+fi++# Warn if --number and --timestamp are both specified+if [ "$USE_TIMESTAMP" = true ] && [ -n "$BRANCH_NUMBER" ]; then+    >&2 echo "[specify] Warning: --number is ignored when --timestamp is used"+    BRANCH_NUMBER=""+fi++# Determine branch prefix+if [ "$USE_TIMESTAMP" = true ]; then+    FEATURE_NUM=$(date +%Y%m%d-%H%M%S)+    BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"+else+    # Determine branch number+    if [ -z "$BRANCH_NUMBER" ]; then+        if [ "$HAS_GIT" = true ]; then+            # Check existing branches on remotes+            BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")+        else+            # Fall back to local directory check+            HIGHEST=$(get_highest_from_specs "$SPECS_DIR")+            BRANCH_NUMBER=$((HIGHEST + 1))+        fi+    fi++    # Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)+    FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")+    BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"+fi++# GitHub enforces a 244-byte limit on branch names+# Validate and truncate if necessary+MAX_BRANCH_LENGTH=244+if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then+    # Calculate how much we need to trim from suffix+    # Account for prefix length: timestamp (15) + hyphen (1) = 16, or sequential (3) + hyphen (1) = 4+    PREFIX_LENGTH=$(( ${#FEATURE_NUM} + 1 ))+    MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - PREFIX_LENGTH))++    # Truncate suffix at word boundary if possible+    TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)+    # Remove trailing hyphen if truncation created one+    TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//')++    ORIGINAL_BRANCH_NAME="$BRANCH_NAME"+    BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}"++    >&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit"+    >&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"+    >&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"+fi++if [ "$HAS_GIT" = true ]; then+    if ! git checkout -b "$BRANCH_NAME" 2>/dev/null; then+        # Check if branch already exists+        if git branch --list "$BRANCH_NAME" | grep -q .; then+            if [ "$USE_TIMESTAMP" = true ]; then+                >&2 echo "Error: Branch '$BRANCH_NAME' already exists. Rerun to get a new timestamp or use a different --short-name."+            else+                >&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number."+            fi+            exit 1+        else+            >&2 echo "Error: Failed to create git branch '$BRANCH_NAME'. Please check your git configuration and try again."+            exit 1+        fi+    fi+else+    >&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"+fi++FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"+mkdir -p "$FEATURE_DIR"++TEMPLATE=$(resolve_template "spec-template" "$REPO_ROOT") || true+SPEC_FILE="$FEATURE_DIR/spec.md"+if [ -n "$TEMPLATE" ] && [ -f "$TEMPLATE" ]; then+    cp "$TEMPLATE" "$SPEC_FILE"+else+    echo "Warning: Spec template not found; created empty spec file" >&2+    touch "$SPEC_FILE"+fi++# Inform the user how to persist the feature variable in their own shell+printf '# To persist: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" >&2++if $JSON_MODE; then+    if command -v jq >/dev/null 2>&1; then+        jq -cn \+            --arg branch_name "$BRANCH_NAME" \+            --arg spec_file "$SPEC_FILE" \+            --arg feature_num "$FEATURE_NUM" \+            '{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num}'+    else+        printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")"+    fi+else+    echo "BRANCH_NAME: $BRANCH_NAME"+    echo "SPEC_FILE: $SPEC_FILE"+    echo "FEATURE_NUM: $FEATURE_NUM"+    printf '# To persist in your shell: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME"+fi
+ .specify/scripts/bash/setup-plan.sh view
@@ -0,0 +1,72 @@+#!/usr/bin/env bash++set -e++# Parse command line arguments+JSON_MODE=false+ARGS=()++for arg in "$@"; do+    case "$arg" in+        --json)+            JSON_MODE=true+            ;;+        --help|-h)+            echo "Usage: $0 [--json]"+            echo "  --json    Output results in JSON format"+            echo "  --help    Show this help message"+            exit 0+            ;;+        *)+            ARGS+=("$arg")+            ;;+    esac+done++# Get script directory and load common functions+SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"+source "$SCRIPT_DIR/common.sh"++# Get all paths and variables from common functions+_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }+eval "$_paths_output"+unset _paths_output++# Check if we're on a proper feature branch (only for git repos)+check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1++# Ensure the feature directory exists+mkdir -p "$FEATURE_DIR"++# Copy plan template if it exists+TEMPLATE=$(resolve_template "plan-template" "$REPO_ROOT") || true+if [[ -n "$TEMPLATE" ]] && [[ -f "$TEMPLATE" ]]; then+    cp "$TEMPLATE" "$IMPL_PLAN"+    echo "Copied plan template to $IMPL_PLAN"+else+    echo "Warning: Plan template not found"+    # Create a basic plan file if template doesn't exist+    touch "$IMPL_PLAN"+fi++# Output results+if $JSON_MODE; then+    if has_jq; then+        jq -cn \+            --arg feature_spec "$FEATURE_SPEC" \+            --arg impl_plan "$IMPL_PLAN" \+            --arg specs_dir "$FEATURE_DIR" \+            --arg branch "$CURRENT_BRANCH" \+            --arg has_git "$HAS_GIT" \+            '{FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,SPECS_DIR:$specs_dir,BRANCH:$branch,HAS_GIT:$has_git}'+    else+        printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \+            "$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$HAS_GIT")"+    fi+else+    echo "FEATURE_SPEC: $FEATURE_SPEC"+    echo "IMPL_PLAN: $IMPL_PLAN"+    echo "SPECS_DIR: $FEATURE_DIR"+    echo "BRANCH: $CURRENT_BRANCH"+    echo "HAS_GIT: $HAS_GIT"+fi
+ .specify/scripts/bash/update-agent-context.sh view
@@ -0,0 +1,837 @@+#!/usr/bin/env bash++# Update agent context files with information from plan.md+#+# This script maintains AI agent context files by parsing feature specifications+# and updating agent-specific configuration files with project information.+#+# MAIN FUNCTIONS:+# 1. Environment Validation+#    - Verifies git repository structure and branch information+#    - Checks for required plan.md files and templates+#    - Validates file permissions and accessibility+#+# 2. Plan Data Extraction+#    - Parses plan.md files to extract project metadata+#    - Identifies language/version, frameworks, databases, and project types+#    - Handles missing or incomplete specification data gracefully+#+# 3. Agent File Management+#    - Creates new agent context files from templates when needed+#    - Updates existing agent files with new project information+#    - Preserves manual additions and custom configurations+#    - Supports multiple AI agent formats and directory structures+#+# 4. Content Generation+#    - Generates language-specific build/test commands+#    - Creates appropriate project directory structures+#    - Updates technology stacks and recent changes sections+#    - Maintains consistent formatting and timestamps+#+# 5. Multi-Agent Support+#    - Handles agent-specific file paths and naming conventions+#    - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Junie, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Tabnine CLI, Kiro CLI, Mistral Vibe, Kimi Code, Pi Coding Agent, iFlow CLI, Antigravity or Generic+#    - Can update single agents or all existing agent files+#    - Creates default Claude file if no agent files exist+#+# Usage: ./update-agent-context.sh [agent_type]+# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic+# Leave empty to update all existing agent files++set -e++# Enable strict error handling+set -u+set -o pipefail++#==============================================================================+# Configuration and Global Variables+#==============================================================================++# Get script directory and load common functions+SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"+source "$SCRIPT_DIR/common.sh"++# Get all paths and variables from common functions+_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }+eval "$_paths_output"+unset _paths_output++NEW_PLAN="$IMPL_PLAN"  # Alias for compatibility with existing code+AGENT_TYPE="${1:-}"++# Agent-specific file paths+CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"+GEMINI_FILE="$REPO_ROOT/GEMINI.md"+COPILOT_FILE="$REPO_ROOT/.github/agents/copilot-instructions.md"+CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc"+QWEN_FILE="$REPO_ROOT/QWEN.md"+AGENTS_FILE="$REPO_ROOT/AGENTS.md"+WINDSURF_FILE="$REPO_ROOT/.windsurf/rules/specify-rules.md"+JUNIE_FILE="$REPO_ROOT/.junie/AGENTS.md"+KILOCODE_FILE="$REPO_ROOT/.kilocode/rules/specify-rules.md"+AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md"+ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md"+CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md"+QODER_FILE="$REPO_ROOT/QODER.md"+# Amp, Kiro CLI, IBM Bob, and Pi all share AGENTS.md — use AGENTS_FILE to avoid+# updating the same file multiple times.+AMP_FILE="$AGENTS_FILE"+SHAI_FILE="$REPO_ROOT/SHAI.md"+TABNINE_FILE="$REPO_ROOT/TABNINE.md"+KIRO_FILE="$AGENTS_FILE"+AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md"+BOB_FILE="$AGENTS_FILE"+VIBE_FILE="$REPO_ROOT/.vibe/agents/specify-agents.md"+KIMI_FILE="$REPO_ROOT/KIMI.md"+TRAE_FILE="$REPO_ROOT/.trae/rules/AGENTS.md"+IFLOW_FILE="$REPO_ROOT/IFLOW.md"++# Template file+TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md"++# Global variables for parsed plan data+NEW_LANG=""+NEW_FRAMEWORK=""+NEW_DB=""+NEW_PROJECT_TYPE=""++#==============================================================================+# Utility Functions+#==============================================================================++log_info() {+    echo "INFO: $1"+}++log_success() {+    echo "✓ $1"+}++log_error() {+    echo "ERROR: $1" >&2+}++log_warning() {+    echo "WARNING: $1" >&2+}++# Cleanup function for temporary files+cleanup() {+    local exit_code=$?+    # Disarm traps to prevent re-entrant loop+    trap - EXIT INT TERM+    rm -f /tmp/agent_update_*_$$+    rm -f /tmp/manual_additions_$$+    exit $exit_code+}++# Set up cleanup trap+trap cleanup EXIT INT TERM++#==============================================================================+# Validation Functions+#==============================================================================++validate_environment() {+    # Check if we have a current branch/feature (git or non-git)+    if [[ -z "$CURRENT_BRANCH" ]]; then+        log_error "Unable to determine current feature"+        if [[ "$HAS_GIT" == "true" ]]; then+            log_info "Make sure you're on a feature branch"+        else+            log_info "Set SPECIFY_FEATURE environment variable or create a feature first"+        fi+        exit 1+    fi++    # Check if plan.md exists+    if [[ ! -f "$NEW_PLAN" ]]; then+        log_error "No plan.md found at $NEW_PLAN"+        log_info "Make sure you're working on a feature with a corresponding spec directory"+        if [[ "$HAS_GIT" != "true" ]]; then+            log_info "Use: export SPECIFY_FEATURE=your-feature-name or create a new feature first"+        fi+        exit 1+    fi++    # Check if template exists (needed for new files)+    if [[ ! -f "$TEMPLATE_FILE" ]]; then+        log_warning "Template file not found at $TEMPLATE_FILE"+        log_warning "Creating new agent files will fail"+    fi+}++#==============================================================================+# Plan Parsing Functions+#==============================================================================++extract_plan_field() {+    local field_pattern="$1"+    local plan_file="$2"++    grep "^\*\*${field_pattern}\*\*: " "$plan_file" 2>/dev/null | \+        head -1 | \+        sed "s|^\*\*${field_pattern}\*\*: ||" | \+        sed 's/^[ \t]*//;s/[ \t]*$//' | \+        grep -v "NEEDS CLARIFICATION" | \+        grep -v "^N/A$" || echo ""+}++parse_plan_data() {+    local plan_file="$1"++    if [[ ! -f "$plan_file" ]]; then+        log_error "Plan file not found: $plan_file"+        return 1+    fi++    if [[ ! -r "$plan_file" ]]; then+        log_error "Plan file is not readable: $plan_file"+        return 1+    fi++    log_info "Parsing plan data from $plan_file"++    NEW_LANG=$(extract_plan_field "Language/Version" "$plan_file")+    NEW_FRAMEWORK=$(extract_plan_field "Primary Dependencies" "$plan_file")+    NEW_DB=$(extract_plan_field "Storage" "$plan_file")+    NEW_PROJECT_TYPE=$(extract_plan_field "Project Type" "$plan_file")++    # Log what we found+    if [[ -n "$NEW_LANG" ]]; then+        log_info "Found language: $NEW_LANG"+    else+        log_warning "No language information found in plan"+    fi++    if [[ -n "$NEW_FRAMEWORK" ]]; then+        log_info "Found framework: $NEW_FRAMEWORK"+    fi++    if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then+        log_info "Found database: $NEW_DB"+    fi++    if [[ -n "$NEW_PROJECT_TYPE" ]]; then+        log_info "Found project type: $NEW_PROJECT_TYPE"+    fi+}++format_technology_stack() {+    local lang="$1"+    local framework="$2"+    local parts=()++    # Add non-empty parts+    [[ -n "$lang" && "$lang" != "NEEDS CLARIFICATION" ]] && parts+=("$lang")+    [[ -n "$framework" && "$framework" != "NEEDS CLARIFICATION" && "$framework" != "N/A" ]] && parts+=("$framework")++    # Join with proper formatting+    if [[ ${#parts[@]} -eq 0 ]]; then+        echo ""+    elif [[ ${#parts[@]} -eq 1 ]]; then+        echo "${parts[0]}"+    else+        # Join multiple parts with " + "+        local result="${parts[0]}"+        for ((i=1; i<${#parts[@]}; i++)); do+            result="$result + ${parts[i]}"+        done+        echo "$result"+    fi+}++#==============================================================================+# Template and Content Generation Functions+#==============================================================================++get_project_structure() {+    local project_type="$1"++    if [[ "$project_type" == *"web"* ]]; then+        echo "backend/\\nfrontend/\\ntests/"+    else+        echo "src/\\ntests/"+    fi+}++get_commands_for_language() {+    local lang="$1"++    case "$lang" in+        *"Python"*)+            echo "cd src && pytest && ruff check ."+            ;;+        *"Rust"*)+            echo "cargo test && cargo clippy"+            ;;+        *"JavaScript"*|*"TypeScript"*)+            echo "npm test \\&\\& npm run lint"+            ;;+        *)+            echo "# Add commands for $lang"+            ;;+    esac+}++get_language_conventions() {+    local lang="$1"+    echo "$lang: Follow standard conventions"+}++create_new_agent_file() {+    local target_file="$1"+    local temp_file="$2"+    local project_name="$3"+    local current_date="$4"++    if [[ ! -f "$TEMPLATE_FILE" ]]; then+        log_error "Template not found at $TEMPLATE_FILE"+        return 1+    fi++    if [[ ! -r "$TEMPLATE_FILE" ]]; then+        log_error "Template file is not readable: $TEMPLATE_FILE"+        return 1+    fi++    log_info "Creating new agent context file from template..."++    if ! cp "$TEMPLATE_FILE" "$temp_file"; then+        log_error "Failed to copy template file"+        return 1+    fi++    # Replace template placeholders+    local project_structure+    project_structure=$(get_project_structure "$NEW_PROJECT_TYPE")++    local commands+    commands=$(get_commands_for_language "$NEW_LANG")++    local language_conventions+    language_conventions=$(get_language_conventions "$NEW_LANG")++    # Perform substitutions with error checking using safer approach+    # Escape special characters for sed by using a different delimiter or escaping+    local escaped_lang=$(printf '%s\n' "$NEW_LANG" | sed 's/[\[\.*^$()+{}|]/\\&/g')+    local escaped_framework=$(printf '%s\n' "$NEW_FRAMEWORK" | sed 's/[\[\.*^$()+{}|]/\\&/g')+    local escaped_branch=$(printf '%s\n' "$CURRENT_BRANCH" | sed 's/[\[\.*^$()+{}|]/\\&/g')++    # Build technology stack and recent change strings conditionally+    local tech_stack+    if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then+        tech_stack="- $escaped_lang + $escaped_framework ($escaped_branch)"+    elif [[ -n "$escaped_lang" ]]; then+        tech_stack="- $escaped_lang ($escaped_branch)"+    elif [[ -n "$escaped_framework" ]]; then+        tech_stack="- $escaped_framework ($escaped_branch)"+    else+        tech_stack="- ($escaped_branch)"+    fi++    local recent_change+    if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then+        recent_change="- $escaped_branch: Added $escaped_lang + $escaped_framework"+    elif [[ -n "$escaped_lang" ]]; then+        recent_change="- $escaped_branch: Added $escaped_lang"+    elif [[ -n "$escaped_framework" ]]; then+        recent_change="- $escaped_branch: Added $escaped_framework"+    else+        recent_change="- $escaped_branch: Added"+    fi++    local substitutions=(+        "s|\[PROJECT NAME\]|$project_name|"+        "s|\[DATE\]|$current_date|"+        "s|\[EXTRACTED FROM ALL PLAN.MD FILES\]|$tech_stack|"+        "s|\[ACTUAL STRUCTURE FROM PLANS\]|$project_structure|g"+        "s|\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]|$commands|"+        "s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$language_conventions|"+        "s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|$recent_change|"+    )++    for substitution in "${substitutions[@]}"; do+        if ! sed -i.bak -e "$substitution" "$temp_file"; then+            log_error "Failed to perform substitution: $substitution"+            rm -f "$temp_file" "$temp_file.bak"+            return 1+        fi+    done++    # Convert \n sequences to actual newlines+    newline=$(printf '\n')+    sed -i.bak2 "s/\\\\n/${newline}/g" "$temp_file"++    # Clean up backup files+    rm -f "$temp_file.bak" "$temp_file.bak2"++    # Prepend Cursor frontmatter for .mdc files so rules are auto-included+    if [[ "$target_file" == *.mdc ]]; then+        local frontmatter_file+        frontmatter_file=$(mktemp) || return 1+        printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file"+        cat "$temp_file" >> "$frontmatter_file"+        mv "$frontmatter_file" "$temp_file"+    fi++    return 0+}+++++update_existing_agent_file() {+    local target_file="$1"+    local current_date="$2"++    log_info "Updating existing agent context file..."++    # Use a single temporary file for atomic update+    local temp_file+    temp_file=$(mktemp) || {+        log_error "Failed to create temporary file"+        return 1+    }++    # Process the file in one pass+    local tech_stack=$(format_technology_stack "$NEW_LANG" "$NEW_FRAMEWORK")+    local new_tech_entries=()+    local new_change_entry=""++    # Prepare new technology entries+    if [[ -n "$tech_stack" ]] && ! grep -q "$tech_stack" "$target_file"; then+        new_tech_entries+=("- $tech_stack ($CURRENT_BRANCH)")+    fi++    if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]] && ! grep -q "$NEW_DB" "$target_file"; then+        new_tech_entries+=("- $NEW_DB ($CURRENT_BRANCH)")+    fi++    # Prepare new change entry+    if [[ -n "$tech_stack" ]]; then+        new_change_entry="- $CURRENT_BRANCH: Added $tech_stack"+    elif [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]]; then+        new_change_entry="- $CURRENT_BRANCH: Added $NEW_DB"+    fi++    # Check if sections exist in the file+    local has_active_technologies=0+    local has_recent_changes=0++    if grep -q "^## Active Technologies" "$target_file" 2>/dev/null; then+        has_active_technologies=1+    fi++    if grep -q "^## Recent Changes" "$target_file" 2>/dev/null; then+        has_recent_changes=1+    fi++    # Process file line by line+    local in_tech_section=false+    local in_changes_section=false+    local tech_entries_added=false+    local changes_entries_added=false+    local existing_changes_count=0+    local file_ended=false++    while IFS= read -r line || [[ -n "$line" ]]; do+        # Handle Active Technologies section+        if [[ "$line" == "## Active Technologies" ]]; then+            echo "$line" >> "$temp_file"+            in_tech_section=true+            continue+        elif [[ $in_tech_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then+            # Add new tech entries before closing the section+            if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then+                printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"+                tech_entries_added=true+            fi+            echo "$line" >> "$temp_file"+            in_tech_section=false+            continue+        elif [[ $in_tech_section == true ]] && [[ -z "$line" ]]; then+            # Add new tech entries before empty line in tech section+            if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then+                printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"+                tech_entries_added=true+            fi+            echo "$line" >> "$temp_file"+            continue+        fi++        # Handle Recent Changes section+        if [[ "$line" == "## Recent Changes" ]]; then+            echo "$line" >> "$temp_file"+            # Add new change entry right after the heading+            if [[ -n "$new_change_entry" ]]; then+                echo "$new_change_entry" >> "$temp_file"+            fi+            in_changes_section=true+            changes_entries_added=true+            continue+        elif [[ $in_changes_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then+            echo "$line" >> "$temp_file"+            in_changes_section=false+            continue+        elif [[ $in_changes_section == true ]] && [[ "$line" == "- "* ]]; then+            # Keep only first 2 existing changes+            if [[ $existing_changes_count -lt 2 ]]; then+                echo "$line" >> "$temp_file"+                ((existing_changes_count++))+            fi+            continue+        fi++        # Update timestamp+        if [[ "$line" =~ (\*\*)?Last\ updated(\*\*)?:.*[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then+            echo "$line" | sed "s/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/$current_date/" >> "$temp_file"+        else+            echo "$line" >> "$temp_file"+        fi+    done < "$target_file"++    # Post-loop check: if we're still in the Active Technologies section and haven't added new entries+    if [[ $in_tech_section == true ]] && [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then+        printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"+        tech_entries_added=true+    fi++    # If sections don't exist, add them at the end of the file+    if [[ $has_active_technologies -eq 0 ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then+        echo "" >> "$temp_file"+        echo "## Active Technologies" >> "$temp_file"+        printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"+        tech_entries_added=true+    fi++    if [[ $has_recent_changes -eq 0 ]] && [[ -n "$new_change_entry" ]]; then+        echo "" >> "$temp_file"+        echo "## Recent Changes" >> "$temp_file"+        echo "$new_change_entry" >> "$temp_file"+        changes_entries_added=true+    fi++    # Ensure Cursor .mdc files have YAML frontmatter for auto-inclusion+    if [[ "$target_file" == *.mdc ]]; then+        if ! head -1 "$temp_file" | grep -q '^---'; then+            local frontmatter_file+            frontmatter_file=$(mktemp) || { rm -f "$temp_file"; return 1; }+            printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file"+            cat "$temp_file" >> "$frontmatter_file"+            mv "$frontmatter_file" "$temp_file"+        fi+    fi++    # Move temp file to target atomically+    if ! mv "$temp_file" "$target_file"; then+        log_error "Failed to update target file"+        rm -f "$temp_file"+        return 1+    fi++    return 0+}+#==============================================================================+# Main Agent File Update Function+#==============================================================================++update_agent_file() {+    local target_file="$1"+    local agent_name="$2"++    if [[ -z "$target_file" ]] || [[ -z "$agent_name" ]]; then+        log_error "update_agent_file requires target_file and agent_name parameters"+        return 1+    fi++    log_info "Updating $agent_name context file: $target_file"++    local project_name+    project_name=$(basename "$REPO_ROOT")+    local current_date+    current_date=$(date +%Y-%m-%d)++    # Create directory if it doesn't exist+    local target_dir+    target_dir=$(dirname "$target_file")+    if [[ ! -d "$target_dir" ]]; then+        if ! mkdir -p "$target_dir"; then+            log_error "Failed to create directory: $target_dir"+            return 1+        fi+    fi++    if [[ ! -f "$target_file" ]]; then+        # Create new file from template+        local temp_file+        temp_file=$(mktemp) || {+            log_error "Failed to create temporary file"+            return 1+        }++        if create_new_agent_file "$target_file" "$temp_file" "$project_name" "$current_date"; then+            if mv "$temp_file" "$target_file"; then+                log_success "Created new $agent_name context file"+            else+                log_error "Failed to move temporary file to $target_file"+                rm -f "$temp_file"+                return 1+            fi+        else+            log_error "Failed to create new agent file"+            rm -f "$temp_file"+            return 1+        fi+    else+        # Update existing file+        if [[ ! -r "$target_file" ]]; then+            log_error "Cannot read existing file: $target_file"+            return 1+        fi++        if [[ ! -w "$target_file" ]]; then+            log_error "Cannot write to existing file: $target_file"+            return 1+        fi++        if update_existing_agent_file "$target_file" "$current_date"; then+            log_success "Updated existing $agent_name context file"+        else+            log_error "Failed to update existing agent file"+            return 1+        fi+    fi++    return 0+}++#==============================================================================+# Agent Selection and Processing+#==============================================================================++update_specific_agent() {+    local agent_type="$1"++    case "$agent_type" in+        claude)+            update_agent_file "$CLAUDE_FILE" "Claude Code" || return 1+            ;;+        gemini)+            update_agent_file "$GEMINI_FILE" "Gemini CLI" || return 1+            ;;+        copilot)+            update_agent_file "$COPILOT_FILE" "GitHub Copilot" || return 1+            ;;+        cursor-agent)+            update_agent_file "$CURSOR_FILE" "Cursor IDE" || return 1+            ;;+        qwen)+            update_agent_file "$QWEN_FILE" "Qwen Code" || return 1+            ;;+        opencode)+            update_agent_file "$AGENTS_FILE" "opencode" || return 1+            ;;+        codex)+            update_agent_file "$AGENTS_FILE" "Codex CLI" || return 1+            ;;+        windsurf)+            update_agent_file "$WINDSURF_FILE" "Windsurf" || return 1+            ;;+        junie)+            update_agent_file "$JUNIE_FILE" "Junie" || return 1+            ;;+        kilocode)+            update_agent_file "$KILOCODE_FILE" "Kilo Code" || return 1+            ;;+        auggie)+            update_agent_file "$AUGGIE_FILE" "Auggie CLI" || return 1+            ;;+        roo)+            update_agent_file "$ROO_FILE" "Roo Code" || return 1+            ;;+        codebuddy)+            update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" || return 1+            ;;+        qodercli)+            update_agent_file "$QODER_FILE" "Qoder CLI" || return 1+            ;;+        amp)+            update_agent_file "$AMP_FILE" "Amp" || return 1+            ;;+        shai)+            update_agent_file "$SHAI_FILE" "SHAI" || return 1+            ;;+        tabnine)+            update_agent_file "$TABNINE_FILE" "Tabnine CLI" || return 1+            ;;+        kiro-cli)+            update_agent_file "$KIRO_FILE" "Kiro CLI" || return 1+            ;;+        agy)+            update_agent_file "$AGY_FILE" "Antigravity" || return 1+            ;;+        bob)+            update_agent_file "$BOB_FILE" "IBM Bob" || return 1+            ;;+        vibe)+            update_agent_file "$VIBE_FILE" "Mistral Vibe" || return 1+            ;;+        kimi)+            update_agent_file "$KIMI_FILE" "Kimi Code" || return 1+            ;;+        trae)+            update_agent_file "$TRAE_FILE" "Trae" || return 1+            ;;+        pi)+            update_agent_file "$AGENTS_FILE" "Pi Coding Agent" || return 1+            ;;+        iflow)+            update_agent_file "$IFLOW_FILE" "iFlow CLI" || return 1+            ;;+        generic)+            log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent."+            ;;+        *)+            log_error "Unknown agent type '$agent_type'"+            log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic"+            exit 1+            ;;+    esac+}++# Helper: skip non-existent files and files already updated (dedup by+# realpath so that variables pointing to the same file — e.g. AMP_FILE,+# KIRO_FILE, BOB_FILE all resolving to AGENTS_FILE — are only written once).+# Uses a linear array instead of associative array for bash 3.2 compatibility.+# Note: defined at top level because bash 3.2 does not support true+# nested/local functions. _updated_paths, _found_agent, and _all_ok are+# initialised exclusively inside update_all_existing_agents so that+# sourcing this script has no side effects on the caller's environment.++_update_if_new() {+    local file="$1" name="$2"+    [[ -f "$file" ]] || return 0+    local real_path+    real_path=$(realpath "$file" 2>/dev/null || echo "$file")+    local p+    if [[ ${#_updated_paths[@]} -gt 0 ]]; then+        for p in "${_updated_paths[@]}"; do+            [[ "$p" == "$real_path" ]] && return 0+        done+    fi+    # Record the file as seen before attempting the update so that:+    # (a) aliases pointing to the same path are not retried on failure+    # (b) _found_agent reflects file existence, not update success+    _updated_paths+=("$real_path")+    _found_agent=true+    update_agent_file "$file" "$name"+}++update_all_existing_agents() {+    _found_agent=false+    _updated_paths=()+    local _all_ok=true++    _update_if_new "$CLAUDE_FILE" "Claude Code"           || _all_ok=false+    _update_if_new "$GEMINI_FILE" "Gemini CLI"             || _all_ok=false+    _update_if_new "$COPILOT_FILE" "GitHub Copilot"        || _all_ok=false+    _update_if_new "$CURSOR_FILE" "Cursor IDE"             || _all_ok=false+    _update_if_new "$QWEN_FILE" "Qwen Code"                || _all_ok=false+    _update_if_new "$AGENTS_FILE" "Codex/opencode"         || _all_ok=false+    _update_if_new "$AMP_FILE" "Amp"                       || _all_ok=false+    _update_if_new "$KIRO_FILE" "Kiro CLI"                 || _all_ok=false+    _update_if_new "$BOB_FILE" "IBM Bob"                   || _all_ok=false+    _update_if_new "$WINDSURF_FILE" "Windsurf"             || _all_ok=false+    _update_if_new "$JUNIE_FILE" "Junie"                || _all_ok=false+    _update_if_new "$KILOCODE_FILE" "Kilo Code"            || _all_ok=false+    _update_if_new "$AUGGIE_FILE" "Auggie CLI"             || _all_ok=false+    _update_if_new "$ROO_FILE" "Roo Code"                  || _all_ok=false+    _update_if_new "$CODEBUDDY_FILE" "CodeBuddy CLI"       || _all_ok=false+    _update_if_new "$SHAI_FILE" "SHAI"                     || _all_ok=false+    _update_if_new "$TABNINE_FILE" "Tabnine CLI"           || _all_ok=false+    _update_if_new "$QODER_FILE" "Qoder CLI"               || _all_ok=false+    _update_if_new "$AGY_FILE" "Antigravity"               || _all_ok=false+    _update_if_new "$VIBE_FILE" "Mistral Vibe"             || _all_ok=false+    _update_if_new "$KIMI_FILE" "Kimi Code"                || _all_ok=false+    _update_if_new "$TRAE_FILE" "Trae"                     || _all_ok=false+    _update_if_new "$IFLOW_FILE" "iFlow CLI"               || _all_ok=false++    # If no agent files exist, create a default Claude file+    if [[ "$_found_agent" == false ]]; then+        log_info "No existing agent files found, creating default Claude file..."+        update_agent_file "$CLAUDE_FILE" "Claude Code" || return 1+    fi++    [[ "$_all_ok" == true ]]+}+print_summary() {+    echo+    log_info "Summary of changes:"++    if [[ -n "$NEW_LANG" ]]; then+        echo "  - Added language: $NEW_LANG"+    fi++    if [[ -n "$NEW_FRAMEWORK" ]]; then+        echo "  - Added framework: $NEW_FRAMEWORK"+    fi++    if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then+        echo "  - Added database: $NEW_DB"+    fi++    echo+    log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic]"+}++#==============================================================================+# Main Execution+#==============================================================================++main() {+    # Validate environment before proceeding+    validate_environment++    log_info "=== Updating agent context files for feature $CURRENT_BRANCH ==="++    # Parse the plan file to extract project information+    if ! parse_plan_data "$NEW_PLAN"; then+        log_error "Failed to parse plan data"+        exit 1+    fi++    # Process based on agent type argument+    local success=true++    if [[ -z "$AGENT_TYPE" ]]; then+        # No specific agent provided - update all existing agent files+        log_info "No agent specified, updating all existing agent files..."+        if ! update_all_existing_agents; then+            success=false+        fi+    else+        # Specific agent provided - update only that agent+        log_info "Updating specific agent: $AGENT_TYPE"+        if ! update_specific_agent "$AGENT_TYPE"; then+            success=false+        fi+    fi++    # Print summary+    print_summary++    if [[ "$success" == true ]]; then+        log_success "Agent context update completed successfully"+        exit 0+    else+        log_error "Agent context update completed with errors"+        exit 1+    fi+}++# Execute main function if script is run directly+if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then+    main "$@"+fi
+ .specify/templates/agent-file-template.md view
@@ -0,0 +1,28 @@+# [PROJECT NAME] Development Guidelines++Auto-generated from all feature plans. Last updated: [DATE]++## Active Technologies++[EXTRACTED FROM ALL PLAN.MD FILES]++## Project Structure++```text+[ACTUAL STRUCTURE FROM PLANS]+```++## Commands++[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES]++## Code Style++[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE]++## Recent Changes++[LAST 3 FEATURES AND WHAT THEY ADDED]++<!-- MANUAL ADDITIONS START -->+<!-- MANUAL ADDITIONS END -->
+ .specify/templates/checklist-template.md view
@@ -0,0 +1,40 @@+# [CHECKLIST TYPE] Checklist: [FEATURE NAME]++**Purpose**: [Brief description of what this checklist covers]+**Created**: [DATE]+**Feature**: [Link to spec.md or relevant documentation]++**Note**: This checklist is generated by the `/speckit.checklist` command based on feature context and requirements.++<!--+  ============================================================================+  IMPORTANT: The checklist items below are SAMPLE ITEMS for illustration only.++  The /speckit.checklist command MUST replace these with actual items based on:+  - User's specific checklist request+  - Feature requirements from spec.md+  - Technical context from plan.md+  - Implementation details from tasks.md++  DO NOT keep these sample items in the generated checklist file.+  ============================================================================+-->++## [Category 1]++- [ ] CHK001 First checklist item with clear action+- [ ] CHK002 Second checklist item+- [ ] CHK003 Third checklist item++## [Category 2]++- [ ] CHK004 Another category item+- [ ] CHK005 Item with specific criteria+- [ ] CHK006 Final item in this category++## Notes++- Check items off as completed: `[x]`+- Add comments or findings inline+- Link to relevant resources or documentation+- Items are numbered sequentially for easy reference
+ .specify/templates/constitution-template.md view
@@ -0,0 +1,50 @@+# [PROJECT_NAME] Constitution+<!-- Example: Spec Constitution, TaskFlow Constitution, etc. -->++## Core Principles++### [PRINCIPLE_1_NAME]+<!-- Example: I. Library-First -->+[PRINCIPLE_1_DESCRIPTION]+<!-- Example: Every feature starts as a standalone library; Libraries must be self-contained, independently testable, documented; Clear purpose required - no organizational-only libraries -->++### [PRINCIPLE_2_NAME]+<!-- Example: II. CLI Interface -->+[PRINCIPLE_2_DESCRIPTION]+<!-- Example: Every library exposes functionality via CLI; Text in/out protocol: stdin/args → stdout, errors → stderr; Support JSON + human-readable formats -->++### [PRINCIPLE_3_NAME]+<!-- Example: III. Test-First (NON-NEGOTIABLE) -->+[PRINCIPLE_3_DESCRIPTION]+<!-- Example: TDD mandatory: Tests written → User approved → Tests fail → Then implement; Red-Green-Refactor cycle strictly enforced -->++### [PRINCIPLE_4_NAME]+<!-- Example: IV. Integration Testing -->+[PRINCIPLE_4_DESCRIPTION]+<!-- Example: Focus areas requiring integration tests: New library contract tests, Contract changes, Inter-service communication, Shared schemas -->++### [PRINCIPLE_5_NAME]+<!-- Example: V. Observability, VI. Versioning & Breaking Changes, VII. Simplicity -->+[PRINCIPLE_5_DESCRIPTION]+<!-- Example: Text I/O ensures debuggability; Structured logging required; Or: MAJOR.MINOR.BUILD format; Or: Start simple, YAGNI principles -->++## [SECTION_2_NAME]+<!-- Example: Additional Constraints, Security Requirements, Performance Standards, etc. -->++[SECTION_2_CONTENT]+<!-- Example: Technology stack requirements, compliance standards, deployment policies, etc. -->++## [SECTION_3_NAME]+<!-- Example: Development Workflow, Review Process, Quality Gates, etc. -->++[SECTION_3_CONTENT]+<!-- Example: Code review requirements, testing gates, deployment approval process, etc. -->++## Governance+<!-- Example: Constitution supersedes all other practices; Amendments require documentation, approval, migration plan -->++[GOVERNANCE_RULES]+<!-- Example: All PRs/reviews must verify compliance; Complexity must be justified; Use [GUIDANCE_FILE] for runtime development guidance -->++**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE]+<!-- Example: Version: 2.1.1 | Ratified: 2025-06-13 | Last Amended: 2025-07-16 -->
+ .specify/templates/plan-template.md view
@@ -0,0 +1,104 @@+# Implementation Plan: [FEATURE]++**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]+**Input**: Feature specification from `/specs/[###-feature-name]/spec.md`++**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/plan-template.md` for the execution workflow.++## Summary++[Extract from feature spec: primary requirement + technical approach from research]++## Technical Context++<!--+  ACTION REQUIRED: Replace the content in this section with the technical details+  for the project. The structure here is presented in advisory capacity to guide+  the iteration process.+-->++**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION]+**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION]+**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A]+**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION]+**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION]+**Project Type**: [e.g., library/cli/web-service/mobile-app/compiler/desktop-app or NEEDS CLARIFICATION]+**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION]+**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION]+**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION]++## Constitution Check++*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*++[Gates determined based on constitution file]++## Project Structure++### Documentation (this feature)++```text+specs/[###-feature]/+├── plan.md              # This file (/speckit.plan command output)+├── research.md          # Phase 0 output (/speckit.plan command)+├── data-model.md        # Phase 1 output (/speckit.plan command)+├── quickstart.md        # Phase 1 output (/speckit.plan command)+├── contracts/           # Phase 1 output (/speckit.plan command)+└── tasks.md             # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)+```++### Source Code (repository root)+<!--+  ACTION REQUIRED: Replace the placeholder tree below with the concrete layout+  for this feature. Delete unused options and expand the chosen structure with+  real paths (e.g., apps/admin, packages/something). The delivered plan must+  not include Option labels.+-->++```text+# [REMOVE IF UNUSED] Option 1: Single project (DEFAULT)+src/+├── models/+├── services/+├── cli/+└── lib/++tests/+├── contract/+├── integration/+└── unit/++# [REMOVE IF UNUSED] Option 2: Web application (when "frontend" + "backend" detected)+backend/+├── src/+│   ├── models/+│   ├── services/+│   └── api/+└── tests/++frontend/+├── src/+│   ├── components/+│   ├── pages/+│   └── services/+└── tests/++# [REMOVE IF UNUSED] Option 3: Mobile + API (when "iOS/Android" detected)+api/+└── [same as backend above]++ios/ or android/+└── [platform-specific structure: feature modules, UI flows, platform tests]+```++**Structure Decision**: [Document the selected structure and reference the real+directories captured above]++## Complexity Tracking++> **Fill ONLY if Constitution Check has violations that must be justified**++| Violation | Why Needed | Simpler Alternative Rejected Because |+|-----------|------------|-------------------------------------|+| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |+| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
+ .specify/templates/spec-template.md view
@@ -0,0 +1,128 @@+# Feature Specification: [FEATURE NAME]++**Feature Branch**: `[###-feature-name]`+**Created**: [DATE]+**Status**: Draft+**Input**: User description: "$ARGUMENTS"++## User Scenarios & Testing *(mandatory)*++<!--+  IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance.+  Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them,+  you should still have a viable MVP (Minimum Viable Product) that delivers value.++  Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical.+  Think of each story as a standalone slice of functionality that can be:+  - Developed independently+  - Tested independently+  - Deployed independently+  - Demonstrated to users independently+-->++### User Story 1 - [Brief Title] (Priority: P1)++[Describe this user journey in plain language]++**Why this priority**: [Explain the value and why it has this priority level]++**Independent Test**: [Describe how this can be tested independently - e.g., "Can be fully tested by [specific action] and delivers [specific value]"]++**Acceptance Scenarios**:++1. **Given** [initial state], **When** [action], **Then** [expected outcome]+2. **Given** [initial state], **When** [action], **Then** [expected outcome]++---++### User Story 2 - [Brief Title] (Priority: P2)++[Describe this user journey in plain language]++**Why this priority**: [Explain the value and why it has this priority level]++**Independent Test**: [Describe how this can be tested independently]++**Acceptance Scenarios**:++1. **Given** [initial state], **When** [action], **Then** [expected outcome]++---++### User Story 3 - [Brief Title] (Priority: P3)++[Describe this user journey in plain language]++**Why this priority**: [Explain the value and why it has this priority level]++**Independent Test**: [Describe how this can be tested independently]++**Acceptance Scenarios**:++1. **Given** [initial state], **When** [action], **Then** [expected outcome]++---++[Add more user stories as needed, each with an assigned priority]++### Edge Cases++<!--+  ACTION REQUIRED: The content in this section represents placeholders.+  Fill them out with the right edge cases.+-->++- What happens when [boundary condition]?+- How does system handle [error scenario]?++## Requirements *(mandatory)*++<!--+  ACTION REQUIRED: The content in this section represents placeholders.+  Fill them out with the right functional requirements.+-->++### Functional Requirements++- **FR-001**: System MUST [specific capability, e.g., "allow users to create accounts"]+- **FR-002**: System MUST [specific capability, e.g., "validate email addresses"]+- **FR-003**: Users MUST be able to [key interaction, e.g., "reset their password"]+- **FR-004**: System MUST [data requirement, e.g., "persist user preferences"]+- **FR-005**: System MUST [behavior, e.g., "log all security events"]++*Example of marking unclear requirements:*++- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified - email/password, SSO, OAuth?]+- **FR-007**: System MUST retain user data for [NEEDS CLARIFICATION: retention period not specified]++### Key Entities *(include if feature involves data)*++- **[Entity 1]**: [What it represents, key attributes without implementation]+- **[Entity 2]**: [What it represents, relationships to other entities]++## Success Criteria *(mandatory)*++<!--+  ACTION REQUIRED: Define measurable success criteria.+  These must be technology-agnostic and measurable.+-->++### Measurable Outcomes++- **SC-001**: [Measurable metric, e.g., "Users can complete account creation in under 2 minutes"]+- **SC-002**: [Measurable metric, e.g., "System handles 1000 concurrent users without degradation"]+- **SC-003**: [User satisfaction metric, e.g., "90% of users successfully complete primary task on first attempt"]+- **SC-004**: [Business metric, e.g., "Reduce support tickets related to [X] by 50%"]++## Assumptions++<!--+  ACTION REQUIRED: The content in this section represents placeholders.+  Fill them out with the right assumptions based on reasonable defaults+  chosen when the feature description did not specify certain details.+-->++- [Assumption about target users, e.g., "Users have stable internet connectivity"]+- [Assumption about scope boundaries, e.g., "Mobile support is out of scope for v1"]+- [Assumption about data/environment, e.g., "Existing authentication system will be reused"]+- [Dependency on existing system/service, e.g., "Requires access to the existing user profile API"]
+ .specify/templates/tasks-template.md view
@@ -0,0 +1,251 @@+---++description: "Task list template for feature implementation"+---++# Tasks: [FEATURE NAME]++**Input**: Design documents from `/specs/[###-feature-name]/`+**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/++**Tests**: The examples below include test tasks. Tests are OPTIONAL - only include them if explicitly requested in the feature specification.++**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.++## Format: `[ID] [P?] [Story] Description`++- **[P]**: Can run in parallel (different files, no dependencies)+- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)+- Include exact file paths in descriptions++## Path Conventions++- **Single project**: `src/`, `tests/` at repository root+- **Web app**: `backend/src/`, `frontend/src/`+- **Mobile**: `api/src/`, `ios/src/` or `android/src/`+- Paths shown below assume single project - adjust based on plan.md structure++<!--+  ============================================================================+  IMPORTANT: The tasks below are SAMPLE TASKS for illustration purposes only.++  The /speckit.tasks command MUST replace these with actual tasks based on:+  - User stories from spec.md (with their priorities P1, P2, P3...)+  - Feature requirements from plan.md+  - Entities from data-model.md+  - Endpoints from contracts/++  Tasks MUST be organized by user story so each story can be:+  - Implemented independently+  - Tested independently+  - Delivered as an MVP increment++  DO NOT keep these sample tasks in the generated tasks.md file.+  ============================================================================+-->++## Phase 1: Setup (Shared Infrastructure)++**Purpose**: Project initialization and basic structure++- [ ] T001 Create project structure per implementation plan+- [ ] T002 Initialize [language] project with [framework] dependencies+- [ ] T003 [P] Configure linting and formatting tools++---++## Phase 2: Foundational (Blocking Prerequisites)++**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented++**⚠️ CRITICAL**: No user story work can begin until this phase is complete++Examples of foundational tasks (adjust based on your project):++- [ ] T004 Setup database schema and migrations framework+- [ ] T005 [P] Implement authentication/authorization framework+- [ ] T006 [P] Setup API routing and middleware structure+- [ ] T007 Create base models/entities that all stories depend on+- [ ] T008 Configure error handling and logging infrastructure+- [ ] T009 Setup environment configuration management++**Checkpoint**: Foundation ready - user story implementation can now begin in parallel++---++## Phase 3: User Story 1 - [Title] (Priority: P1) 🎯 MVP++**Goal**: [Brief description of what this story delivers]++**Independent Test**: [How to verify this story works on its own]++### Tests for User Story 1 (OPTIONAL - only if tests requested) ⚠️++> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**++- [ ] T010 [P] [US1] Contract test for [endpoint] in tests/contract/test_[name].py+- [ ] T011 [P] [US1] Integration test for [user journey] in tests/integration/test_[name].py++### Implementation for User Story 1++- [ ] T012 [P] [US1] Create [Entity1] model in src/models/[entity1].py+- [ ] T013 [P] [US1] Create [Entity2] model in src/models/[entity2].py+- [ ] T014 [US1] Implement [Service] in src/services/[service].py (depends on T012, T013)+- [ ] T015 [US1] Implement [endpoint/feature] in src/[location]/[file].py+- [ ] T016 [US1] Add validation and error handling+- [ ] T017 [US1] Add logging for user story 1 operations++**Checkpoint**: At this point, User Story 1 should be fully functional and testable independently++---++## Phase 4: User Story 2 - [Title] (Priority: P2)++**Goal**: [Brief description of what this story delivers]++**Independent Test**: [How to verify this story works on its own]++### Tests for User Story 2 (OPTIONAL - only if tests requested) ⚠️++- [ ] T018 [P] [US2] Contract test for [endpoint] in tests/contract/test_[name].py+- [ ] T019 [P] [US2] Integration test for [user journey] in tests/integration/test_[name].py++### Implementation for User Story 2++- [ ] T020 [P] [US2] Create [Entity] model in src/models/[entity].py+- [ ] T021 [US2] Implement [Service] in src/services/[service].py+- [ ] T022 [US2] Implement [endpoint/feature] in src/[location]/[file].py+- [ ] T023 [US2] Integrate with User Story 1 components (if needed)++**Checkpoint**: At this point, User Stories 1 AND 2 should both work independently++---++## Phase 5: User Story 3 - [Title] (Priority: P3)++**Goal**: [Brief description of what this story delivers]++**Independent Test**: [How to verify this story works on its own]++### Tests for User Story 3 (OPTIONAL - only if tests requested) ⚠️++- [ ] T024 [P] [US3] Contract test for [endpoint] in tests/contract/test_[name].py+- [ ] T025 [P] [US3] Integration test for [user journey] in tests/integration/test_[name].py++### Implementation for User Story 3++- [ ] T026 [P] [US3] Create [Entity] model in src/models/[entity].py+- [ ] T027 [US3] Implement [Service] in src/services/[service].py+- [ ] T028 [US3] Implement [endpoint/feature] in src/[location]/[file].py++**Checkpoint**: All user stories should now be independently functional++---++[Add more user story phases as needed, following the same pattern]++---++## Phase N: Polish & Cross-Cutting Concerns++**Purpose**: Improvements that affect multiple user stories++- [ ] TXXX [P] Documentation updates in docs/+- [ ] TXXX Code cleanup and refactoring+- [ ] TXXX Performance optimization across all stories+- [ ] TXXX [P] Additional unit tests (if requested) in tests/unit/+- [ ] TXXX Security hardening+- [ ] TXXX Run quickstart.md validation++---++## Dependencies & Execution Order++### Phase Dependencies++- **Setup (Phase 1)**: No dependencies - can start immediately+- **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories+- **User Stories (Phase 3+)**: All depend on Foundational phase completion+  - User stories can then proceed in parallel (if staffed)+  - Or sequentially in priority order (P1 → P2 → P3)+- **Polish (Final Phase)**: Depends on all desired user stories being complete++### User Story Dependencies++- **User Story 1 (P1)**: Can start after Foundational (Phase 2) - No dependencies on other stories+- **User Story 2 (P2)**: Can start after Foundational (Phase 2) - May integrate with US1 but should be independently testable+- **User Story 3 (P3)**: Can start after Foundational (Phase 2) - May integrate with US1/US2 but should be independently testable++### Within Each User Story++- Tests (if included) MUST be written and FAIL before implementation+- Models before services+- Services before endpoints+- Core implementation before integration+- Story complete before moving to next priority++### Parallel Opportunities++- All Setup tasks marked [P] can run in parallel+- All Foundational tasks marked [P] can run in parallel (within Phase 2)+- Once Foundational phase completes, all user stories can start in parallel (if team capacity allows)+- All tests for a user story marked [P] can run in parallel+- Models within a story marked [P] can run in parallel+- Different user stories can be worked on in parallel by different team members++---++## Parallel Example: User Story 1++```bash+# Launch all tests for User Story 1 together (if tests requested):+Task: "Contract test for [endpoint] in tests/contract/test_[name].py"+Task: "Integration test for [user journey] in tests/integration/test_[name].py"++# Launch all models for User Story 1 together:+Task: "Create [Entity1] model in src/models/[entity1].py"+Task: "Create [Entity2] model in src/models/[entity2].py"+```++---++## Implementation Strategy++### MVP First (User Story 1 Only)++1. Complete Phase 1: Setup+2. Complete Phase 2: Foundational (CRITICAL - blocks all stories)+3. Complete Phase 3: User Story 1+4. **STOP and VALIDATE**: Test User Story 1 independently+5. Deploy/demo if ready++### Incremental Delivery++1. Complete Setup + Foundational → Foundation ready+2. Add User Story 1 → Test independently → Deploy/Demo (MVP!)+3. Add User Story 2 → Test independently → Deploy/Demo+4. Add User Story 3 → Test independently → Deploy/Demo+5. Each story adds value without breaking previous stories++### Parallel Team Strategy++With multiple developers:++1. Team completes Setup + Foundational together+2. Once Foundational is done:+   - Developer A: User Story 1+   - Developer B: User Story 2+   - Developer C: User Story 3+3. Stories complete and integrate independently++---++## Notes++- [P] tasks = different files, no dependencies+- [Story] label maps task to specific user story for traceability+- Each user story should be independently completable and testable+- Verify tests fail before implementing+- Commit after each task or logical group+- Stop at any checkpoint to validate story independently+- Avoid: vague tasks, same file conflicts, cross-story dependencies that break independence
+ CHANGELOG.md view
@@ -0,0 +1,18 @@+# Changelog++## 0.1.0.2 — release candidate++- Verify GHC 9.12.3 support and migrate development from Stack/hpack to Cabal 3.0 and a locked Nix build.+- Point package homepage, source repository and issue links to `lambdasistemi/tasty-bdd` following the repository transfer.+- Bound dependencies and add executable build, package, formatting, lint, documentation and source-archive checks.+- Add regression tests for recursive decorators over Tasty dependencies, including blocked tests after a failed prerequisite.+- Add a compiled example, contributor documentation and Spec Kit records.+- Enforce the published 0.1.0.1 source API and Hackage-quality archive/Haddock checks in CI. Document all exported lenses explicitly and test `onEach` across every Tasty wrapper.+- Deploy MkDocs to GitHub Pages and verify the served commit and generated bytes.+- Preserve the public API and published 0.1.0.1 behavior, including the `Succeded` spelling and dependency-wrapper traversal fix.++This candidate has not been uploaded to Hackage or tagged as a release. Fresh compiler evidence covers GHC 9.12.3; older compilers allowed by dependency bounds have not been revalidated.++## 0.1.0.1 — published++The published library and test sources match GitLab master. This release includes the recursive Tasty dependency-wrapper traversal fix; 0.1.0.2 preserves that behavior.
README.md view
@@ -1,39 +1,44 @@-[![CircleCI](https://circleci.com/gh/ptek/tasty-bdd/tree/master.svg?style=svg)](https://circleci.com/gh/ptek/tasty-bdd/tree/master)+# tasty-bdd -# Behavior-driven development +Typed Given/When/Then scenarios for Haskell's Tasty test framework. -## A [Haskell](https://www.haskell.org/) Behavior Driven Development framework featuring:+## User stories: write readable test scenarios -* A type constrained language to express-  *  *Given* as ordered preconditions or-  *  *GivenAndAfter* as oredered preconditions with reversed order of teardown actions (sort of resource management)-  *  One only *When* to introduce a last precondition and catch it's output to be fed to-  *  Some *Then* tests that will receive the output of *When*-* Support for do notation via free monad for composing _givens_ and _thens_ -* One monad independent pure interpreter-* One driver for the great [tasty](https://github.com/feuerbach/tasty) test library,  monad parametrized-* Support for [tasty-fail-fast](https://hackage.haskell.org/package/tasty-fail-fast) strategy flag which will not execute the teardown actions of the failed test-* A sophisticated form of value introspection to show the differences on equality failure from [tree-diff](https://github.com/phadej/tree-diffdifftree) package -* Recursive test decorators to prepend or append action to all the tests inside a test tree+As a test author, you can express setup, an action and assertions using constructors or `do` notation. Successful assertions pass the Tasty test; failed equality assertions show structural differences. -## Background+```mermaid+flowchart LR+  Given -->|prepare input| When+  When -->|pass result| Then+  Then -->|report outcome| Tasty+``` -[Behavior Driven Development](https://en.wikipedia.org/wiki/Behavior-driven_development) is a software development process that emerged from test-driven development (TDD) and is based on principles of [Hoare Logic](https://en.wikipedia.org/wiki/Hoare_logic). The process requires a strict structure of the tests - {Given} When {Then} - to make them understandable.+```haskell+import Test.BDD.LanguageFree (given, then_, when_)+import Test.Tasty (defaultMain)+import Test.Tasty.Bdd ((@?=), testBehaviorF) -## Example+main :: IO ()+main = defaultMain $ testBehaviorF id "addition" $ do+    initial <- given $ pure (40 :: Int)+    when_ (pure $ initial + 2) $ then_ (@?= 42)+``` -```haskell-import Test.Tasty.Bdd+This example is compiled and run by the `example` test suite. See [scenario examples](docs/stories/scenarios.md) and the [execution model](docs/architecture/execution.md) for setup, teardown and fail-fast behavior. -tests :: TestTree-tests = testBdd "Test sequence" -    $ Given (print "Some effect")-    $ Given (print "Another effect")-    $ GivenAndAfter (print "Aquiring resource" >> return "Resource 1")-                    (print . ("Release "++))-    $ GivenAndAfter (print "Aquiring resource" >> return "Resource 2")-                    (print . ("Release "++))-    $ When (print "Action returning" >> return ([1..10]++[100..106]) :: IO [Int])-    $ Then (@?= ([1..10]++[700..706]))-    $ End+## Build and contribute++```sh+nix develop --accept-flake-config+just build+just unit+just ci ```++The locked flake uses GHC 9.12.3 on x86_64 Linux. `nix flake check --accept-flake-config` runs the packaged build, tests, lint, formatting and documentation checks. Other systems have not been verified. Read the [published documentation](https://lambdasistemi.github.io/tasty-bdd/). See [development](docs/development.md) and the [Spec Kit plan](specs/001-modernization/plan.md).++## Published package++[Hackage](https://hackage.haskell.org/package/tasty-bdd) carries 0.1.0.0 and 0.1.0.1. GitLab history through January 2025 is retained here, including the source published in 0.1.0.1. This modernization has not published a new package. See [release preparation](docs/releases.md).++Licensed under [BSD-3-Clause](LICENSE).
+ cabal.project view
@@ -0,0 +1,5 @@+packages: .+-- The published API baseline generates lenses with Template Haskell.+extra-packages: microlens-th ==0.4.3.17+index-state: 2026-02-17T10:15:41Z+tests: True
+ docs/api.md view
@@ -0,0 +1,27 @@+# API reference++## Find the types and functions for your scenario++As a test author, you can browse the exact API rendering built for the current Hackage release candidate. Choose a module below, or use the symbol index inside the reference.++- <a href="../haddock/Test-Tasty-Bdd.html" target="bdd-api">Tasty integration and decorators</a>+- <a href="../haddock/Test-BDD-Language.html" target="bdd-api">Typed constructor DSL and lenses</a>+- <a href="../haddock/Test-BDD-LanguageFree.html" target="bdd-api">Free-monad DSL and interpretation</a>+- <a href="../haddock/System-CaptureStdout.html" target="bdd-api">Standard-output capture</a>++<iframe name="bdd-api" title="Current tasty-bdd Haddock API reference" src="../haddock/Test-Tasty-Bdd.html" style="width: 100%; height: 75vh; border: 1px solid #888; border-radius: 4px;"></iframe>++[Open the API reference in a full page](haddock/index.html).++## Keep reference and release together++The documentation build extracts the checked Hackage Haddock bundle. CI rejects missing or stale embedded files; hosting links and missing generated instance-anchor targets are adapted for GitHub Pages. Available dependency links lead to Hackage. The package’s own types, functions and source remain browsable here.++```mermaid+flowchart LR+  Source -->|package and test| Archive+  Archive -->|render and verify coverage| Haddock+  Haddock -->|bundle| HackageCandidate+  Haddock -->|embed same rendering| Documentation+  Documentation -->|verify served bytes| Pages+```
+ docs/architecture/execution.md view
@@ -0,0 +1,30 @@+# Scenario execution++## Understand how your scenario runs++As a test author, you need to know which actions execute and where failures are reported before choosing a scenario interface.++```mermaid+flowchart TD+  Constructors -->|collects actions| BDDTest+  FreeMonad -->|interprets and records teardown| BDDResult+  BDDTest -->|constructor provider| Tasty+  BDDResult -->|free provider| Tasty+  Decorators -->|wrap each leaf, preserving dependencies| TastyTree+  TastyTree -->|schedules| Tasty+```++`Test.BDD.Language` constrains preparation and testing phases with types. Its interpreter collects actions without executing them. `Test.BDD.LanguageFree` provides the `do` interface and returns a result with recorded teardown. `Test.Tasty.Bdd` adapts both forms to Tasty.++Recursive `beforeEach` and `afterEach` decorators descend through groups, resources, options and Tasty `After` dependency wrappers. They preserve the dependency wrapper while decorating its children.++## Design choices++| Choice | Alternative | Reason |+|---|---|---|+| Keep both public interfaces | Replace one with the other | Existing users depend on their distinct API and teardown behavior. |+| Preserve released dependency traversal | Start from GitHub's older code alone | GitLab and Hackage already include the compatibility fix. |+| Compile the documentation example | Show an unchecked snippet | Readers receive an example exercised by CI. |+| Keep runtime semantics stable | Redesign resource safety in this update | Tooling modernization is not a resource-management API change. |++`captureStdout` redirects process-wide standard output and suppresses action exceptions. It is intended for controlled test execution; concurrent capture is not isolated. The test harness runs serially by default.
+ docs/development.md view
@@ -0,0 +1,38 @@+# Develop tasty-bdd++## Make a change and verify it++As a contributor, you use the same locked tools and executed checks locally and in CI.++```sh+nix develop --accept-flake-config+just build+just unit+just format+just ci+```++`just build` and `just unit` use `-O0`; packaged Nix builds use `-O2`. CI enables the manual `werror` flag. Published consumers do not inherit `-Werror` by default. `cabal.project` pins the package index and enables all test suites.++```mermaid+flowchart LR+  Spec -->|guides| Change+  Change -->|build and test| Shell+  Change -->|sandbox verification| Flake+  Shell -->|evidence| PR+  Flake -->|evidence| PR+```++The flake's `unit`, `format-check`, `hlint`, `cabal-check`, `workflow-check`, `api-compat`, `hackage-quality` and `docs` apps provide focused checks. `api-compat` loads all four modules from the hash-pinned Hackage 0.1.0.1 source and this checkout under the same GHC, then compares their exported types, constructors, roles and signatures, including `onEach`. The runtime suite separately checks traversal through resources, options, groups and dependencies. This is a source-API check on the locked compiler, not a binary-ABI or full dependency-range guarantee. `nix build .#docs` produces the strict MkDocs site. No Hackage credentials are needed.++## Specify a change++Read `.specify/memory/constitution.md` first. The `.specify/scripts/bash` helpers and `.specify/templates` support numbered features under `specs/`. The modernization is recorded in `specs/001-modernization`. Use `SPECIFY_FEATURE=001-modernization` with helpers when working on the named maintenance branch. Keep spec, plan and task status consistent with evidence.++Spec Kit's agent prompts are installed globally rather than duplicated in this repository. The bundled scripts/templates come from Spec Kit 0.4.2; direct initialization avoided an upstream release-packaging SIGPIPE failure.++CI uses GitHub-hosted runners while the repository belongs to `paolino` and the organization `nixos` runner after transfer to `lambdasistemi`. Runner access and cache credentials must be rechecked after transfer.++## Read the deployed documentation++[GitHub Pages](https://lambdasistemi.github.io/tasty-bdd/) hosts the MkDocs site. Deployment checks the served revision and compares every generated HTML page and the Mermaid renderer with the build. During modernization, the named maintenance branch may deploy before merge; normal deployments follow `master`.
+ docs/index.md view
@@ -0,0 +1,16 @@+# tasty-bdd++## Write scenarios your team can read++As a Haskell test author, you describe preconditions, perform an action and check its result. Tasty runs each scenario and reports success or a structural equality difference.++```mermaid+flowchart LR+  Author -->|writes| Scenario+  Scenario -->|interpreted by| Provider+  Provider -->|reports to| Tasty+```++Start with [writing a scenario](stories/scenarios.md). The [execution model](architecture/execution.md) explains ordering and teardown; [development](development.md) describes reproducible builds and contribution checks.++The public interfaces remain `Test.BDD.Language`, `Test.BDD.LanguageFree`, `Test.Tasty.Bdd` and `System.CaptureStdout`. The constructor spelling `Succeded` remains part of the published API.
+ docs/releases.md view
@@ -0,0 +1,22 @@+# Prepare a release++## Review a package before publication++As a maintainer, you need a checked source distribution and a deliberate version choice before publishing a package used by others.++```mermaid+flowchart LR+  Checks -->|pass| SourceArchive+  SourceArchive -->|review contents and rebuild| Maintainer+  Maintainer -->|separate future decision| Release+```++Run `nix develop --accept-flake-config -c just release-check` to create checked source and Hackage-mode Haddock archives plus `SHA256SUMS` in `result-release`. The prepared candidate is version 0.1.0.2. The `hackage-quality` gate unpacks the source archive, runs `cabal check`, builds and tests with the consumer warning policy, and requires 100% Haddock coverage for every module with no unresolved local references. It runs offline against the locked dependency environment and is required by CI. The source archive and Haddock bundle are review artifacts; preparing them does not publish a package.++Repository ownership does not change Hackage ownership or existing package descriptions. Hackage 0.1.0.1 currently points to GitLab. A GitHub transfer alone will not update those links. Existing Hackage tarballs remain historical artifacts.++The release workflow prepares an archive for review and never uploads to Hackage. No automatic tagging or package publication is enabled by this modernization.++External dependency links in the optional Haddock bundle depend on installed dependency interfaces; missing external interfaces do not waive the package’s own documentation coverage.++Every library, whether published or unpublished, has its current generated API embedded in the documentation. For tasty-bdd, the [API reference](api.md) embeds the same Haddock bundle checked for release. CI verifies every embedded file and local API link; deployment compares the served API and source pages with that build.
+ docs/stories/scenarios.md view
@@ -0,0 +1,36 @@+# Write a scenario++## Check an action's result++As a test author, you want a scenario to state its preparation and expected outcome clearly. The free-monad interface supports `do` notation:++```haskell+import Test.BDD.LanguageFree (given, then_, when_)+import Test.Tasty (defaultMain)+import Test.Tasty.Bdd ((@?=), testBehaviorF)++main :: IO ()+main = defaultMain $ testBehaviorF id "addition" $ do+    initial <- given $ pure (40 :: Int)+    when_ (pure $ initial + 2) $ then_ (@?= 42)+```++Change the expectation to 43 and the scenario fails with an equality diagnostic. `(@?/=)` checks inequality. `then__` runs an assertion that does not need the `when_` result.++## Set up and tear down resources++`givenAndAfter` returns both a value for later steps and a resource for teardown. `givenAndAfter_` acquires a resource only for teardown. Teardowns run in reverse acquisition order.++```mermaid+sequenceDiagram+  participant S as Scenario+  participant A as Resource A+  participant B as Resource B+  S->>A: acquire+  S->>B: acquire+  S->>S: action and assertions+  S->>B: release+  S->>A: release+```++The free provider runs recorded teardown on success and caught failure. The constructor provider's fail-fast mode intentionally skips teardown after its equality failure. These existing behaviors differ; choose deliberately. Neither API promises exception-safe resource management for every possible asynchronous exception.
+ examples/Main.hs view
@@ -0,0 +1,10 @@+module Main (main) where++import Test.BDD.LanguageFree (given, then_, when_)+import Test.Tasty (defaultMain)+import Test.Tasty.Bdd (testBehaviorF, (@?=))++main :: IO ()+main = defaultMain $ testBehaviorF id "addition" $ do+    initial <- given $ pure (40 :: Int)+    when_ (pure $ initial + 2) $ then_ (@?= 42)
+ flake.lock view
@@ -0,0 +1,710 @@+{+  "nodes": {+    "HTTP": {+      "flake": false,+      "locked": {+        "lastModified": 1451647621,+        "narHash": "sha256-oHIyw3x0iKBexEo49YeUDV1k74ZtyYKGR2gNJXXRxts=",+        "owner": "phadej",+        "repo": "HTTP",+        "rev": "9bc0996d412fef1787449d841277ef663ad9a915",+        "type": "github"+      },+      "original": {+        "owner": "phadej",+        "repo": "HTTP",+        "type": "github"+      }+    },+    "cabal-34": {+      "flake": false,+      "locked": {+        "lastModified": 1645834128,+        "narHash": "sha256-wG3d+dOt14z8+ydz4SL7pwGfe7SiimxcD/LOuPCV6xM=",+        "owner": "haskell",+        "repo": "cabal",+        "rev": "5ff598c67f53f7c4f48e31d722ba37172230c462",+        "type": "github"+      },+      "original": {+        "owner": "haskell",+        "ref": "3.4",+        "repo": "cabal",+        "type": "github"+      }+    },+    "cabal-36": {+      "flake": false,+      "locked": {+        "lastModified": 1669081697,+        "narHash": "sha256-I5or+V7LZvMxfbYgZATU4awzkicBwwok4mVoje+sGmU=",+        "owner": "haskell",+        "repo": "cabal",+        "rev": "8fd619e33d34924a94e691c5fea2c42f0fc7f144",+        "type": "github"+      },+      "original": {+        "owner": "haskell",+        "ref": "3.6",+        "repo": "cabal",+        "type": "github"+      }+    },+    "cardano-shell": {+      "flake": false,+      "locked": {+        "lastModified": 1608537748,+        "narHash": "sha256-PulY1GfiMgKVnBci3ex4ptk2UNYMXqGjJOxcPy2KYT4=",+        "owner": "input-output-hk",+        "repo": "cardano-shell",+        "rev": "9392c75087cb9a3d453998f4230930dea3a95725",+        "type": "github"+      },+      "original": {+        "owner": "input-output-hk",+        "repo": "cardano-shell",+        "type": "github"+      }+    },+    "flake-compat": {+      "flake": false,+      "locked": {+        "lastModified": 1672831974,+        "narHash": "sha256-z9k3MfslLjWQfnjBtEtJZdq3H7kyi2kQtUThfTgdRk0=",+        "owner": "input-output-hk",+        "repo": "flake-compat",+        "rev": "45f2638735f8cdc40fe302742b79f248d23eb368",+        "type": "github"+      },+      "original": {+        "owner": "input-output-hk",+        "ref": "hkm/gitlab-fix",+        "repo": "flake-compat",+        "type": "github"+      }+    },+    "flake-parts": {+      "inputs": {+        "nixpkgs-lib": "nixpkgs-lib"+      },+      "locked": {+        "lastModified": 1763759067,+        "narHash": "sha256-LlLt2Jo/gMNYAwOgdRQBrsRoOz7BPRkzvNaI/fzXi2Q=",+        "owner": "hercules-ci",+        "repo": "flake-parts",+        "rev": "2cccadc7357c0ba201788ae99c4dfa90728ef5e0",+        "type": "github"+      },+      "original": {+        "owner": "hercules-ci",+        "repo": "flake-parts",+        "type": "github"+      }+    },+    "hackage-for-stackage": {+      "flake": false,+      "locked": {+        "lastModified": 1777941947,+        "narHash": "sha256-lv/D6tAU+kOARnDutmRqBKeES+YtngYnfRwuEciqGlw=",+        "owner": "input-output-hk",+        "repo": "hackage.nix",+        "rev": "c790aeedf8b119a2cce4c085a4274ab570942bfd",+        "type": "github"+      },+      "original": {+        "owner": "input-output-hk",+        "ref": "for-stackage",+        "repo": "hackage.nix",+        "type": "github"+      }+    },+    "hackage-internal": {+      "flake": false,+      "locked": {+        "lastModified": 1750307553,+        "narHash": "sha256-iiafNoeLHwlSLQTyvy8nPe2t6g5AV4PPcpMeH/2/DLs=",+        "owner": "input-output-hk",+        "repo": "hackage.nix",+        "rev": "f7867baa8817fab296528f4a4ec39d1c7c4da4f3",+        "type": "github"+      },+      "original": {+        "owner": "input-output-hk",+        "repo": "hackage.nix",+        "type": "github"+      }+    },+    "hackageNix": {+      "flake": false,+      "locked": {+        "lastModified": 1777942856,+        "narHash": "sha256-uNleQgyctFm13GstMF0HiO4trN/4lQWroPYlfQJO7kM=",+        "owner": "input-output-hk",+        "repo": "hackage.nix",+        "rev": "b6b4aa4bd699f743238da45c7f43da5a26a822f7",+        "type": "github"+      },+      "original": {+        "owner": "input-output-hk",+        "repo": "hackage.nix",+        "rev": "b6b4aa4bd699f743238da45c7f43da5a26a822f7",+        "type": "github"+      }+    },+    "haskellNix": {+      "inputs": {+        "HTTP": "HTTP",+        "cabal-34": "cabal-34",+        "cabal-36": "cabal-36",+        "cardano-shell": "cardano-shell",+        "flake-compat": "flake-compat",+        "hackage": [+          "hackageNix"+        ],+        "hackage-for-stackage": "hackage-for-stackage",+        "hackage-internal": "hackage-internal",+        "hls": "hls",+        "hls-1.10": "hls-1.10",+        "hls-2.0": "hls-2.0",+        "hls-2.10": "hls-2.10",+        "hls-2.11": "hls-2.11",+        "hls-2.12": "hls-2.12",+        "hls-2.2": "hls-2.2",+        "hls-2.3": "hls-2.3",+        "hls-2.4": "hls-2.4",+        "hls-2.5": "hls-2.5",+        "hls-2.6": "hls-2.6",+        "hls-2.7": "hls-2.7",+        "hls-2.8": "hls-2.8",+        "hls-2.9": "hls-2.9",+        "hpc-coveralls": "hpc-coveralls",+        "iserv-proxy": "iserv-proxy",+        "nixpkgs": [+          "haskellNix",+          "nixpkgs-unstable"+        ],+        "nixpkgs-2305": "nixpkgs-2305",+        "nixpkgs-2311": "nixpkgs-2311",+        "nixpkgs-2405": "nixpkgs-2405",+        "nixpkgs-2411": "nixpkgs-2411",+        "nixpkgs-2505": "nixpkgs-2505",+        "nixpkgs-2511": "nixpkgs-2511",+        "nixpkgs-unstable": "nixpkgs-unstable",+        "old-ghc-nix": "old-ghc-nix",+        "stackage": "stackage"+      },+      "locked": {+        "lastModified": 1777977445,+        "narHash": "sha256-2MvOtjkuTdhqZnZI0g8HuvACrZKpGq0/4EESqsWt9ag=",+        "owner": "input-output-hk",+        "repo": "haskell.nix",+        "rev": "8b447d7f57d62fab9249f79bb916bc891e29b9d0",+        "type": "github"+      },+      "original": {+        "owner": "input-output-hk",+        "repo": "haskell.nix",+        "rev": "8b447d7f57d62fab9249f79bb916bc891e29b9d0",+        "type": "github"+      }+    },+    "hls": {+      "flake": false,+      "locked": {+        "lastModified": 1741604408,+        "narHash": "sha256-tuq3+Ip70yu89GswZ7DSINBpwRprnWnl6xDYnS4GOsc=",+        "owner": "haskell",+        "repo": "haskell-language-server",+        "rev": "682d6894c94087da5e566771f25311c47e145359",+        "type": "github"+      },+      "original": {+        "owner": "haskell",+        "repo": "haskell-language-server",+        "type": "github"+      }+    },+    "hls-1.10": {+      "flake": false,+      "locked": {+        "lastModified": 1680000865,+        "narHash": "sha256-rc7iiUAcrHxwRM/s0ErEsSPxOR3u8t7DvFeWlMycWgo=",+        "owner": "haskell",+        "repo": "haskell-language-server",+        "rev": "b08691db779f7a35ff322b71e72a12f6e3376fd9",+        "type": "github"+      },+      "original": {+        "owner": "haskell",+        "ref": "1.10.0.0",+        "repo": "haskell-language-server",+        "type": "github"+      }+    },+    "hls-2.0": {+      "flake": false,+      "locked": {+        "lastModified": 1687698105,+        "narHash": "sha256-OHXlgRzs/kuJH8q7Sxh507H+0Rb8b7VOiPAjcY9sM1k=",+        "owner": "haskell",+        "repo": "haskell-language-server",+        "rev": "783905f211ac63edf982dd1889c671653327e441",+        "type": "github"+      },+      "original": {+        "owner": "haskell",+        "ref": "2.0.0.1",+        "repo": "haskell-language-server",+        "type": "github"+      }+    },+    "hls-2.10": {+      "flake": false,+      "locked": {+        "lastModified": 1743069404,+        "narHash": "sha256-q4kDFyJDDeoGqfEtrZRx4iqMVEC2MOzCToWsFY+TOzY=",+        "owner": "haskell",+        "repo": "haskell-language-server",+        "rev": "2318c61db3a01e03700bd4b05665662929b7fe8b",+        "type": "github"+      },+      "original": {+        "owner": "haskell",+        "ref": "2.10.0.0",+        "repo": "haskell-language-server",+        "type": "github"+      }+    },+    "hls-2.11": {+      "flake": false,+      "locked": {+        "lastModified": 1747306193,+        "narHash": "sha256-/MmtpF8+FyQlwfKHqHK05BdsxC9LHV70d/FiMM7pzBM=",+        "owner": "haskell",+        "repo": "haskell-language-server",+        "rev": "46ef4523ea4949f47f6d2752476239f1c6d806fe",+        "type": "github"+      },+      "original": {+        "owner": "haskell",+        "ref": "2.11.0.0",+        "repo": "haskell-language-server",+        "type": "github"+      }+    },+    "hls-2.12": {+      "flake": false,+      "locked": {+        "lastModified": 1758709460,+        "narHash": "sha256-xkI8MIIVEVARskfWbGAgP5sHG/lyeKnkm0LIOJ19X5w=",+        "owner": "haskell",+        "repo": "haskell-language-server",+        "rev": "7d983de4fa7ff54369f6dd31444bdb9869aec83e",+        "type": "github"+      },+      "original": {+        "owner": "haskell",+        "ref": "2.12.0.0",+        "repo": "haskell-language-server",+        "type": "github"+      }+    },+    "hls-2.2": {+      "flake": false,+      "locked": {+        "lastModified": 1693064058,+        "narHash": "sha256-8DGIyz5GjuCFmohY6Fa79hHA/p1iIqubfJUTGQElbNk=",+        "owner": "haskell",+        "repo": "haskell-language-server",+        "rev": "b30f4b6cf5822f3112c35d14a0cba51f3fe23b85",+        "type": "github"+      },+      "original": {+        "owner": "haskell",+        "ref": "2.2.0.0",+        "repo": "haskell-language-server",+        "type": "github"+      }+    },+    "hls-2.3": {+      "flake": false,+      "locked": {+        "lastModified": 1695910642,+        "narHash": "sha256-tR58doOs3DncFehHwCLczJgntyG/zlsSd7DgDgMPOkI=",+        "owner": "haskell",+        "repo": "haskell-language-server",+        "rev": "458ccdb55c9ea22cd5d13ec3051aaefb295321be",+        "type": "github"+      },+      "original": {+        "owner": "haskell",+        "ref": "2.3.0.0",+        "repo": "haskell-language-server",+        "type": "github"+      }+    },+    "hls-2.4": {+      "flake": false,+      "locked": {+        "lastModified": 1699862708,+        "narHash": "sha256-YHXSkdz53zd0fYGIYOgLt6HrA0eaRJi9mXVqDgmvrjk=",+        "owner": "haskell",+        "repo": "haskell-language-server",+        "rev": "54507ef7e85fa8e9d0eb9a669832a3287ffccd57",+        "type": "github"+      },+      "original": {+        "owner": "haskell",+        "ref": "2.4.0.1",+        "repo": "haskell-language-server",+        "type": "github"+      }+    },+    "hls-2.5": {+      "flake": false,+      "locked": {+        "lastModified": 1701080174,+        "narHash": "sha256-fyiR9TaHGJIIR0UmcCb73Xv9TJq3ht2ioxQ2mT7kVdc=",+        "owner": "haskell",+        "repo": "haskell-language-server",+        "rev": "27f8c3d3892e38edaef5bea3870161815c4d014c",+        "type": "github"+      },+      "original": {+        "owner": "haskell",+        "ref": "2.5.0.0",+        "repo": "haskell-language-server",+        "type": "github"+      }+    },+    "hls-2.6": {+      "flake": false,+      "locked": {+        "lastModified": 1705325287,+        "narHash": "sha256-+P87oLdlPyMw8Mgoul7HMWdEvWP/fNlo8jyNtwME8E8=",+        "owner": "haskell",+        "repo": "haskell-language-server",+        "rev": "6e0b342fa0327e628610f2711f8c3e4eaaa08b1e",+        "type": "github"+      },+      "original": {+        "owner": "haskell",+        "ref": "2.6.0.0",+        "repo": "haskell-language-server",+        "type": "github"+      }+    },+    "hls-2.7": {+      "flake": false,+      "locked": {+        "lastModified": 1708965829,+        "narHash": "sha256-LfJ+TBcBFq/XKoiNI7pc4VoHg4WmuzsFxYJ3Fu+Jf+M=",+        "owner": "haskell",+        "repo": "haskell-language-server",+        "rev": "50322b0a4aefb27adc5ec42f5055aaa8f8e38001",+        "type": "github"+      },+      "original": {+        "owner": "haskell",+        "ref": "2.7.0.0",+        "repo": "haskell-language-server",+        "type": "github"+      }+    },+    "hls-2.8": {+      "flake": false,+      "locked": {+        "lastModified": 1715153580,+        "narHash": "sha256-Vi/iUt2pWyUJlo9VrYgTcbRviWE0cFO6rmGi9rmALw0=",+        "owner": "haskell",+        "repo": "haskell-language-server",+        "rev": "dd1be1beb16700de59e0d6801957290bcf956a0a",+        "type": "github"+      },+      "original": {+        "owner": "haskell",+        "ref": "2.8.0.0",+        "repo": "haskell-language-server",+        "type": "github"+      }+    },+    "hls-2.9": {+      "flake": false,+      "locked": {+        "lastModified": 1719993701,+        "narHash": "sha256-wy348++MiMm/xwtI9M3vVpqj2qfGgnDcZIGXw8sF1sA=",+        "owner": "haskell",+        "repo": "haskell-language-server",+        "rev": "90319a7e62ab93ab65a95f8f2bcf537e34dae76a",+        "type": "github"+      },+      "original": {+        "owner": "haskell",+        "ref": "2.9.0.1",+        "repo": "haskell-language-server",+        "type": "github"+      }+    },+    "hpc-coveralls": {+      "flake": false,+      "locked": {+        "lastModified": 1607498076,+        "narHash": "sha256-8uqsEtivphgZWYeUo5RDUhp6bO9j2vaaProQxHBltQk=",+        "owner": "sevanspowell",+        "repo": "hpc-coveralls",+        "rev": "14df0f7d229f4cd2e79f8eabb1a740097fdfa430",+        "type": "github"+      },+      "original": {+        "owner": "sevanspowell",+        "repo": "hpc-coveralls",+        "type": "github"+      }+    },+    "iserv-proxy": {+      "flake": false,+      "locked": {+        "lastModified": 1775620557,+        "narHash": "sha256-10x8/G0x3eR/++XRHPx4MBuqlnc6+N+ajIxXyLkG+nU=",+        "owner": "stable-haskell",+        "repo": "iserv-proxy",+        "rev": "3f7b2815307c20a0dfd816bdf4a39ab86af3e0d4",+        "type": "github"+      },+      "original": {+        "owner": "stable-haskell",+        "ref": "iserv-syms",+        "repo": "iserv-proxy",+        "type": "github"+      }+    },+    "lintNixpkgs": {+      "locked": {+        "lastModified": 1759070547,+        "narHash": "sha256-JVZl8NaVRYb0+381nl7LvPE+A774/dRpif01FKLrYFQ=",+        "owner": "NixOS",+        "repo": "nixpkgs",+        "rev": "647e5c14cbd5067f44ac86b74f014962df460840",+        "type": "github"+      },+      "original": {+        "owner": "NixOS",+        "repo": "nixpkgs",+        "rev": "647e5c14cbd5067f44ac86b74f014962df460840",+        "type": "github"+      }+    },+    "mkdocs": {+      "inputs": {+        "flake-parts": "flake-parts",+        "nixpkgs": "nixpkgs"+      },+      "locked": {+        "dir": "mkdocs",+        "lastModified": 1781117575,+        "narHash": "sha256-cgXoc9shsVKGk3OCK7roSK2JvBA785EhvbyOfVh9iRE=",+        "owner": "paolino",+        "repo": "dev-assets",+        "rev": "60fcec8ed6ec760e60cf92a41e6713d77ce49900",+        "type": "github"+      },+      "original": {+        "dir": "mkdocs",+        "owner": "paolino",+        "repo": "dev-assets",+        "rev": "60fcec8ed6ec760e60cf92a41e6713d77ce49900",+        "type": "github"+      }+    },+    "nixpkgs": {+      "locked": {+        "lastModified": 1763678758,+        "narHash": "sha256-+hBiJ+kG5IoffUOdlANKFflTT5nO3FrrR2CA3178Y5s=",+        "owner": "NixOS",+        "repo": "nixpkgs",+        "rev": "117cc7f94e8072499b0a7aa4c52084fa4e11cc9b",+        "type": "github"+      },+      "original": {+        "owner": "NixOS",+        "ref": "nixos-unstable",+        "repo": "nixpkgs",+        "type": "github"+      }+    },+    "nixpkgs-2305": {+      "locked": {+        "lastModified": 1705033721,+        "narHash": "sha256-K5eJHmL1/kev6WuqyqqbS1cdNnSidIZ3jeqJ7GbrYnQ=",+        "owner": "NixOS",+        "repo": "nixpkgs",+        "rev": "a1982c92d8980a0114372973cbdfe0a307f1bdea",+        "type": "github"+      },+      "original": {+        "owner": "NixOS",+        "ref": "nixpkgs-23.05-darwin",+        "repo": "nixpkgs",+        "type": "github"+      }+    },+    "nixpkgs-2311": {+      "locked": {+        "lastModified": 1719957072,+        "narHash": "sha256-gvFhEf5nszouwLAkT9nWsDzocUTqLWHuL++dvNjMp9I=",+        "owner": "NixOS",+        "repo": "nixpkgs",+        "rev": "7144d6241f02d171d25fba3edeaf15e0f2592105",+        "type": "github"+      },+      "original": {+        "owner": "NixOS",+        "ref": "nixpkgs-23.11-darwin",+        "repo": "nixpkgs",+        "type": "github"+      }+    },+    "nixpkgs-2405": {+      "locked": {+        "lastModified": 1735564410,+        "narHash": "sha256-HB/FA0+1gpSs8+/boEavrGJH+Eq08/R2wWNph1sM1Dg=",+        "owner": "NixOS",+        "repo": "nixpkgs",+        "rev": "1e7a8f391f1a490460760065fa0630b5520f9cf8",+        "type": "github"+      },+      "original": {+        "owner": "NixOS",+        "ref": "nixpkgs-24.05-darwin",+        "repo": "nixpkgs",+        "type": "github"+      }+    },+    "nixpkgs-2411": {+      "locked": {+        "lastModified": 1751290243,+        "narHash": "sha256-kNf+obkpJZWar7HZymXZbW+Rlk3HTEIMlpc6FCNz0Ds=",+        "owner": "NixOS",+        "repo": "nixpkgs",+        "rev": "5ab036a8d97cb9476fbe81b09076e6e91d15e1b6",+        "type": "github"+      },+      "original": {+        "owner": "NixOS",+        "ref": "nixpkgs-24.11-darwin",+        "repo": "nixpkgs",+        "type": "github"+      }+    },+    "nixpkgs-2505": {+      "locked": {+        "lastModified": 1764560356,+        "narHash": "sha256-M5aFEFPppI4UhdOxwdmceJ9bDJC4T6C6CzCK1E2FZyo=",+        "owner": "NixOS",+        "repo": "nixpkgs",+        "rev": "6c8f0cca84510cc79e09ea99a299c9bc17d03cb6",+        "type": "github"+      },+      "original": {+        "owner": "NixOS",+        "ref": "nixpkgs-25.05-darwin",+        "repo": "nixpkgs",+        "type": "github"+      }+    },+    "nixpkgs-2511": {+      "locked": {+        "lastModified": 1775749320,+        "narHash": "sha256-msT6frWJSQ2WR+0cpk+KPcZdLTLagUIsJwQwIX9JNSo=",+        "owner": "NixOS",+        "repo": "nixpkgs",+        "rev": "74b87959b2d16f59f54d8559cf3cf26b9d907949",+        "type": "github"+      },+      "original": {+        "owner": "NixOS",+        "ref": "nixpkgs-25.11-darwin",+        "repo": "nixpkgs",+        "type": "github"+      }+    },+    "nixpkgs-lib": {+      "locked": {+        "lastModified": 1761765539,+        "narHash": "sha256-b0yj6kfvO8ApcSE+QmA6mUfu8IYG6/uU28OFn4PaC8M=",+        "owner": "nix-community",+        "repo": "nixpkgs.lib",+        "rev": "719359f4562934ae99f5443f20aa06c2ffff91fc",+        "type": "github"+      },+      "original": {+        "owner": "nix-community",+        "repo": "nixpkgs.lib",+        "type": "github"+      }+    },+    "nixpkgs-unstable": {+      "locked": {+        "lastModified": 1775888245,+        "narHash": "sha256-nwASzrRDD1JBEu/o8ekKYEXm/oJW6EMCzCRdrwcLe90=",+        "owner": "NixOS",+        "repo": "nixpkgs",+        "rev": "13043924aaa7375ce482ebe2494338e058282925",+        "type": "github"+      },+      "original": {+        "owner": "NixOS",+        "ref": "nixpkgs-unstable",+        "repo": "nixpkgs",+        "type": "github"+      }+    },+    "old-ghc-nix": {+      "flake": false,+      "locked": {+        "lastModified": 1631092763,+        "narHash": "sha256-sIKgO+z7tj4lw3u6oBZxqIhDrzSkvpHtv0Kki+lh9Fg=",+        "owner": "angerman",+        "repo": "old-ghc-nix",+        "rev": "af48a7a7353e418119b6dfe3cd1463a657f342b8",+        "type": "github"+      },+      "original": {+        "owner": "angerman",+        "ref": "master",+        "repo": "old-ghc-nix",+        "type": "github"+      }+    },+    "root": {+      "inputs": {+        "hackageNix": "hackageNix",+        "haskellNix": "haskellNix",+        "lintNixpkgs": "lintNixpkgs",+        "mkdocs": "mkdocs",+        "nixpkgs": [+          "haskellNix",+          "nixpkgs-unstable"+        ]+      }+    },+    "stackage": {+      "flake": false,+      "locked": {+        "lastModified": 1777941049,+        "narHash": "sha256-QyXmYtr1Sqzezfot7ARd5OL/0sMot/cybcZJh7EmYE0=",+        "owner": "input-output-hk",+        "repo": "stackage.nix",+        "rev": "aeb46c4a0af96a116fb0c9dc54cfae2fd6e01a39",+        "type": "github"+      },+      "original": {+        "owner": "input-output-hk",+        "repo": "stackage.nix",+        "type": "github"+      }+    }+  },+  "root": "root",+  "version": 7+}
+ flake.nix view
@@ -0,0 +1,67 @@+{+  description = "Typed BDD scenarios for Tasty, built with GHC 9.12.3";+  nixConfig = {+    extra-substituters = [ "https://cache.iog.io" ];+    extra-trusted-public-keys =+      [ "hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ=" ];+  };+  inputs = {+    haskellNix.url =+      "github:input-output-hk/haskell.nix/8b447d7f57d62fab9249f79bb916bc891e29b9d0";+    hackageNix = {+      url =+        "github:input-output-hk/hackage.nix/b6b4aa4bd699f743238da45c7f43da5a26a822f7";+      flake = false;+    };+    haskellNix.inputs.hackage.follows = "hackageNix";+    nixpkgs.follows = "haskellNix/nixpkgs-unstable";+    lintNixpkgs.url =+      "github:NixOS/nixpkgs/647e5c14cbd5067f44ac86b74f014962df460840";+    mkdocs.url =+      "github:paolino/dev-assets/60fcec8ed6ec760e60cf92a41e6713d77ce49900?dir=mkdocs";+  };+  outputs = { nixpkgs, haskellNix, lintNixpkgs, mkdocs, ... }:+    let+      system = "x86_64-linux";+      pkgs = import nixpkgs {+        inherit system;+        overlays = [ haskellNix.overlay ];+      };+      lintPkgs = import lintNixpkgs { inherit system; };+      project = import ./nix/project.nix { inherit pkgs lintPkgs; };+      components = project.hsPkgs.tasty-bdd.components;+      sourceDist = import ./nix/source-dist.nix {+        pkgs = lintPkgs;+        src = ./.;+      };+      archiveProject = import ./nix/project.nix {+        inherit pkgs lintPkgs;+        src = "${sourceDist}/source";+      };+      checks = import ./nix/checks.nix {+        inherit pkgs lintPkgs components;+        inherit sourceDist;+        buildShell = project.shell;+        archiveComponents = archiveProject.hsPkgs.tasty-bdd.components;+        src = ./.;+        docsShell = mkdocs.devShells.${system}.default;+      };+    in {+      packages.${system} = {+        default = components.library;+        tests = components.tests.test;+        example = components.tests.example;+        docs = checks.docs;+        source-dist = sourceDist;+        hackage-release = checks.hackageRelease;+      };+      devShells.${system}.default = project.shell;+      checks.${system} =+        builtins.removeAttrs checks [ "apps" "hackageRelease" ];+      apps.${system} = builtins.mapAttrs (_: app: {+        type = "app";+        meta.description = "Run the tasty-bdd ${app.name} check";+        program = pkgs.lib.getExe app;+      }) checks.apps;+    };+}
+ fourmolu.yaml view
@@ -0,0 +1,10 @@+column-limit: 70+indentation: 4+comma-style: leading+function-arrows: leading+import-export-style: leading+record-brace-space: false+indent-wheres: false+in-style: left-align+newlines-between-decls: 1+haddock-style: multi-line
+ justfile view
@@ -0,0 +1,41 @@+# Run these recipes inside nix develop.+default:+    @just --list++build:+    cabal build all --enable-tests -O0 -fwerror++unit:+    cabal test all --enable-tests -O0 -fwerror --test-show-details=direct++format:+    fourmolu -i src tests examples+    cabal-fmt -i tasty-bdd.cabal+    nixfmt flake.nix nix/*.nix++format-check:+    nix run .#format-check++hlint:+    nix run .#hlint++cabal-check:+    cabal check++build-docs:+    nix build .#docs++sdist:+    cabal sdist --output-directory=dist++api-compat:+    nix run .#api-compat++hackage-quality:+    nix run .#hackage-quality++release-check:+    nix build .#hackage-release -o result-release++ci: build unit+    nix flake check --no-eval-cache
+ mkdocs.yml view
@@ -0,0 +1,37 @@+site_name: tasty-bdd+site_description: Typed Given/When/Then scenarios for Haskell and Tasty+site_url: https://lambdasistemi.github.io/tasty-bdd/+repo_url: https://github.com/lambdasistemi/tasty-bdd+edit_uri: edit/master/docs/+nav:+  - Home: index.md+  - Write a scenario: stories/scenarios.md+  - API reference: api.md+  - Architecture: architecture/execution.md+  - Development: development.md+  - Release preparation: releases.md+theme:+  name: material+  palette:+    - media: '(prefers-color-scheme: light)'+      scheme: default+      toggle:+        icon: material/brightness-7+        name: Switch to dark mode+    - media: '(prefers-color-scheme: dark)'+      scheme: slate+      toggle:+        icon: material/brightness-4+        name: Switch to light mode+plugins:+  - search+markdown_extensions:+  - admonition+  - pymdownx.highlight+  - pymdownx.superfences:+      custom_fences:+        - name: mermaid+          class: mermaid+          format: !!python/name:pymdownx.superfences.fence_code_format+extra_javascript:+  - assets/javascripts/mermaid.min.js
+ nix/checks.nix view
@@ -0,0 +1,154 @@+{ pkgs, lintPkgs, components, archiveComponents, src, docsShell, buildShell+, sourceDist }:+let+  compilerInputs = buildShell.nativeBuildInputs ++ buildShell.buildInputs;+  scriptInputs = with lintPkgs; [+    coreutils+    gnutar+    gzip+    gnugrep+    diffutils+    python3+  ];+  publishedArchive = pkgs.fetchurl {+    url =+      "https://hackage.haskell.org/package/tasty-bdd-0.1.0.1/tasty-bdd-0.1.0.1.tar.gz";+    sha256 = "f13238d7445fc5afeb9c6cda858f60f1c3b90247b7f369ea9cf65605074a41b5";+  };+  published = pkgs.runCommand "tasty-bdd-published-0.1.0.1" {+    nativeBuildInputs = [ pkgs.gnutar pkgs.gzip ];+  } ''+    mkdir -p $out+    tar -xzf ${publishedArchive} --strip-components=1 -C $out+  '';+  hackageRelease = pkgs.runCommand "tasty-bdd-hackage-release" {+    nativeBuildInputs = compilerInputs ++ scriptInputs ++ [ pkgs.glibcLocales ];+    LANG = "C.UTF-8";+    LC_ALL = "C.UTF-8";+  } ''+    bash ${src}/tools/check-hackage.sh ${sourceDist}/tasty-bdd-*.tar.gz $out+  '';+  scripts = {+    api-compat = {+      runtimeInputs = compilerInputs ++ scriptInputs;+      text = ''+        bash ${src}/tools/check-api.sh ${published} ${src}+      '';+    };+    hackage-quality = {+      runtimeInputs = [ pkgs.coreutils ];+      text = ''+        cd ${hackageRelease}+        sha256sum -c SHA256SUMS+        cat haddock.log+      '';+    };+    sdist-rebuild = {+      runtimeInputs = [ ];+      text = ''+        ${archiveComponents.tests.test}/bin/test+        ${archiveComponents.tests.example}/bin/example+      '';+    };+    unit = {+      runtimeInputs = [ ];+      text = ''+        ${components.tests.test}/bin/test+        ${components.tests.example}/bin/example+      '';+    };+    format-check = {+      runtimeInputs = with lintPkgs; [+        haskellPackages.fourmolu+        haskellPackages.cabal-fmt+        nixfmt-classic+        diffutils+        findutils+      ];+      text = ''+        cd ${src}+        find src tests examples -name '*.hs' -exec fourmolu -m check {} ++        diff -u tasty-bdd.cabal <(cabal-fmt tasty-bdd.cabal)+        nixfmt --check flake.nix nix/*.nix+      '';+    };+    hlint = {+      runtimeInputs = [ lintPkgs.haskellPackages.hlint ];+      text = ''+        cd ${src}+        hlint src tests examples+      '';+    };+    cabal-check = {+      runtimeInputs = [ lintPkgs.cabal-install ];+      text = ''+        cd ${src}+        cabal check+      '';+    };+    workflow-check = {+      runtimeInputs = [ lintPkgs.actionlint ];+      text = ''+        cd ${src}+        actionlint -config-file .github/actionlint.yaml .github/workflows/*.yml+      '';+    };+    docs-check = {+      runtimeInputs = [ lintPkgs.python3 ];+      text = ''+        cd ${src}+        python3 tools/check_presentation.py --no-speech --front README.md README.md docs specs+      '';+    };+  };+  apps = builtins.mapAttrs+    (name: spec: pkgs.writeShellApplication (spec // { inherit name; }))+    scripts;+  mkCheck = name: app:+    pkgs.runCommand name {+      nativeBuildInputs = [ pkgs.glibcLocales ];+      LANG = "C.UTF-8";+      LC_ALL = "C.UTF-8";+    } ''+      ${pkgs.lib.getExe app}+      touch $out+    '';+  mermaid = pkgs.fetchurl {+    url = "https://unpkg.com/mermaid@11.17.2/dist/mermaid.min.js";+    hash = "sha256-WB7X10vZBI0OOpE2OSfXLvIpQtdyJUayf3zCnjU5Drg=";+  };+  docs = pkgs.runCommand "tasty-bdd-docs" {+    nativeBuildInputs = docsShell.nativeBuildInputs ++ docsShell.buildInputs;+    LANG = "C.UTF-8";+    LC_ALL = "C.UTF-8";+  } ''+    cp -R ${src} work+    chmod -R u+w work+    cd work+    mkdir -p docs/assets/javascripts+    cp ${mermaid} docs/assets/javascripts/mermaid.min.js+    python3 tools/embed-haddock.py ${hackageRelease}/*-docs.tar.gz docs/haddock+    mkdocs build --strict --site-dir $out+    python3 tools/embed-haddock.py ${hackageRelease}/*-docs.tar.gz $out/haddock --check+  '';+in builtins.mapAttrs mkCheck apps // {+  inherit docs hackageRelease;+  library = components.library;+  apps = apps // {+    pages-smoke = pkgs.writeShellApplication {+      name = "pages-smoke";+      runtimeInputs = [ lintPkgs.python3 ];+      text = ''+        python3 ${src}/tools/check-pages.py ${docs} "$@"+      '';+    };+    docs = pkgs.writeShellApplication {+      name = "docs";+      runtimeInputs = [ pkgs.coreutils ];+      text = ''+        test -s ${docs}/index.html+        echo "Strict documentation build: ${docs}"+      '';+    };+  };+}
+ nix/project.nix view
@@ -0,0 +1,24 @@+{ pkgs, lintPkgs, src ? ../. }:+pkgs.haskell-nix.cabalProject' {+  name = "tasty-bdd";+  inherit src;+  compiler-nix-name = "ghc9123";+  modules = [{+    packages.tasty-bdd.flags.werror = true;+    packages.tasty-bdd.ghcOptions = [ "-O2" ];+  }];+  shell = {+    additional = ps: [ ps.microlens-th ];+    withHoogle = false;+    buildInputs = with lintPkgs; [+      cabal-install+      just+      nixfmt-classic+      actionlint+      python3+      haskellPackages.fourmolu+      haskellPackages.hlint+      haskellPackages.cabal-fmt+    ];+  };+}
+ nix/source-dist.nix view
@@ -0,0 +1,13 @@+{ pkgs, src }:+pkgs.runCommand "tasty-bdd-source-dist" {+  nativeBuildInputs = [ pkgs.cabal-install pkgs.gnutar ];+} ''+  export HOME=$TMPDIR/home+  mkdir -p "$HOME" $out+  cp -R ${src} work+  chmod -R u+w work+  cd work+  cabal sdist --output-directory=$out+  mkdir $out/source+  tar -xzf "$out"/tasty-bdd-*.tar.gz --strip-components=1 -C $out/source+''
+ specs/001-modernization/plan.md view
@@ -0,0 +1,39 @@+# Modernization implementation plan++## Status++Completed: original GitHub builds on GHC 9.12.3 and all six tests pass; GitLab comparison found nine descendant commits and source parity with published 0.1.0.1; branch fast-forwarded to GitLab history.+Current: the modernization branch is pushed for PR review and live CI. Cabal/Nix, documentation and Spec Kit setup are implemented; nine tests and the example are required, including from the rebuilt source archive. Two compiled mutations confirm dependency traversal and failed-prerequisite coverage.+External setup: the operator initialized the wiki and its Home, sidebar and September logbook are published. The organization Default runner group includes this public repository and has ten online nixos runners; CI selects nixos after transfer. The operator subsequently authorized the transfer; it completed with repository ID 110524936 preserved. Package publication remains outside execution scope.++## Contributor story++A contributor gets one reproducible build and a visible, executable review gate. A maintainer gets a safe, separately executable transfer handoff.++```mermaid+flowchart TD+  Baseline -->|retain history| GitLab+  GitLab -->|specify promises| SpecKit+  SpecKit -->|implement| CabalAndNix+  CabalAndNix -->|verify| CI+  CI -->|document and hand over| Runbook+```++## Decisions++| Choice | Alternative | Reason |+|---|---|---|+| Fast-forward the work branch through GitLab | Reimplement its patch | Preserve attribution and already-published behavior. |+| Handwritten Cabal 3.0, release candidate 0.1.0.2 | Keep Stack/hpack or reuse a published version | One package description; the operator requested release preparation after modernization. |+| haskell.nix with GHC 9.12.3, pinned dependencies | Permissive bounds alone | Compilation and test evidence must be reproducible. |+| Executed sandbox checks and matching apps | Script derivations masquerading as checks | Green means the check ran. |+| Preserve master | Rename during ownership transfer | Avoid unrelated branch/link churn. |+| Prepared release configuration and artifacts | Automatic publication | A live Hackage package needs a separate version/release decision. |++## Forecast and resource limits++The original dependency build took approximately five minutes and about 0.3 GiB of new Cabal packages. No compiler port is indicated. Forecast: 15–30 minutes for history and specification; 45–90 minutes for Cabal/Nix and strict verification; 30–60 minutes for CI/docs/repository conventions; 15–30 minutes for final checks and handoff. Total 1.75–3.5 hours, subject to cache and runner availability. Inspect Nix realization size before downloads; use `/code` for sizeable scratch work and notify before approximately 10 GiB growth.++## Verification++Run the original and GitLab baselines, then `nix flake check --no-eval-cache`, `nix develop --quiet -c just ci`, and each surfaced app. Confirm a malformed source/formatting input makes the corresponding sandbox check fail. Compile and run the documented example. Inspect source distribution contents and build the unpacked archive. Verify branch/PR identity and live CI separately from local results. Transfer checks are operator-run steps, never claimed executed.
+ specs/001-modernization/spec.md view
@@ -0,0 +1,33 @@+# Modernize development and transfer the repository++## User stories++As a contributor, I can clone the project, enter its reproducible development environment, build the library and run its examples and tests with GHC 9.12.3. An invalid change causes a failing check rather than a misleading green workflow.++As an existing user, I retain the published BDD API and behavior, including recursive decorators over dependent Tasty tests. Modernization preserves the providers. The operator subsequently requested a Hackage-level release preparation: prepare version 0.1.0.2 with source and Haddock archives, without publication, then stop.++As a maintainer, I have readable documentation, a Spec Kit constitution and task record, a draft pull request, CI and review rules, and a concrete transfer runbook that accounts for live Hackage metadata.++```mermaid+flowchart LR+  Contributor -->|clone and develop| Source+  Source -->|build and verify| Checks+  Checks -->|evidence| Review+  Review -->|operator authorizes transfer| Transfer+  Hackage -->|published metadata links| Source+```++## Acceptance++- Record an actual untouched GitHub GHC 9.12 build verdict before planning. Preserve its command, output and source identity.+- Reconcile GitLab history and published 0.1.0.1 sources before source modernization; retain attribution and existing API.+- Cabal 3.x replaces Stack/hpack; a locked Nix flake builds and runs every test with GHC 9.12.3.+- Flake checks execute tests, standard Fourmolu formatting, HLint, package checks and strict documentation builds. Compare the source API against hash-pinned Hackage 0.1.0.1, exercise `onEach` across all Tasty wrappers, and require clean package checks plus complete package Haddocks from the unpacked archive. Embed the current rendered Haddock reference in MkDocs for every library, regardless of Hackage publication, and reject stale files or broken local API links. Deploy to GitHub Pages and verify served guide/API bytes. CI has a Build Gate, named downstream checks and a development-shell build.+- Spec Kit is initialized with a filled constitution and consistent spec, plan and tasks. README, user stories, architecture and development docs contain usable examples and diagrams.+- Configure applicable new-repository conventions on the existing GitHub repository; record any settings requiring operator interaction or the future organization transfer.+- Provide release preparation without uploading to Hackage or cutting a release. Published 0.1.0.1 and GitHub 0.1.0.0 cannot be treated as interchangeable package artifacts.+- Prepare the ownership-transfer procedure, verification and recovery steps in the lane handoffs. The operator subsequently authorized execution; transfer the existing repository and verify identity, redirects and settings.++## Constraints++Use the existing repository and preserve `master`. Do not create a destination repository that would block transfer. No comments or personal messages. Track disk growth and notify the operator before exceeding approximately 10 GiB. No `nixos-rebuild`.
+ specs/001-modernization/tasks.md view
@@ -0,0 +1,30 @@+# Modernization tasks++## Build and preserve published behavior++- [x] Build untouched GitHub on GHC 9.12.3 and retain full output.+- [x] Run the six original tests successfully.+- [x] Compare GitLab ancestry and Hackage source; retain the nine GitLab commits.+- [x] Initialize Spec Kit scripts/templates and filled constitution.+- [x] Modernize Cabal, retire Stack/hpack and preserve API.+- [x] Add locked haskell.nix build, shell, executed checks and apps.+- [x] Add regression coverage for the published dependent-test decorator fix.++## Contribute and review++- [x] Add just recipes, formatting, lint and strict warning gates.+- [x] Add README, user stories, architecture and development documentation.+- [x] Add CI Build Gate, concern jobs, development-shell gate and docs deployment.+- [x] Install PR template; open draft PR; assign and label it.+- [x] Apply labels, workflow permissions, branch rules and Pages settings.+- [x] Initialize wiki or hand off the specific initialization requirement.+- [x] Prepare release configuration and source artifacts without publication.++## Transfer with evidence++- [x] Write transfer runbook covering GitHub redirects, GitLab and Hackage links, settings and recovery.+- [x] Run fresh local gates and effective negative controls.+- [x] Validate source archive and documented example.+- [x] Push the reviewed branch and record CI status and remaining operator steps.++The modernization is pushed to [PR 3](https://github.com/lambdasistemi/tasty-bdd/pull/3). Local checkout/archive gates pass; GitHub CI is tracked on the PR and is not implied green by local results. The authorized ownership transfer is complete and the wiki is published. The operator subsequently requested candidate 0.1.0.2 preparation. PR acceptance and publication remain maintainer steps.
src/System/CaptureStdout.hs view
@@ -1,7 +1,9 @@-{--BSD3 credits to Merijn Verstraaten--}+{- | Capture process-wide standard output while suppressing action exceptions. +Module: System.CaptureStdout+License: BSD-3-Clause+Credits: Merijn Verstraaten+-} module System.CaptureStdout (captureStdout) where  import Control.Exception (SomeException, bracket, try)@@ -11,17 +13,18 @@ import System.IO (Handle, SeekMode (..), hFlush, hSeek, stdout) import System.IO.Temp (withSystemTempFile) +-- | Capture output using a temporary file. Concurrent captures are not isolated. captureStdout :: String -> IO () -> IO Text captureStdout tmp act = withSystemTempFile tmp $ \_ hnd -> do-  let redirect :: IO Handle-      redirect = do-        hFlush stdout-        hDuplicate stdout <* hDuplicateTo hnd stdout+    let redirect :: IO Handle+        redirect = do+            hFlush stdout+            hDuplicate stdout <* hDuplicateTo hnd stdout -      undo :: Handle -> IO ()-      undo h = hFlush stdout >> hDuplicateTo h stdout+        undo :: Handle -> IO ()+        undo h = hFlush stdout >> hDuplicateTo h stdout -  _ <- bracket redirect undo $ \_ -> try act :: IO (Either SomeException ())+    _ <- bracket redirect undo $ \_ -> try act :: IO (Either SomeException ()) -  hSeek hnd AbsoluteSeek 0-  hGetContents hnd+    hSeek hnd AbsoluteSeek 0+    hGetContents hnd
src/Test/BDD/Language.hs view
@@ -7,49 +7,48 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE Rank2Types #-}-{-# LANGUAGE TemplateHaskell #-} --- |------ Module    :  Test.BDD.Language--- Copyright :  (c) Paolo Veronelli, Pavlo Kerestey 2017--- License   :  BSD3--- Maintainer:  paolo.veronelli@gmail.com--- Stability :  experimental--- Portability: non-portable--------- The constrained language to define behaviors in BDD terminology------ @--- exampleL :: TestTree--- exampleL = testBehavior "Test sequence"---     $ Given (print "Some effect")---     $ Given (print "Another effect")---     $ GivenAndAfter (print "Aquiring resource" >> return "Resource 1")---                    (print . ("Release "++))---     $ GivenAndAfter (print "Aquiring resource" >> return "Resource 2")---                    (print . ("Release "++))---     $ When (print "Action returning" >> return ([1..10]++[100..106]) :: IO [Int])---     $ Then (@?= ([1..10]++[700..706]))---     $ End--- @+{- |++Module    :  Test.BDD.Language+Copyright :  (c) Paolo Veronelli, Pavlo Kerestey 2017+License   :  BSD3+Maintainer:  paolo.veronelli@gmail.com+Stability :  experimental+Portability: non-portable+++The constrained language to define behaviors in BDD terminology++@+exampleL :: TestTree+exampleL = testBehavior "Test sequence"+    $ Given (print "Some effect")+    $ Given (print "Another effect")+    $ GivenAndAfter (print "Aquiring resource" >> return "Resource 1")+                   (print . ("Release "++))+    $ GivenAndAfter (print "Aquiring resource" >> return "Resource 2")+                   (print . ("Release "++))+    $ When (print "Action returning" >> return ([1..10]++[100..106]) :: IO [Int])+    $ Then (@?= ([1..10]++[700..706]))+    $ End+@+-} module Test.BDD.Language-  ( Language (..)-  , BDDPreparing-  , BDDTesting-  , BDDTest (..)-  , TestContext (..)-  , context-  , when-  , tests-  , interpret-  , Phase (..)-  )+    ( Language (..)+    , BDDPreparing+    , BDDTesting+    , BDDTest (..)+    , TestContext (..)+    , context+    , when+    , tests+    , interpret+    , Phase (..)+    ) where  import Lens.Micro-import Lens.Micro.TH  -- | Separating the 2 phases by type data Phase = Preparing | Testing@@ -59,42 +58,64 @@  -- | Bare hoare language data Language m t q a where-  -- | action to prepare the test-  Given-    :: m ()-    -> Language m t q 'Preparing-    -> Language m t q 'Preparing-  -- | action to prepare the test, and related teardown action-  GivenAndAfter-    :: m r-    -> (r -> m ())-    -> Language m t q 'Preparing-    -> Language m t q 'Preparing-  -- | core logic of the test (last preparing action)-  When-    :: m t-    -> Language m t q 'Testing-    -> Language m t q 'Preparing-  -- | action producing a test-  Then-    :: (t -> m q)-    -> Language m t q 'Testing-    -> Language m t q 'Testing-  -- | final placeholder-  End :: Language m t q 'Testing+    -- | action to prepare the test+    Given+        :: m ()+        -> Language m t q 'Preparing+        -> Language m t q 'Preparing+    -- | action to prepare the test, and related teardown action+    GivenAndAfter+        :: m r+        -> (r -> m ())+        -> Language m t q 'Preparing+        -> Language m t q 'Preparing+    -- | core logic of the test (last preparing action)+    When+        :: m t+        -> Language m t q 'Testing+        -> Language m t q 'Preparing+    -- | action producing a test+    Then+        :: (t -> m q)+        -> Language m t q 'Testing+        -> Language m t q 'Testing+    -- | final placeholder+    End :: Language m t q 'Testing  -- | Result of this module interpreter data BDDTest m t q = BDDTest-  { -- | tests from 't'-    _tests :: [t -> m q]-  , -- | test context-    _context :: [TestContext m]-  , -- | when action to compute 't'-    _when :: m t-  }+    { _tests :: [t -> m q]+    -- ^ tests from @t@+    , _context :: [TestContext m]+    -- ^ test context+    , _when :: m t+    -- ^ when action to compute @t@+    } -makeLenses ''BDDTest+-- | Lens for the ordered preparation actions and their teardowns.+context+    :: (Functor f)+    => ([TestContext m] -> f [TestContext m])+    -> BDDTest m t q+    -> f (BDDTest m t q)+context f (BDDTest ts c w) = (\c' -> BDDTest ts c' w) <$> f c +-- | Lens for the assertions, allowing their result type to change.+tests+    :: (Functor f)+    => ([t -> m q1] -> f [t -> m q2])+    -> BDDTest m t q1+    -> f (BDDTest m t q2)+tests f (BDDTest ts c w) = (\ts' -> BDDTest ts' c w) <$> f ts++-- | Lens for the action whose result is supplied to the assertions.+when+    :: (Functor f)+    => (m t -> f (m t))+    -> BDDTest m t q+    -> f (BDDTest m t q)+when f (BDDTest ts c w) = BDDTest ts c <$> f w+ -- | Preparing language types type BDDPreparing m t q = Language m t q 'Preparing @@ -102,15 +123,15 @@ type BDDTesting m t q = Language m t q 'Testing  -- | An interpreter collecting the actions-interpret :: Monad m => Language m t q a -> BDDTest m t q+interpret :: (Monad m) => Language m t q a -> BDDTest m t q interpret (Given given p) =-  interpret $ GivenAndAfter given (const $ return ()) p+    interpret $ GivenAndAfter given (const $ return ()) p interpret (GivenAndAfter given after p) =-  over context ((:) $ TestContext given after) $-    interpret p+    over context ((:) $ TestContext given after) $+        interpret p interpret (When fa p) =-  set when fa $ interpret p-interpret (Then ca p) = over tests ((:) ca) $ interpret p+    set when fa $ interpret p+interpret (Then ca p) = over tests (ca :) $ interpret p interpret End =-  BDDTest [] [] $-    error "End on its own does not make sense as a test"+    BDDTest [] [] $+        error "End on its own does not make sense as a test"
src/Test/BDD/LanguageFree.hs view
@@ -11,35 +11,31 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} --- | Free monads to introduce do notation in ''Language''------ Module    :--- Copyright :  (c) Paolo Veronelli 2017--- License   :  All rights reserved--- Maintainer:  paolo.veronelli@gmail.com--- Stability :  experimental--- Portability: non-portable+{- | Free monads for composing Given/When/Then scenarios.++Module    : Test.BDD.LanguageFree+Copyright :  (c) Paolo Veronelli 2017+License   :  BSD-3-Clause+Maintainer:  paolo.veronelli@gmail.com+Stability :  experimental+Portability: non-portable+-} module Test.BDD.LanguageFree-  ( given-  , givenAndAfter_-  , givenAndAfter-  , then_-  , then__-  , when_-  , GivenFree-  , ThenFree-  , FreeBDD-  , testFreeBDD-  , BDDResult (..)-  --        , testBehaviorFree-  ---  ---  )+    ( given+    , givenAndAfter_+    , givenAndAfter+    , then_+    , then__+    , when_+    , GivenFree+    , ThenFree+    , FreeBDD+    , testFreeBDD+    , BDDResult (..)+    ) where --- import Control.Exception import Control.Monad.Catch-import Control.Monad.Cont import Control.Monad.Free import Control.Monad.Reader @@ -48,73 +44,92 @@  -- | Bare hoare language data Language m a where-  -- | action to prepare the test-  Given :: m a -> (a -> Language m 'Preparing) -> Language m 'Preparing-  -- | action to prepare the test, and related teardown action-  GivenAndAfter :: m (a, r) -> (r -> m ()) -> (a -> Language m 'Preparing) -> Language m 'Preparing-  -- | core logic of the test (last preparing action)-  When :: m t -> Language m ('Testing t) -> Language m 'Preparing-  -- | action producing a test-  Then :: (t -> m ()) -> Language m ('Testing t) -> Language m ('Testing t)-  -- | final placeholder-  End :: Language m x-  And :: Language m 'Preparing -> Language m 'Preparing -> Language m 'Preparing+    -- | action to prepare the test+    Given :: m a -> (a -> Language m 'Preparing) -> Language m 'Preparing+    -- | action to prepare the test, and related teardown action+    GivenAndAfter+        :: m (a, r)+        -> (r -> m ())+        -> (a -> Language m 'Preparing)+        -> Language m 'Preparing+    -- | core logic of the test (last preparing action)+    When :: m t -> Language m ('Testing t) -> Language m 'Preparing+    -- | action producing a test+    Then+        :: (t -> m ()) -> Language m ('Testing t) -> Language m ('Testing t)+    -- | final placeholder+    End :: Language m x+    And+        :: Language m 'Preparing+        -> Language m 'Preparing+        -> Language m 'Preparing +-- | A scenario result carrying the recorded teardown action. data BDDResult m = Failed SomeException (m ()) | Succeded (m ())  type CJR m = ReaderT (m ()) m (BDDResult m) -catchCJR :: MonadCatch m => CJR m -> CJR m+catchCJR :: (MonadCatch m) => CJR m -> CJR m catchCJR f = catch f $ asks . Failed -stepIn :: MonadCatch m => m a -> (a -> CJR m) -> CJR m+stepIn :: (MonadCatch m) => m a -> (a -> CJR m) -> CJR m stepIn g q = catchCJR (lift g >>= q) -interpret :: forall m. MonadCatch m => Language m 'Preparing -> m (BDDResult m)+interpret+    :: forall m. (MonadCatch m) => Language m 'Preparing -> m (BDDResult m) interpret y = runReaderT (interpret' y) (return ())   where     interpret' :: Language m 'Preparing -> CJR m     interpret' (Given g p) = stepIn g $ interpret' . p     interpret' (GivenAndAfter g z p) =-      stepIn g $ \(x, r) -> local (z r >>) $ interpret' $ p x+        stepIn g $ \(x, r) -> local (z r >>) $ interpret' $ p x     interpret' (When fa p) =-      stepIn fa $ \x -> interpretT' x p+        stepIn fa $ \x -> interpretT' x p     interpret' (And f g) = do-      r <- interpret' f-      case r of-        Succeded _ -> interpret' g-        w -> pure w+        r <- interpret' f+        case r of+            Succeded _ -> interpret' g+            w -> pure w     interpret' End = asks Succeded     interpretT' :: t -> Language m ('Testing t) -> CJR m     interpretT' _ End = asks Succeded     interpretT' x (Then f p) =-      stepIn (f x) $ \() -> interpretT' x p+        stepIn (f x) $ \() -> interpretT' x p +-- | Preparation instructions for the free-monad interface. data GivenFree m a where-  GivenFree :: m b -> (b -> a) -> GivenFree m a-  GivenAndAfterFree :: m (b, r) -> (r -> m ()) -> (b -> a) -> GivenFree m a-  WhenFree :: m t -> Free (ThenFree m t) c -> a -> GivenFree m a+    GivenFree :: m b -> (b -> a) -> GivenFree m a+    GivenAndAfterFree+        :: m (b, r) -> (r -> m ()) -> (b -> a) -> GivenFree m a+    WhenFree :: m t -> Free (ThenFree m t) c -> a -> GivenFree m a +-- | Assertions that consume the result of a scenario action. data ThenFree m t a-  = ThenFree (t -> m ()) a-  deriving (Functor)+    = ThenFree (t -> m ()) a+    deriving (Functor)  instance Functor (GivenFree m) where-  fmap f (GivenFree m x) = GivenFree m $ f <$> x-  fmap f (GivenAndAfterFree mr rm x) = GivenAndAfterFree mr rm $ f <$> x-  fmap f (WhenFree mt ft x) = WhenFree mt ft $ f x+    fmap f (GivenFree m x) = GivenFree m $ f <$> x+    fmap f (GivenAndAfterFree mr rm x) = GivenAndAfterFree mr rm $ f <$> x+    fmap f (WhenFree mt ft x) = WhenFree mt ft $ f x +-- | A scenario built from preparation instructions. type FreeBDD m x = Free (GivenFree m) x +-- | Run a preparation action and return its value to subsequent steps. given :: m a -> Free (GivenFree m) a given m = liftF $ GivenFree m id +-- | Acquire a value and resource, recording the resource teardown. givenAndAfter :: m (b, r) -> (r -> m ()) -> Free (GivenFree m) b givenAndAfter g td = liftF $ GivenAndAfterFree g td id -givenAndAfter_ :: Functor m => m r -> (r -> m ()) -> Free (GivenFree m) ()+-- | Acquire a resource and record its teardown without returning a value.+givenAndAfter_+    :: (Functor m) => m r -> (r -> m ()) -> Free (GivenFree m) () givenAndAfter_ g td = liftF $ GivenAndAfterFree (((),) <$> g) td id +-- | Run the scenario action and feed its result to the assertions. when_ :: m t -> Free (ThenFree m t) b -> Free (GivenFree m) () when_ mt ts = liftF $ WhenFree mt ts () @@ -125,18 +140,21 @@ bddFree :: Free (GivenFree m) x -> Language m 'Preparing bddFree (Free (GivenFree m f)) = Given m $ bddFree <$> f bddFree (Free (GivenAndAfterFree mr rm f)) =-  GivenAndAfter mr rm $ bddFree <$> f+    GivenAndAfter mr rm $ bddFree <$> f bddFree (Free (WhenFree mt ts f)) = And (When mt $ thens ts) (bddFree f) bddFree (Pure _) = End +-- | Add an assertion using the scenario action result. then_ :: (t -> m ()) -> Free (ThenFree m t) () then_ m = liftF $ ThenFree m () +-- | Add an assertion independent of the scenario action result. then__ :: m () -> Free (ThenFree m t) () then__ = then_ . const +-- | Execute a scenario and return its result with recorded teardown. testFreeBDD-  :: (MonadCatch m)-  => Free (GivenFree m) x-  -> m (BDDResult m)+    :: (MonadCatch m)+    => Free (GivenFree m) x+    -> m (BDDResult m) testFreeBDD = interpret . bddFree
src/Test/Tasty/Bdd.hs view
@@ -6,47 +6,48 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# OPTIONS_GHC -fno-warn-orphans #-} --- |--- Module    :  Test.Tasty.Bdd--- Copyright :  (c) Paolo Veronelli, Pavlo Kerestey 2017-2020--- License   :  All rights reserved--- Maintainer:  paolo.veronelli@gmail.com--- Stability :  experimental--- Portability: non-portable------ Tasty driver for 'Language'+{- |+Module    :  Test.Tasty.Bdd+Copyright :  (c) Paolo Veronelli, Pavlo Kerestey 2017-2020+License   :  BSD-3-Clause+Maintainer:  paolo.veronelli@gmail.com+Stability :  experimental+Portability: non-portable++Tasty driver for 'Language'+-} module Test.Tasty.Bdd-  ( (@?=)-  , (@?/=)-  , (^?=)-  , (^?/=)-  , acquire-  , acquirePure-  , Phase (..)-  , Language (..)-  , testBehavior-  , testBehaviorIO-  , BDDTesting-  , BDDPreparing-  , TestableMonad (..)-  , failFastIngredients-  , failFastTester-  , prettyDifferences-  , beforeEach-  , afterEach-  , before-  , after-  , onEach-  , captureStdout-  , testBehaviorF-  )+    ( (@?=)+    , (@?/=)+    , (^?=)+    , (^?/=)+    , acquire+    , acquirePure+    , Phase (..)+    , Language (..)+    , testBehavior+    , testBehaviorIO+    , BDDTesting+    , BDDPreparing+    , TestableMonad (..)+    , failFastIngredients+    , failFastTester+    , prettyDifferences+    , beforeEach+    , afterEach+    , before+    , after+    , onEach+    , captureStdout+    , testBehaviorF+    ) where  import Control.Monad.Catch-  ( Exception (..)-  , MonadCatch (..)-  , MonadThrow (..)-  )+    ( Exception (..)+    , MonadCatch (..)+    , MonadThrow (..)+    ) import Control.Monad.IO.Class (MonadIO, liftIO) import Data.Tagged (Tagged (..)) import Data.TreeDiff@@ -56,84 +57,86 @@ import Test.BDD.Language import Test.BDD.LanguageFree import Test.Tasty-  ( withResource-  )+    ( withResource+    ) import Test.Tasty.Ingredients.FailFast (FailFast (..), failFast) import Test.Tasty.Options (OptionDescription (..), lookupOption) import Test.Tasty.Providers-  ( IsTest (..)-  , singleTest-  , testFailed-  , testPassed-  )+    ( IsTest (..)+    , singleTest+    , testFailed+    , testPassed+    ) import Test.Tasty.Runners import Text.Printf (printf)  data FreeBDDCase m = FreeBDDCase (m Result -> IO Result) (m (BDDResult m)) +-- | Adapt a free-monad scenario to Tasty using a runner for its monad. testBehaviorF-  :: (Typeable m, MonadCatch m)-  => (m Result -> IO Result)-  -> String-  -> FreeBDD m x-  -> TestTree+    :: (Typeable m, MonadCatch m)+    => (m Result -> IO Result)+    -> String+    -> FreeBDD m x+    -> TestTree testBehaviorF f s = singleTest s . FreeBDDCase f . testFreeBDD  instance (MonadCatch m, Typeable m) => IsTest (FreeBDDCase m) where-  run _ (FreeBDDCase rc test) _ = rc $ test >>= g-    where-      g (Failed e td) = do-        td-        maybe-          (throwM e)-          (return . testFailed . testFailMessage)-          $ fromException e-      g (Succeded td) = td >> return (testPassed "")-  testOptions = Tagged [Option (Proxy :: Proxy FailFast)]+    run _ (FreeBDDCase rc test) _ = rc $ test >>= g+      where+        g (Failed e td) = do+            td+            maybe+                (throwM e)+                (return . testFailed . testFailMessage)+                $ fromException e+        g (Succeded td) = td >> return (testPassed "")+    testOptions = Tagged [Option (Proxy :: Proxy FailFast)]  -- | testable monads can map to IO a Tasty Result class (MonadCatch m, MonadIO m, Monad m, Typeable m) => TestableMonad m where-  runCase :: m Result -> IO Result+    runCase :: m Result -> IO Result  instance TestableMonad IO where-  runCase = id+    runCase = id  -- | any testable monad can make a BDDTest a tasty test instance-  (Typeable t, TestableMonad m)-  => IsTest (BDDTest m t ())-  where-  run os (BDDTest ts rup w) f = runCase $ do-    teardowns <--      sequence_ . reverse <$> mapM (\(TestContext g a) -> a <$> g) rup-    resultOfWhen <- w-    let loop [] = return Nothing-        loop (then' : xs) = do-          liftIO $-            f-              (Progress-                 ""-                 (fromIntegral (length xs) / fromIntegral (length ts)))-          (then' resultOfWhen >> loop xs)-            `catch` (\(EqualityDoesntHold e) -> return (Just e))-    resultOfThen <- loop ts-    case resultOfThen of-      Just reason -> do-        case lookupOption os of-          FailFast False -> teardowns-          _ -> return ()-        return $ testFailed reason-      Nothing -> teardowns >> return (testPassed "")-  testOptions = Tagged [Option (Proxy :: Proxy FailFast)]+    (Typeable t, TestableMonad m)+    => IsTest (BDDTest m t ())+    where+    run os (BDDTest ts rup w) f = runCase $ do+        teardowns <-+            sequence_ . reverse <$> mapM (\(TestContext g a) -> a <$> g) rup+        resultOfWhen <- w+        let loop [] = return Nothing+            loop (then' : xs) = do+                liftIO $+                    f+                        ( Progress+                            ""+                            (fromIntegral (length xs) / fromIntegral (length ts))+                        )+                (then' resultOfWhen >> loop xs)+                    `catch` (\(EqualityDoesntHold e) -> return (Just e))+        resultOfThen <- loop ts+        case resultOfThen of+            Just reason -> do+                case lookupOption os of+                    FailFast False -> teardowns+                    _ -> return ()+                return $ testFailed reason+            Nothing -> teardowns >> return (testPassed "")+    testOptions = Tagged [Option (Proxy :: Proxy FailFast)]  -- | show a coloured difference of 2 values prettyDifferences :: (ToExpr a) => a -> a -> String prettyDifferences a1 a2 =-  show $ ansiWlEditExpr $ exprDiff (toExpr a1) (toExpr a2)+    show $ ansiWlEditExpr $ exprDiff (toExpr a1) (toExpr a2)  -- internal exception to trigger visual inspection on output newtype EqualityDoesntHold = EqualityDoesntHold {testFailMessage :: String}-  deriving (Show, Typeable)+    deriving (Show)  instance Exception EqualityDoesntHold @@ -142,41 +145,48 @@ -- | equality test which show pretty differences on fail (@?=) :: (ToExpr a, Eq a, Typeable a, MonadThrow m) => a -> a -> m () a1 @?= a2 =-  if a1 == a2-    then return ()-    else-      throwM $-        EqualityDoesntHold $-          printf "Expected equality:\n%s" $-            prettyDifferences a1 a2+    if a1 == a2+        then return ()+        else+            throwM $+                EqualityDoesntHold $+                    printf "Expected equality:\n%s" $+                        prettyDifferences a1 a2  -- | inequality test which show pretty differences on fail (@?/=) :: (ToExpr a, Eq a, Typeable a, MonadThrow m) => a -> a -> m () a1 @?/= a2 =-  if a1 /= a2-    then return ()-    else-      throwM $-        EqualityDoesntHold $-          printf "Expected inequality:\n%s" $-            prettyDifferences a1 a2+    if a1 /= a2+        then return ()+        else+            throwM $+                EqualityDoesntHold $+                    printf "Expected inequality:\n%s" $+                        prettyDifferences a1 a2 --- | shortcut to ignore the input and run another action instead in Then--- matching equality-(^?=) :: (ToExpr a, Eq a, Typeable a, MonadThrow m) => m a -> a -> b -> m ()+{- | shortcut to ignore the input and run another action instead in Then+matching equality+-}+(^?=)+    :: (ToExpr a, Eq a, Typeable a, MonadThrow m) => m a -> a -> b -> m () f ^?= t = const $ f >>= (@?= t) --- | shortcut to ignore the input and run another action instead in Then--- matching inequality-(^?/=) :: (ToExpr a, Eq a, Typeable a, MonadThrow m) => m a -> a -> b -> m ()+{- | shortcut to ignore the input and run another action instead in Then+matching inequality+-}+(^?/=)+    :: (ToExpr a, Eq a, Typeable a, MonadThrow m) => m a -> a -> b -> m () f ^?/= t = const $ f >>= (@?/= t) --- | interpret 'Bdd' sentence to a single 'TestTree'+-- | interpret a 'Language' scenario to a single 'TestTree' testBehavior-  :: (MonadIO m, TestableMonad m, Typeable t)-  => String -- ^ test name-  -> BDDPreparing m t () -- ^ bdd test definition-  -> TestTree -- ^ resulting tasty test+    :: (MonadIO m, TestableMonad m, Typeable t)+    => String+    -- ^ test name+    -> BDDPreparing m t ()+    -- ^ bdd test definition+    -> TestTree+    -- ^ resulting tasty test testBehavior s = singleTest s . interpret  -- | specialize withResource to prepend an action@@ -205,17 +215,22 @@ afterEach = onEach . after  -- | specialize withResource to just acquire a resource-acquire :: MonadIO m => IO a -> (m a -> TestTree) -> TestTree+acquire :: (MonadIO m) => IO a -> (m a -> TestTree) -> TestTree acquire f g = withResource f (const $ return ()) (g . liftIO) +-- | Acquire a resource and expose its value through the resource callback. acquirePure :: IO a -> (a -> TestTree) -> TestTree acquirePure f g = acquire f $ g . unsafePerformIO +-- | Acquire a constructor-based scenario in IO and adapt it to Tasty. testBehaviorIO-  :: (Typeable t, MonadIO m, TestableMonad m)-  => String -- ^ test name-  -> IO (BDDPreparing m t ()) -- ^ bdd test definition-  -> TestTree -- ^ resulting tasty test+    :: (Typeable t, MonadIO m, TestableMonad m)+    => String+    -- ^ test name+    -> IO (BDDPreparing m t ())+    -- ^ bdd test definition+    -> TestTree+    -- ^ resulting tasty test testBehaviorIO s f = acquirePure f (testBehavior s)  -- | default test runner fail-fast aware
tasty-bdd.cabal view
@@ -1,82 +1,108 @@-cabal-version: 1.12+cabal-version:      3.0+name:               tasty-bdd+version:            0.1.0.2+synopsis:           BDD tests language and tasty provider+description:+  A typed Given/When/Then language and a free-monad interface for Tasty,+  with ordered setup, reverse-order teardown, recursive test decorators+  and structural equality diagnostics. --- This file has been generated from package.yaml by hpack version 0.33.0.------ see: https://github.com/sol/hpack------ hash: 2092064cb96d2bfbdd0184d0d9652a09a67bfeb44af393b1da2095d85d4e84a8+category:           Test+stability:          experimental+homepage:           https://github.com/lambdasistemi/tasty-bdd+bug-reports:        https://github.com/lambdasistemi/tasty-bdd/issues+author:             Paolo Veronelli, Pavlo Kerestey+maintainer:         paolo.veronelli@gmail.com+copyright:          2017 Paolo Veronelli+license:            BSD-3-Clause+license-file:       LICENSE+build-type:         Simple+tested-with:        GHC ==9.12.3+extra-doc-files:+  .specify/memory/constitution.md+  CHANGELOG.md+  docs/*.md+  docs/architecture/*.md+  docs/stories/*.md+  README.md+  specs/001-modernization/*.md -name:           tasty-bdd-version:        0.1.0.1-synopsis:       BDD tests language and tasty provider-description:    https://gitlab.com/devs.global.de/tasty-bdd/-/blob/master/README.md-category:       Test-homepage:       https://gitlab.com/devs.global.de/tasty-bdd-author:         Paolo Veronelli, Pavlo Kerestey-maintainer:     paolo.veronelli@gmail.com-copyright:      2017 Paolo Veronelli-license:        BSD3-license-file:   LICENSE-build-type:     Simple extra-source-files:-    README.md+  .github/actionlint.yaml+  .github/pull_request_template.md+  .github/workflows/*.yml+  .specify/scripts/bash/*.sh+  .specify/templates/*.md+  cabal.project+  flake.lock+  flake.nix+  fourmolu.yaml+  justfile+  mkdocs.yml+  nix/*.nix+  tools/*.py+  tools/*.sh +flag werror+  description: Treat compiler warnings as errors during development and CI+  default:     False+  manual:      True++common warnings+  default-language: Haskell2010+  ghc-options:+    -Wall -Wunused-imports -Wunused-packages -Wmissing-export-lists+    -Wname-shadowing -Wdeferred-out-of-scope-variables+    -Wpartial-type-signatures++  if flag(werror)+    ghc-options: -Werror+ library+  import:          warnings   exposed-modules:-      Test.Tasty.Bdd-      Test.BDD.Language-      Test.BDD.LanguageFree-      System.CaptureStdout-  other-modules:-      Paths_tasty_bdd-  hs-source-dirs:-      src-  default-extensions: OverloadedStrings-  ghc-options: -Wall+    System.CaptureStdout+    Test.BDD.Language+    Test.BDD.LanguageFree+    Test.Tasty.Bdd++  hs-source-dirs:  src   build-depends:-      HUnit-    , base >=4.7 && <5-    , exceptions-    , free-    , microlens-    , microlens-th-    , mtl-    , pretty-    , pretty-show-    , tagged-    , tasty-    , tasty-fail-fast-    , tasty-hunit-    , temporary-    , text-    , transformers-    , tree-diff-  default-language: Haskell2010+    , base             >=4.7   && <5+    , exceptions       >=0.10  && <0.11+    , free             >=5     && <6+    , microlens        >=0.4   && <0.5+    , mtl              >=2.2   && <2.4+    , tagged           >=0.8   && <0.9+    , tasty            >=1.2   && <1.6+    , tasty-fail-fast  >=0.0.3 && <0.1+    , temporary        >=1.3   && <1.4+    , text             >=1.2   && <2.2+    , tree-diff        >=0.2   && <0.5  test-suite test-  type: exitcode-stdio-1.0-  main-is: Main.hs-  other-modules:-      Paths_tasty_bdd-  hs-source-dirs:-      tests-  default-extensions: OverloadedStrings-  ghc-options: -Wall+  import:         warnings+  type:           exitcode-stdio-1.0+  main-is:        Main.hs+  hs-source-dirs: tests   build-depends:-      HUnit-    , aeson-    , aeson-qq-    , base >=4.7 && <5-    , exceptions-    , mtl-    , qm-interpolated-string-    , regex-posix-    , tasty+    , base                    >=4.7  && <5+    , HUnit                   >=1.6  && <1.7+    , tasty                   >=1.2  && <1.6     , tasty-bdd-    , tasty-expected-failure-    , tasty-fail-fast-    , tasty-hunit-    , temporary-    , text-    , transformers-  default-language: Haskell2010+    , tasty-expected-failure  >=0.12 && <0.13+    , tasty-hunit             >=0.10 && <0.11++test-suite example+  import:         warnings+  type:           exitcode-stdio-1.0+  main-is:        Main.hs+  hs-source-dirs: examples+  build-depends:+    , base       >=4.7 && <5+    , tasty      >=1.2 && <1.6+    , tasty-bdd++source-repository head+  type:     git+  location: https://github.com/lambdasistemi/tasty-bdd
tests/Main.hs view
@@ -1,150 +1,202 @@-{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-}-module Main where -import           Control.Concurrent.MVar-import           Control.Exception-import           Test.BDD.LanguageFree-import qualified Test.HUnit              as H-import           Test.Tasty-import           Test.Tasty.Bdd-import           Test.Tasty.HUnit        hiding ((@?=))+module Main (main) where++import Control.Concurrent.MVar+import Control.Exception+import Test.BDD.LanguageFree+import qualified Test.HUnit as H+import Test.Tasty hiding (after)+import qualified Test.Tasty as Tasty+import Test.Tasty.Bdd import Test.Tasty.ExpectedFailure+import Test.Tasty.HUnit hiding ((@?=))+import Test.Tasty.Runners (NumThreads (..))  main :: IO ()-main = defaultMain $ testGroup-    "All"-    [ testCase-            "givens and givenandafter order is respected, constructors language"-        $ do-              let-                  t write =-                      defaultMain-                          $ testBehavior "Test sequence"-                          $ Given (write "First effect")-                          $ Given (write "Another effect")-                          $ GivenAndAfter-                                (  write "Aquiring resource"-                                >> return "Resource 1"+main =+    defaultMain $+        testGroup+            "All"+            [ testCase+                "givens and givenandafter order is respected, constructors language"+                $ do+                    let+                        t write =+                            defaultMain+                                $ testBehavior "Test sequence"+                                $ Given (write "First effect")+                                $ Given (write "Another effect")+                                $ GivenAndAfter+                                    ( write "Aquiring resource"+                                        >> return "Resource 1"+                                    )+                                    (write . ("Release " ++))+                                $ GivenAndAfter+                                    ( write "Aquiring resource"+                                        >> return "Resource 2"+                                    )+                                    (write . ("Release " ++))+                                $ When (write "Action returning")+                                $ Then (\_ -> return ()) End+                    testTest+                        t+                        [ "First effect"+                        , "Another effect"+                        , "Aquiring resource"+                        , "Aquiring resource"+                        , "Action returning"+                        , "Release Resource 2"+                        , "Release Resource 1"+                        ]+            , testCase+                "3 givens and givenandafter order is respected, free language"+                $ do+                    let+                        t write = defaultMain $ testBehaviorF id "test free" $ do+                            q <- given $ do+                                write "First effect"+                                return (1 :: Int)+                            given $ write "Another effect"+                            q' <-+                                givenAndAfter+                                    ( write "Aquiring resource"+                                        >> return (q + 1, "Resource " <> show q)+                                    )+                                    $ write+                                        . ("Release " ++)+                            givenAndAfter_+                                ( write "Aquiring resource"+                                    >> return ("Resource " <> show q')                                 )-                                (write . ("Release " ++))-                          $ GivenAndAfter-                                (  write "Aquiring resource"-                                >> return "Resource 2"+                                $ write+                                    . ("Release " ++)+                            when_ (write "Action returning" >> return (q' + q)) $ do+                                then_ (@?= 7)+                                then_ (assertBool "less then" . (< 4))+                    {-when_ (write "Action returning2" >> return "different type") $ do+                        then_  (@?= ("different typ" :: String))-}+                    testTest+                        t+                        [ "First effect"+                        , "Another effect"+                        , "Aquiring resource"+                        , "Aquiring resource"+                        , "Action returning"+                        , -- , "Action returning2"+                          "Release Resource 2"+                        , "Release Resource 1"+                        ]+            , testCase+                "3 givens and givenandafter order is respected, free language, with exceptions"+                $ do+                    let+                        t write = defaultMain $ testBehaviorF id "test free" $ do+                            q <- given $ do+                                write "First effect"+                                return (1 :: Int)+                            given $ write "Another effect"+                            q' <-+                                givenAndAfter+                                    ( write "Aquiring resource"+                                        >> return (q + 1, "Resource " <> show q)+                                    )+                                    $ write+                                        . ("Release " ++)+                            givenAndAfter_+                                ( write "Aquiring resource"+                                    >> return ("Resource " <> show q')                                 )-                                (write . ("Release " ++))-                          $ When (write "Action returning")-                          $ Then (\_ -> return ()) End-              testTest-                  t-                  [ "First effect"-                  , "Another effect"-                  , "Aquiring resource"-                  , "Aquiring resource"-                  , "Action returning"-                  , "Release Resource 2"-                  , "Release Resource 1"-                  ]-    , testCase "3 givens and givenandafter order is respected, free language"-        $ do-              let-                  t write = defaultMain $ testBehaviorF id "test free" $ do-                      q <- given $ do-                          write "First effect"-                          return (1 :: Int)-                      given $ write "Another effect"-                      q' <--                          givenAndAfter-                              (  write "Aquiring resource"-                              >> return (q + 1, "Resource " <> show q)-                              )-                          $ write-                          . ("Release " ++)-                      givenAndAfter_-                              (  write "Aquiring resource"-                              >> return ("Resource " <> show q')-                              )-                          $ write-                          . ("Release " ++)-                      when_ (write "Action returning" >> return (q' + q)) $ do-                          then_ (@?= 7)-                          then_ (assertBool "less then" . (< 4))-                      {-when_ (write "Action returning2" >> return "different type") $ do-                          then_  (@?= ("different typ" :: String))-}-              testTest-                  t-                  [ "First effect"-                  , "Another effect"-                  , "Aquiring resource"-                  , "Aquiring resource"-                  , "Action returning"-                  -- , "Action returning2"-                  , "Release Resource 2"-                  , "Release Resource 1"-                  ]-    , testCase-            "3 givens and givenandafter order is respected, free language, with exceptions"-        $ do-              let-                  t write = defaultMain $ testBehaviorF id "test free" $ do-                      q <- given $ do-                          write "First effect"-                          return (1 :: Int)-                      given $ write "Another effect"-                      q' <--                          givenAndAfter-                              (  write "Aquiring resource"-                              >> return (q + 1, "Resource " <> show q)-                              )-                          $ write-                          . ("Release " ++)-                      givenAndAfter_-                              (  write "Aquiring resource"-                              >> return ("Resource " <> show q')-                              )-                          $ write-                          . ("Release " ++)-                      when_ ((1 :: Int) @?= 2 >> return (q' + q))-                          $ then_ (@?= 3)-              testTest-                  t-                  [ "First effect"-                  , "Another effect"-                  , "Aquiring resource"-                  , "Aquiring resource"-                  , "Release Resource 2"-                  , "Release Resource 1"-                  ]-    , testCase "recursive before decorations are honored" $ do-        let t write = defaultMain $ beforeEach (write 0) $ testGroup-                "g1"-                [ testCase "t1" $ write 1-                , testCase "t2" $ write 2-                , testGroup "g2" [testCase "t3" $ write 3]-                , testCase "t4" $ write 4-                ]-        testTest t ([0, 1, 0, 2, 0, 3, 0, 4] :: [Int])-    , testCase "recursive after decorations are honored" $ do-        let t write = defaultMain $ afterEach (write 0) $ testGroup-                "g1"-                [ testCase "t1" $ write 1-                , testCase "t2" $ write 2-                , testGroup "g2" [testCase "t3" $ write 3]-                , testCase "t4" $ write 4-                ]-        testTest t ([1, 0, 2, 0, 3, 0, 4, 0] :: [Int])-    , expectFail $ testBehaviorF runCase "didn't break tasty" $ do-        when_ (pure 42 :: IO Int) $ then_ $ \x -> x @?= 43--    ]-+                                $ write+                                    . ("Release " ++)+                            when_ ((1 :: Int) @?= 2 >> return (q' + q)) $+                                then_ (@?= 3)+                    testTest+                        t+                        [ "First effect"+                        , "Another effect"+                        , "Aquiring resource"+                        , "Aquiring resource"+                        , "Release Resource 2"+                        , "Release Resource 1"+                        ]+            , testCase "recursive before decorations are honored" $ do+                let t write =+                        defaultMain $+                            beforeEach (write 0) $+                                testGroup+                                    "g1"+                                    [ testCase "t1" $ write 1+                                    , testCase "t2" $ write 2+                                    , testGroup "g2" [testCase "t3" $ write 3]+                                    , testCase "t4" $ write 4+                                    ]+                testTest t ([0, 1, 0, 2, 0, 3, 0, 4] :: [Int])+            , testCase "recursive after decorations are honored" $ do+                let t write =+                        defaultMain $+                            afterEach (write 0) $+                                testGroup+                                    "g1"+                                    [ testCase "t1" $ write 1+                                    , testCase "t2" $ write 2+                                    , testGroup "g2" [testCase "t3" $ write 3]+                                    , testCase "t4" $ write 4+                                    ]+                testTest t ([1, 0, 2, 0, 3, 0, 4, 0] :: [Int])+            , testCase "decorators traverse dependent tests" $ do+                let t write =+                        defaultMain $+                            beforeEach (write 0) $+                                testGroup+                                    "dependencies"+                                    [ testCase "dependency" $ write 1+                                    , Tasty.after AllSucceed "dependency" $ testCase "dependent" $ write 2+                                    ]+                testTest t ([0, 1, 0, 2] :: [Int])+            , testCase "decorators preserve failed dependency blocking" $ do+                let t write =+                        defaultMain $+                            beforeEach (write 0) $+                                testGroup+                                    "blocked dependencies"+                                    [ testCase "prerequisite" $+                                        write 1 >> assertFailure "expected prerequisite failure"+                                    , Tasty.after AllSucceed "prerequisite" $ testCase "dependent" $ write 2+                                    ]+                testTest t ([0, 1] :: [Int])+            , testCase+                "onEach traverses resources, options, groups and dependencies"+                $ do+                    let t write =+                            defaultMain+                                $ localOption (NumThreads 1)+                                $ onEach (before $ write 0)+                                $ withResource+                                    (write 10)+                                    (const $ write 11)+                                $ \resource ->+                                    askOption $ \(NumThreads _) ->+                                        localOption (NumThreads 1) $+                                            testGroup+                                                "wrapped"+                                                [ testCase "first" $ resource >> write 1+                                                , Tasty.after AllSucceed "first" $+                                                    testGroup "nested" [testCase "second" $ write 2]+                                                ]+                    testTest t ([10, 0, 1, 0, 2, 11] :: [Int])+            , expectFail $ testBehaviorF runCase "didn't break tasty" $ do+                when_ (pure 42 :: IO Int) $ then_ $ \x -> x @?= 43+            ]  testTest :: (Show a, Eq a) => ((a -> IO ()) -> IO ()) -> [a] -> IO () testTest t r' = do     l <- newMVar []     let write x = modifyMVar_ l (return . (x :))-    _ <- handle (\(_ :: SomeException) -> return "")-        $ captureStdout "tasty-bdd-test-suite"-        $ t write+    _ <-+        handle (\(_ :: SomeException) -> return "") $+            captureStdout "tasty-bdd-test-suite" $+                t write     r <- readMVar l     r H.@?= reverse r'
+ tools/check-api.sh view
@@ -0,0 +1,32 @@+#!/usr/bin/env bash+set -euo pipefail+baseline=$1+candidate=$2+scratch=$(mktemp -d)+trap 'rm -rf "$scratch"' EXIT+browse() {+  local source=$1+  local output=$2+  (+    cd "$source"+    ghci -ignore-dot-ghci -v0 -isrc \+      src/Test/Tasty/Bdd.hs src/Test/BDD/Language.hs \+      src/Test/BDD/LanguageFree.hs src/System/CaptureStdout.hs <<'GHC'+:browse Test.Tasty.Bdd+:browse Test.BDD.Language+:browse Test.BDD.LanguageFree+:browse System.CaptureStdout+:quit+GHC+  ) > "$output" 2> "$output.errors"+  # GHCi can return success after a failed load: errors must also fail the gate.+  if grep -E 'error:|Failed,|Could not find module' "$output.errors"; then+    cat "$output.errors" >&2+    return 1+  fi+  grep -Fx 'onEach :: (TestTree -> TestTree) -> TestTree -> TestTree' "$output"+}+browse "$baseline" "$scratch/published.api"+browse "$candidate" "$scratch/candidate.api"+diff -u "$scratch/published.api" "$scratch/candidate.api"+echo 'Public API matches published tasty-bdd 0.1.0.1 on GHC 9.12.3.'
+ tools/check-hackage.sh view
@@ -0,0 +1,32 @@+#!/usr/bin/env bash+set -euo pipefail+source_archive=$1+artifacts=$2+mkdir -p "$artifacts"+artifacts=$(realpath "$artifacts")+scratch=$(mktemp -d)+trap 'rm -rf "$scratch"' EXIT+tar -xzf "$source_archive" -C "$scratch"+cd "$scratch"/tasty-bdd-*+chmod -R u+w .+# All dependencies come from the locked compiler environment, even in a sandbox.+printf 'active-repositories: :none\n' > cabal.project.local+printf 'remote-repo-cache: %s\nstore-dir: %s\nlogs-dir: %s\nworld-file: %s\n' \+  "$scratch/cache" "$scratch/store" "$scratch/logs" "$scratch/world" > "$scratch/cabal.config"+cabal --config-file="$scratch/cabal.config" check+cabal --config-file="$scratch/cabal.config" build all --offline --enable-tests -O0 -f-werror+cabal --config-file="$scratch/cabal.config" test all --offline --enable-tests -O0 -f-werror --test-show-details=direct+cabal --config-file="$scratch/cabal.config" haddock lib:tasty-bdd --offline --haddock-for-hackage -O0 -f-werror 2>&1 | tee "$artifacts/haddock.log"+python3 - "$artifacts/haddock.log" <<'PY'+import pathlib, re, sys+log = pathlib.Path(sys.argv[1]).read_text()+coverage = re.findall(r"^\s*(\d+)%.* in '([^']+)'", log, re.M)+expected = {'System.CaptureStdout', 'Test.BDD.Language', 'Test.BDD.LanguageFree', 'Test.Tasty.Bdd'}+assert {name for _, name in coverage} == expected, 'Missing module documentation coverage'+assert all(int(percent) == 100 for percent, _ in coverage), coverage+assert 'is out of scope' not in log, 'Unresolved local Haddock reference'+assert 'Missing documentation for:' not in log, 'Undocumented public declaration'+PY+cp "$source_archive" dist-newstyle/*-docs.tar.gz "$artifacts/"+(cd "$artifacts" && sha256sum ./*.tar.gz > SHA256SUMS)+echo "Hackage source and Haddock bundles checked: $artifacts"
+ tools/check-pages.py view
@@ -0,0 +1,23 @@+"""Verify that GitHub Pages serves the exact MkDocs build and commit."""+import pathlib+import sys+import time+import urllib.request++site, url, revision = pathlib.Path(sys.argv[1]), sys.argv[2].rstrip('/') + '/', sys.argv[3]+files = sorted(site.rglob('*.html')) + [site / 'assets/javascripts/mermaid.min.js']+for attempt in range(12):+    try:+        with urllib.request.urlopen(url + 'revision.txt', timeout=20) as response:+            assert response.read().decode().strip() == revision, 'stale deployed revision'+        for path in files:+            relative = path.relative_to(site).as_posix()+            with urllib.request.urlopen(url + relative, timeout=20) as response:+                assert response.read() == path.read_bytes(), f'stale or incorrect bytes: {relative}'+        print(f'Pages verified: {revision}; {len(files)} HTML/renderer files match the build.')+        break+    except (OSError, AssertionError) as error:+        if attempt == 11:+            raise+        print(f'Waiting for Pages: {error}', flush=True)+        time.sleep(5)
+ tools/check_presentation.py view
@@ -0,0 +1,148 @@+#!/usr/bin/env python3+"""Presentation gate for reader-facing Markdown: stories, diagrams, no index labels.++Usage: check_presentation.py [--register PATH ...] [--front FILE] ROOT [ROOT ...]++ROOT is a Markdown file or a directory searched recursively for *.md.+--register marks a file (or directory) as a machine-facing register whose+index labels are permitted; everything else is reader-facing.+--front names the front page (default: README.md if present under the first+directory root, else index.md) that must carry a story heading and a diagram.+--no-speech skips the speech binding. By default every page must have a sibling+PAGE.speech.json whose `_source.sha256` equals the page's current hash (see+stamp_speech.py) and whose keys are exactly the page's h2/h3 anchor ids: a page+edited without its speech being redone and re-stamped fails.++Exit 1 with a JSON report of every violation; exit 0 with a JSON summary.+"""+import hashlib+import json+import re+import sys+from pathlib import Path++LABEL = re.compile(r"(?<![\w/#.-])(?:R|S|D|N|M|INV)-?\d{1,3}[a-z]?(?![\w.-])")+FENCE = re.compile(r"^```")+MERMAID = re.compile(r"^```mermaid\b")+HEADING = re.compile(r"^(#{1,6})\s+(.*)$")+STORY = re.compile(r"\b(stor(y|ies)|who (this|it) is for|what you can do)\b", re.I)+STRUCTURAL = re.compile(r"(architecture|design|lifecycle|flow|protocol|overview|spec)", re.I)+++def parse_args(argv):+    registers, front, roots, speech = [], None, [], True+    it = iter(argv)+    for a in it:+        if a == "--register":+            registers.append(Path(next(it)).resolve())+        elif a == "--front":+            front = Path(next(it)).resolve()+        elif a == "--no-speech":+            speech = False+        else:+            roots.append(Path(a))+    if not roots:+        sys.exit(__doc__)+    return registers, front, roots, speech+++def slugify(text):+    """python-markdown's toc slugify, so ids match what MkDocs renders."""+    text = re.sub(r"[^\w\s-]", "", text.strip().lower())+    return re.sub(r"[\s]+", "-", text).strip("-")+++def check_speech(md, headings):+    """Return violations binding md's speech companion to md's current text."""+    speech = md.with_suffix(".speech.json")+    if not speech.exists():+        return [{"file": str(md), "rule": "speech-missing"}]+    try:+        data = json.loads(speech.read_text(encoding="utf-8"))+    except json.JSONDecodeError as e:+        return [{"file": str(speech), "rule": "speech-invalid-json", "detail": str(e)}]+    out = []+    stamp = (data.get("_source") or {}).get("sha256")+    actual = hashlib.sha256(md.read_bytes()).hexdigest()+    if stamp != actual:+        out.append({"file": str(speech), "rule": "speech-stale", "stamped": (stamp or "")[:12], "page": actual[:12]})+    keys = {k for k in data if not k.startswith("_")}+    # Speech covers the sections a reader can play: h2 and h3, as the reader and the generator do.+    ids = {slugify(text) for level, text in headings if level in (2, 3)}+    if keys != ids:+        out.append({"file": str(speech), "rule": "speech-headings-mismatch", "missing": sorted(ids - keys), "extra": sorted(keys - ids)})+    for k in keys:+        segs = data[k]+        if not (isinstance(segs, list) and segs and all(isinstance(s, dict) and isinstance(s.get("text"), str) and s["text"].strip() for s in segs)):+            out.append({"file": str(speech), "rule": "speech-empty-section", "section": k})+    return out+++def is_register(path, registers):+    return any(path == r or r in path.parents for r in registers)+++def scan(path):+    """Return (labels, mermaid_count, headings) for one Markdown file, skipping code fences."""+    labels, mermaid, headings, in_fence = [], 0, [], False+    for n, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):+        if FENCE.match(line):+            if not in_fence and MERMAID.match(line):+                mermaid += 1+            in_fence = not in_fence+            continue+        if in_fence:+            continue+        h = HEADING.match(line)+        if h:+            headings.append((len(h.group(1)), h.group(2).strip()))+        for m in LABEL.finditer(line):+            labels.append((n, m.group(0)))+    return labels, mermaid, headings+++def main(argv):+    registers, front, roots, speech = parse_args(argv)+    files = []+    for r in roots:+        files.extend(sorted(r.rglob("*.md")) if r.is_dir() else [r])+    files = [f.resolve() for f in files]+    if front is None:+        for r in roots:+            base = r if r.is_dir() else r.parent+            for cand in ("README.md", "index.md"):+                if (base / cand).exists():+                    front = (base / cand).resolve()+                    break+            if front:+                break+    violations, summary = [], {"files": len(files), "diagrams": 0, "registers": len(registers)}+    summary["speechBound"] = 0+    for f in files:+        labels, mermaid, headings = scan(f)+        summary["diagrams"] += mermaid+        rel = str(f)+        if speech:+            sv = check_speech(f, headings)+            violations.extend(sv)+            summary["speechBound"] += not sv+        if labels and not is_register(f, registers):+            sample = ", ".join(f"{n}:{lab}" for n, lab in labels[:6])+            violations.append({"file": rel, "rule": "index-labels", "count": len(labels), "sample": sample})+        if STRUCTURAL.search(f.stem) or STRUCTURAL.search(str(f.parent.name)):+            if mermaid == 0 and not is_register(f, registers):+                violations.append({"file": rel, "rule": "structural-page-without-diagram"})+        if front and f == front:+            if not any(STORY.search(text) for _, text in headings):+                violations.append({"file": rel, "rule": "front-page-without-story-heading"})+            if mermaid == 0:+                violations.append({"file": rel, "rule": "front-page-without-diagram"})+    if front is None:+        violations.append({"file": str(roots[0]), "rule": "no-front-page"})+    report = {"summary": summary, "violations": violations}+    print(json.dumps(report, indent=2))+    return 1 if violations else 0+++if __name__ == "__main__":+    sys.exit(main(sys.argv[1:]))
+ tools/embed-haddock.py view
@@ -0,0 +1,98 @@+"""Embed the checked Hackage rendering, adapting only its hosting links."""+import argparse+import html+from functools import cache+from html.parser import HTMLParser+import pathlib+import re+import tarfile+from urllib.parse import unquote, urljoin, urlsplit++parser = argparse.ArgumentParser(description=__doc__)+parser.add_argument('archive', type=pathlib.Path)+parser.add_argument('destination', type=pathlib.Path)+parser.add_argument('--check', action='store_true')+args = parser.parse_args()+rendered = {}+with tarfile.open(args.archive) as archive:+    for member in archive.getmembers():+        if not member.isfile():+            continue+        parts = pathlib.PurePosixPath(member.name).parts+        assert len(parts) > 1 and '..' not in parts and not member.name.startswith('/')+        relative = pathlib.Path(*parts[1:])+        # Haddock includes extra-doc-files; MkDocs owns those Markdown pages.+        if relative.suffix == '.md':+            continue+        data = archive.extractfile(member).read()+        if relative.suffix == '.html':+            text = data.decode()+            package = parts[0].removesuffix('-docs')+            text = text.replace(f'href="/package/{package}"', 'href="index.html"')+            text = text.replace('href="/package/', 'href="https://hackage.haskell.org/package/')+            # Haddock emits orphan-instance self-links without target IDs.+            anchors = set(re.findall(r'\bid="([^"]+)"', text))+            for fragment in re.findall(r'<a href="#([^"]+)" class="selflink">', text):+                if not {fragment, unquote(fragment)} & anchors:+                    old = f'<a href="#{fragment}" class="selflink">'+                    text = text.replace(old, f'<a id="{fragment}" href="#{fragment}" class="selflink">', 1)+                    anchors.add(fragment)+            for fragment in re.findall(r'<p class="src"><a href="#([^"]+)">', text):+                if not {fragment, unquote(fragment)} & anchors:+                    old = f'<p class="src"><a href="#{fragment}">'+                    text = text.replace(old, f'<p class="src"><a id="{fragment}" href="#{fragment}">', 1)+                    anchors.add(fragment)+            data = text.encode()+        rendered[relative] = data+modules = sorted(p for p in rendered if len(p.parts) == 1 and re.fullmatch(r'[A-Z][\w-]+\.html', p.name))+assert len(modules) == 4, 'All four public module renderings must be present'+assert pathlib.Path('doc-index.html') in rendered+links = ''.join(f'<li><a href="{p.name}">{html.escape(p.stem.replace("-", "."))}</a></li>' for p in modules)+rendered[pathlib.Path('index.html')] = (+    '<!doctype html><html lang="en"><meta charset="utf-8">'+    '<title>tasty-bdd API</title><link rel="stylesheet" href="linuwial.css">'+    f'<body><h1>{html.escape(package)} API</h1><ul>{links}</ul>'+    '<p><a href="doc-index.html">Symbol index</a> · '+    '<a href="../api/" target="_top">Documentation</a></p></body></html>'+).encode()+for relative, expected in rendered.items():+    destination = args.destination / relative+    if args.check:+        assert destination.is_file(), f'Missing embedded Haddock: {relative}'+        assert destination.read_bytes() == expected, f'Stale embedded Haddock: {relative}'+    else:+        destination.parent.mkdir(parents=True, exist_ok=True)+        destination.write_bytes(expected)+if args.check:+    class Links(HTMLParser):+        def __init__(self, text):+            super().__init__()+            self.links, self.anchors = [], set()+            self.feed(text)++        def handle_starttag(self, tag, attrs):+            for key, value in attrs:+                if key in ('href', 'src') and value:+                    self.links.append(value)+                if key == 'id' and value:+                    self.anchors.add(value)++    @cache+    def document(path):+        return Links(path.read_text())++    for relative, data in rendered.items():+        if relative.suffix != '.html':+            continue+        for link in Links(data.decode()).links:+            target = urlsplit(urljoin('https://docs.invalid/haddock/' + relative.as_posix(), link))+            if target.scheme not in ('http', 'https') or target.netloc != 'docs.invalid':+                continue+            path = args.destination.parent / unquote(target.path).lstrip('/')+            if target.path.endswith('/'):+                path /= 'index.html'+            assert path.is_file(), f'Broken API link in {relative}: {link}'+            if target.fragment and path.suffix == '.html':+                anchors = document(path).anchors+                assert {target.fragment, unquote(target.fragment)} & anchors, f'Broken API anchor in {relative}: {link}'+print(f'{"Verified" if args.check else "Embedded"} {len(rendered)} Haddock files from {args.archive.name}')