
## Architecture

Rust CLI at `engine/` replaces `lib/compile.api.sh` and `lib/resolve.api.sh`. The bash `ops` script shells out to it. The runtime (dispatch, load_ops, `.op.sh` files) stays in bash.

The engine does not parse `.op.sh` files. Bash sources them via `load_ops`, which discovers ops and types through the structural contract pattern (see [bash_api.md](./bash_api.md)), builds the registry JSON, and the engine reads that file.

### Two layers, two targets

The **authoring layer** (`.op.sh` files, runtime, `load_ops`) targets **bash** (minimum 4.3+). It uses bash-specific features extensively: `@`-prefixed functions with `.`, `?`, `[]` in names, inner function definitions, `compgen -A function` for contract discovery, `command_not_found_handle` for dynamic type dispatch, associative arrays, namerefs.

The **emitted scripts** (compiled output from the Rust engine) target **POSIX sh / dash**. Emitted scripts are fully resolved, standalone artifacts — no dependency on the shellops runtime, `@`-prefixed API, or bash. They are half a step from binary: inlined op logic, compiled control flow, POSIX-compatible constructs throughout. This gives maximum portability (Alpine, minimal containers, systems without bash) and faster execution (dash startup is 4-5x faster than bash).

**JSON handling in emitted scripts** uses two strategies, both POSIX-compliant:

1. **Compile-time flattening** — types with known schemas (the common case) are flattened to shell variables by the emitter. Inter-step data passes through indexed variable names (e.g. `__result_endpoint_host`, `__result_nodes_0_port`, `__result_nodes_count`) rather than JSON blobs. No JSON parsing at runtime.
2. **`__json.awk`** — a POSIX awk program emitted to `.opcache` that handles drill-and-extract for opaque JSON (`@json`, `@file-json` types) and dotted path access into untyped `as` captures. Accessed via a shim function: `__json_get "$blob" "config.host"`. Shared across all emitted scripts via `.opcache`; no external dependencies.

**POSIX compliance** — emitted scripts avoid all bash-isms:

- **No `local`** — variables use function-name-prefixed mangling (`__{fn}__{var}`), making them globally unique and greppable without the non-POSIX `local` keyword.
- **No `[[ ]]`** — conditionals use `[ ... ]` (`test`) and `case` statements.
- **No `(( ))`** — arithmetic uses `$(( ))` (POSIX arithmetic expansion) and `[ "$x" -gt 0 ]` for comparisons.
- **No associative arrays** — exports and captures use mangled flat variables (known at compile time from the dependency graph).
- **No bash builtins** — no `compgen`, `namerefs`, `declare`, `command_not_found_handle`, or `@`-prefixed function names.

The `.op.sh` authoring API is the source language. The emitted POSIX script is the target language. All bash-isms are compiled away.

The lowering → resolver boundary is the canonical [IR schema](./ir_schema.md). Its source-map and POSIX emission requirements are defined in [sources-and-provenance](./ir/sources-and-provenance.md) and [posix-emission](./ir/posix-emission.md).

```mermaid
flowchart LR
  subgraph bash_runtime [Bash Runtime]
    OpsScript["ops script"]
    LoadOps["load_ops (contract discovery)"]
    Registry["registry JSON"]
    Dispatch["dispatch / runtime.api.sh"]
    OpFiles["operations/**/*.op.sh"]
  end

  subgraph rust_engine [Rust Engine]
    Lexer["Lexer"]
    Parser["Parser"]
    Lowering["Lowering"]
    Resolver["Resolver"]
    Emitter["Emitter"]
  end

  OpFiles -->|"@operation.* / @type.*"| LoadOps
  LoadOps -->|"call outer fns, diff, remap"| Registry
  OpsScript -->|"shells out"| rust_engine
  Registry -->|"--registry file"| rust_engine
  Lexer -->|"Tokens"| Parser
  Parser -->|"AST"| Lowering
  Lowering -->|"IR JSON"| Resolver
  Resolver -->|"dep graph"| Emitter
  Emitter -->|"POSIX sh scripts"| bash_runtime
```

### load_ops

`load_ops` is a framework that discovers ops and types from `.op.sh` files via structural contracts:

1. Source the `.op.sh` file — outer functions (`@operation.*`, `@type.*`) are defined but not called
2. For each outer function, snapshot defined functions (`compgen -A function`)
3. Call the outer function — metadata calls (`@param`, `@symbol`, etc.) register at load time; inner functions (`@do`, `@undo`, `@is-done`, `@is`) land in global scope
4. Diff the function list: `@`-prefixed = contract functions, `_`-prefixed = private helpers
5. Remap inner functions to namespaced dispatch names (prevents collisions across ops sharing inner names like `@do`)
6. Unset raw inner names
7. Build registry JSON entry from metadata + contract mapping

Multiple ops in the same file (or across files) can define inner functions with the same name (e.g. `@do`). `load_ops` remaps between each outer function call, so there are no collisions.

### CLI

- `ops-engine compile <file.ops> --registry <registry.json>` — emit IR JSON to stdout
- `ops-engine resolve <context> <ir-or-ops> --output <dir> --registry <registry.json>` — write `.op.sh` files
- `ops-engine build <root> --output <dir> --registry <registry.json>` — discover + resolve all `.ops` files

Registry is always required — block ops and infix operators are registry-driven.

### Crate Layout

```
engine/
  Cargo.toml
  ast/                # AST node types
  cli/                # opc CLI surface
  errors/             # Shared @error object, spans, codes, catalog, and renderer bridge
  parser/             # .ops lexer and parser
  resolver/           # Dependency graph and validation
  emitter/            # POSIX sh generation
  posix/              # POSIX sh codegen helpers
```

### Error Reporting & Recovery

Every error uses the `@error` type with hex codes (`e###`), kebab-case keys, file:line:col location, and source context snippets.

```text
error unresolved-import (E001):
at ci.ops:3:6:

   |
 2 | @use std.log as log
 3 | @use ./validaton as validate  <--- The issue is `./validaton`
 4 |
   |

Cannot find module 'validaton'.
HINT: Did you mean './validation'?
```

The parser depends on the shared `errors` crate for `OpXError`, `ErrorCode`, `ErrorKey`, `ErrorOrigin`, `Position`, and source spans. `opx_error.lib.sh` owns human layout and color behavior; Rust shells out to it when it needs a display string, and emitted POSIX scripts can inline the same renderer and catalog.

**Recovery strategy**: panic-mode with block-level sync. On parse error, skip tokens until `}` at the current nesting depth or a top-level token (`@`, `env`, `scripts`, `@enum`, `@use`) at depth 0. This yields multiple errors per file without cascading false positives.

**Cross-file compilation**: each `@use`'d file compiles independently in its own parse pass. Errors in one file don't cascade into another file's parse. If any file has parse errors, all errors are reported and the resolver does not run. The consuming file gets a summary: `"2 imported files had compile errors (validation.ops, deploy.ops)"`.

**Provenance**: requirement provenance traces missing env vars to the consuming op and line. Compile-time unresolved requirement warnings catch wiring issues before runtime.

**Warnings**: unused imports, dead groups, plausible typos (fuzzy match), unused env keys, deprecated syntax.

See [strategies/error-catalog](./strategies/error-catalog.md) for the error identification and catalog strategy.
