# Dependency constructs (`needs` / `has`)

## Usage

```ops
main @setup {
  needs [install] then {
    install-hooks()
  } catch error {
    log.error("Error installing dependencies:" $error)
  }
}

@try-setup {
  has [install] then {
    install-hooks()
  } missing dependencies {
    log.error("Missing dependencies:" $dependencies)
  }
}
```

## Grammar

```
NeedsThenBlock  = 'needs' '[' NeedRef* ']' 'then' '{' Statement* '}'
                  ('catch' IDENT '{' Statement* '}')?

HasBlock        = 'has' '[' NeedRef* ']' 'then' '{' Statement* '}'
                  ('missing' IDENT '{' Statement* '}')?

NeedRef         = '$' IDENT | '@' IDENT | IDENT
```

## IR representation

Canonical dependency graph and safety semantics live in [dependency-and-safety](../ir/dependency-and-safety.md).

```json
// needs-then (optional catch for dependency failures)
{
  "role": "branch",
  "payload": [
    {
      "role": "case",
      "payload": [
        { "role": "need", "payload": [
          { "role": "pipeline", "payload": "install" },
          { "role": "pipeline", "payload": "configure" }
        ]},
        ...
      ]
    },
    {
      "role": "case",
      "payload": [
        { "role": "when", "payload": [{ "role": "failure", "payload": "previous" }] },
        { "role": "error", "payload": "error" },
        ...
      ]
    }
  ]
}

// has/missing (non-failing dependency check)
{
  "role": "branch",
  "payload": [
    {
      "role": "case",
      "payload": [
        { "role": "when", "payload": [{ "role": "dependency-present", "payload": [
          { "role": "pipeline", "payload": "install" }
        ]}]},
        ...
      ]
    },
    {
      "role": "case",
      "payload": [
        { "role": "when", "payload": [{ "role": "dependency-missing", "payload": [
          { "role": "pipeline", "payload": "install" }
        ]}]},
        { "role": "missing-dependencies", "payload": "dependencies" },
        ...
      ]
    }
  ]
}
```

## Types

- **`has` / `missing`:** `dependencies` binds to a list of objects: `[{ "name": "dep_name" }, …]`. Starting with a single `name` property is forward-compatible; new fields can be added without a breaking shape change.

## Implementation

**Build tier:** Tier 11 — needs-then / block ops

| Layer | Responsibility |
|-------|----------------|
| `registry.rs` | Deserialize `__ops_registry` JSON |
| `ast.rs` | `Statement::NeedsThen`, `Statement::BlockOp` |
| `parser.rs` | `needs […] then {`. Registry check for `dsl_block` → `op { body }` |
| `lower.rs` | Needs-then → dedicated IR form; has-block → dedicated IR form |
| `main.rs` | Wire `--registry` flag |

**Validates:** bats tests for `parallel.all` / `parallel.any`, needs-then desugaring, registry-driven block detection.

## Design decisions

- **Needs-then desugaring:** dedicated IR form, not per-step injection. Per-step injection loses block structure, makes `catch` placement awkward, and redundantly checks deps on every step.
- **Needs-then + `catch`:** `catch` handles dependency failures (e.g. install itself failing). Body errors use `try` / `catch` inside the `then` block. Mirrors `has` / `missing`: needs-then-catch is fatal dependency handling; has-missing is optional when deps are absent.

## Caveats

- **Commas in `needs […] then {`:** commas are not stripped in bash. Rust standardizes: commas are optional, whitespace-equivalent separators.

## Open questions

None recorded for this feature.
