
## Build Tiers

Each tier is buildable and testable before the next. Tiers are ordered to avoid rework.

Feature details live in [features/](./features/). This index lists tier order and what ships in each.

The canonical IR contract for lowering, resolver, emitter, and drilling consumers lives in [ir_schema](./ir_schema.md), with focused semantic notes under [ir/](./ir/).

---

### Tier 0 — Scaffold & Tokens

Crate skeleton with error types, span tracking, and token definitions.

- `Cargo.toml`: clap, serde, serde_json, thiserror
- `error.rs` / `span.rs`: `Span { file, line, col }`, `CompileError { span, code, key, message }` formatted as `error[e###] key: message`
- `token.rs`: `Token { kind, span, text }` — kinds: Ident, LParen, RParen, LBrace, RBrace, LBracket, RBracket, Colon, Equals, Dot, Dollar, Comma, StringLit, TemplateLit, Newline, Comment, BlockComment, DocComment, Eof
- `main.rs`: `compile` subcommand stub

**Validates**: `cargo build`

### Tier 1 — Lexer

→ [strings-and-templates](./features/strings-and-templates.md), [doc-comments](./features/doc-comments.md)

Character-by-character scanner producing a token stream.

**Validates**: unit tests — tokenize inputs, verify token kinds and spans

### Tier 2 — Minimal Parser

→ [use-declarations](./features/use-declarations.md), [imperative-blocks](./features/imperative-blocks.md)

Parse comments, `@use` declarations, and imperative blocks with bare steps.

**Validates**: diff against bash `compile` output. Port compile.api.bats.

### Tier 3 — Step Modifiers

→ [steps](./features/steps.md)

Post-step modifiers (`needs`, `export`, `as`) with paren-balanced line continuation.

**Validates**: bats tests for each modifier, combined modifiers, multiline invocations

### Tier 4 — Invocation Arguments

→ [invocation-arguments](./features/invocation-arguments.md)

Positional, named, object literal, and spread args inside `(...)`.

**Validates**: bats tests for each arg form and combinations

### Tier 5 — Type System Foundation

→ [type-system](./features/type-system.md)

Define DslType enum and wire compile-time validation against op signatures. Core types: String, Int, Number, Bool, Path, Glob.

**Note**: Rust tests use hand-crafted registry JSON fixtures until Tier 22.

**Validates**: Rust tests for type validation — correct types pass, mismatches warn, unknown args error

### Tier 6 — Declarative Blocks

→ [declarative-blocks](./features/declarative-blocks.md)

`name = { ... }` blocks with nesting and various value forms.

**Validates**: bats tests for empty entries, nesting, needs in decl values, qualified keys

### Tier 7 — Env Block & Named Enums

→ [env-block](./features/env-block.md), [type-system](./features/type-system.md)

Typed env declarations with `@type` annotations and reusable named enums. Remaining types wired here.

**Validates**: Rust tests for typed env, list types, enum resolution, error cases

### Tier 8 — Scripts Block & Main Keyword

→ [scripts-block](./features/scripts-block.md), [imperative-blocks](./features/imperative-blocks.md)

`scripts = {}` for standalone executables. `main` keyword for entry point declaration.

**Validates**: bats tests for scripts + Rust tests for main keyword, multiple-main error, same-file reference error

---

### Tier 9 — if / try / with

→ [control-flow](./features/control-flow.md), [error-handling](./features/error-handling.md), [with-blocks](./features/with-blocks.md)

Parse `if`/`elif`/`else`, `try`/`catch`/`finally`, `with` blocks, and all three `throw` forms (simple, rethrow, structured `@error`).

**Validates**: bats tests for all if/elif/else/try/catch/with variants, throw forms, @error validation

### Tier 10 — match / each

→ [control-flow](./features/control-flow.md)

**Validates**: bats tests for match arms, wildcard default, each block, malformed syntax errors

### Tier 11 — needs-then / block ops

→ [dependency-constructs](./features/dependency-constructs.md), [imperative-blocks](./features/imperative-blocks.md)

Registry enters the parser here — block ops are identified by `--block` flag. All ops that wrap a body use uniform `op(args) { body }` syntax.

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

### Tier 12 — Infix Expressions

→ [infix-expressions](./features/infix-expressions.md)

Pratt parser for infix conditions in `if` blocks.

**Validates**: bats tests for ==, &&, ||, precedence, parentheses, nesting

---

### Tier 13 — Resolver: Dependency Graph

Build and analyze the dependency graph from compiled IR. The resolver consumes the canonical shape from [ir_schema](./ir_schema.md), especially [pipelines-and-steps](./ir/pipelines-and-steps.md) and [dependency-and-safety](./ir/dependency-and-safety.md).

- `resolve/mod.rs`: orchestrator — read IR + registry, produce resolved plan
- `resolve/graph.rs`: dep graph from `needs`, topo sort, cycle detection, group partitioning, terminal groups. Respect `entrypoint`. In-file group resolution: step names matching `pipelines` keys → group dispatch.

**Validates**: unit tests on ordering, cycles, terminals. Port resolve.api.bats: circular dep, missing need, duplicate entry.

### Tier 14 — Resolver: Dependencies & Env Surface

- `resolve/deps.rs`: inject implicit deps from registry `dependency`/`dependency_default`. Recursive parent chains. Var-needs: `{ "var": "NAME" }` → resolve to exporting entry or drop if env constant.
- `resolve/env.rs`: merge declaration env with op-level env from registry. Error on conflicting defaults/types. Conflicting env key names across ops are a compile error — the pipeline author must disambiguate in `env = {}`.

**Validates**: unit tests on env merging, dep injection, flag disambiguation. Port resolve.api.bats: ambiguous export, missing var export.

### Tier 15 — Resolver: Validation, Hooks & Warnings

- `resolve/validate.rs`: check op refs against registry. Type-check args, env. Skip condition marking.
- `resolve/hooks.rs`: scan registry for `--before`/`--after` ops. Inject hook steps ordered by registration order.

Warnings: unused imports, dead groups, plausible typos, unused env keys.

**Validates**: Rust tests for type mismatches, unused warnings, hook ordering

**Integration checkpoint**: wire `ops-engine compile` into `ops` script. Add `__dump_registry()`. Diff against bash compiler on real `.ops` files.

---

### Tier 16 — Emitter: POSIX Codegen & Root

- `emit/posix.rs`: proper shell quoting, name mangling, `@`-prefixed function references (`@out`, `@export`, `@defer`, `@throw`) in generated code
- `emit/flatten.rs`: compile-time type flattening — known-schema types become shell variable assignments (e.g. `__result_endpoint_host`, `__result_nodes_0_port`, `__result_nodes_count`). No JSON parsing at runtime for typed data.
- `emit/json.rs`: `__json.awk` program generation for opaque JSON access (`@json`, `@file-json`, untyped `as` captures). Emits to `.opcache` for shared use; inlined for standalone scripts. Shim functions: `__json_get "$blob" "path.to.field"`, `__json_set`.
- `emit/root.rs`: root .op.sh dispatches main group, generates typed flag parser from env surface
- `emit/manifest.rs`: `.exports.json`

Emitter tiers consume [posix-emission](./ir/posix-emission.md) and [sources-and-provenance](./ir/sources-and-provenance.md) so generated scripts can report diagnostics without depending on source files at runtime.

**Validates**: generate root .op.sh, diff against bash output. Verify flattened variable access matches jq-based access for all typed ops.

### Tier 17 — Emitter: Groups & CLI/Help

- `emit/group.rs`: group .op.sh with prereq dispatches, step dispatches, `--name`, arg formatting. All variables mangled with function-name prefix (`__{fn}__{var}`) — no `local`, fully POSIX. Exports → `__OPS_EXPORTS`. `as` → flattened shell variables for typed captures, `__json_get` for untyped/opaque captures.
- Every .op.sh gets a flag parser and `--help` with doc comments, outputs, and flag provenance

**Validates**: port resolve.api.bats for root/group structure, env flags, prereqs, step names, export capture

### Tier 18 — Emitter: Control Flow

- `emit/group.rs` extensions: if/elif/else, try/catch/finally, with/undo, match, each, block ops (nested body emission)
- Try/catch: emit error metadata capture for `@throw`, make `@error` fields available in catch blocks via flattened shell variables

**Validates**: port resolve.api.bats for conditionals, try/catch, with, each, match, block ops, infix expressions

### Tier 19 — Emitter: Parallel, Skip & Defer

- `as` capture wiring: typed captures use flattened shell variables; opaque/untyped captures use `__json_get` from `.opcache/__json.awk`
- Parallel emission: FIFO setup/teardown, `@export` → FIFO writes in subshell branches, parent reads after `wait`
- Skip condition emission: `__ops_check_skip` calls, `__SKIPPED__` propagation through exports
- Defer drain: `@defer` stack drained at group completion or via `trap ... EXIT`

**Validates**: Rust tests for `as` capture wiring, parallel FIFO round-trips, skip cascading, defer drain placement

### Tier 20 — Emitter: Shim & Drill Files

- `emit/shim.rs`: runtime shim template — embeds definitions for `@out`, `@export`, `@defer`, `@throw`, logging functions (`@print`/`@debug`/`@info`/`@warn`/`@error`/`@fatal`), `__ops_check_skip`, parallel helpers, FIFO setup/teardown, `__json_get`/`__json_set` wrappers (sourcing `__json.awk` from `.opcache`)
- `emit/steps.rs`: per-step drill files for named steps

**Validates**: shim contains all required function definitions, drill file prefix chains are correct

### Tier 21 — Emitter: Scripts & Build

- `emit/scripts.rs`: standalone script assembly (shebang + shim + inlined ops + groups + root), chmod +x
- `ops-engine build`: discover `**/*.ops`, compile, topo-sort by pipeline imports, resolve in order. `manifest.json` ignores. `--if-changed` freshness.

**Validates**: standalone script round-trips, build ordering, self-bake.

**Integration checkpoint**: wire `ops-engine resolve` into `ops` script. Shadow-diff mode: run both resolvers, diff output, log discrepancies.

---

### Tier 22 — Engine Cutover

- Update `@operation` in bash runtime (serializes `@in`, `@out`, `--before`, `--after`, `--spread`, `--block`, `--requires`). Alias `register_op` → `@operation` for backward compat.
- Add `@out`, `@export`, `@defer`, `@throw` to bash runtime. Alias `declare_output` → `@out`, `run_defer` → `@defer`.
- Add `@error` type to bash runtime with validation (code range, key constraints, schema).
- Add logging functions: `@print`, `@debug`, `@info`, `@warn`, `@error`, `@fatal`
- Update `run_compile`, `run_resolve`, `auto_resolve`, `run_build` to call `ops-engine`
- Fix `std.defer` drain via `@defer` stack
- Binary lookup: `$SHELLOPS_DIR/engine/target/release/ops-engine` or `$PATH`
- Remove shadow-diff, cut over fully

**Validates**: full bats suite, `./ops dev bake`, all `.ops` files across the org

### Tier 23 — Cross-Pipeline Drilling

> Tiers 0–22 are a complete engine. This is a v2 cut point.

`use` imports make imported groups drillable under the parent namespace. `./ops dev validation lint` resolves to validation's `lint` group.

- Resolver builds merged drill tree with imported groups under namespace prefix
- `walk_op_path` follows the tree
- Imported groups inherit applicable env flags from parent
- Each imported pipeline resolves independently (no IR merging)

**Validates**: cross-pipeline drilling, help propagation across boundaries
