# IR Schema

This is the canonical planning contract for Shellops IR. It defines the JSON object produced by parser lowering and consumed by resolver, emitter, drilling, and machine-output tooling as the Rust engine grows through the build tiers.

For OPS-131, the artifact is intentionally human-readable documentation in this file plus focused subdocs under [ir/](./ir/). A machine-readable JSON Schema should be added later under a `schema/` directory only once implementation validates real IR against it.

## Status

- **Schema marker:** `$schema: "shellops-ir"` with `ir_version: "1.0"`.
- **Compiler and resolver:** ship in one binary and evolve in lockstep.
- **External stability:** not promised until third-party IR consumers exist.
- **Current CLI:** `opc compile` remains parked until this contract is implemented.

See [versioning-and-compatibility](./ir/versioning-and-compatibility.md) and [strategies/versioning](./strategies/versioning.md) for the compatibility posture.

## Payload Shape

Every IR node is `{ role, payload }` with an optional `name` for identified nodes. Payloads follow two rules:

1. **Leaf payload**: a scalar value (string, number, boolean) or an array of scalar values. Generators emit these directly.
2. **Composite payload**: an array of `{ role, payload }` items. Generators iterate the array and dispatch recursively by role.

This makes generation predictable: every generator receives a role and its payload, queries for the sub-roles it understands, and dispatches to sub-generators. Higher-order generator patterns compose naturally because every generator shares the same input contract.

### Dispatch Pattern

```
generate(node) → lookup(node.role)(node.payload)
```

Leaf generators emit the scalar directly. Composite generators iterate their payload, filtering by role:

```
find_one(payload, "target")   → the single item with that role
find_all(payload, "call")     → all body steps
```

### Why Lists

- **Extensibility**: adding a new sub-component is always an additive array append, never a key-name invention or a breaking refactor.
- **No unique-key constraint**: arrays naturally accommodate multiplicity (e.g., multiple `type-param` roles in a generic declaration).
- **Ordering when needed**: arrays preserve declaration order for body steps and clause sequences.
- **Uniform validation**: contract validation reduces to `expect(role, count)` combinators over a predictable shape.
- **Format-agnostic**: the logical tree serializes identically to JSON, YAML, MessagePack, or CBOR.

Query performance is a consumer concern. Consumers may build role indexes over a node's payload on first access — the same optimization pattern used in ECS archetypes — without changing the canonical shape.

### Structural Fields

Only two fields live outside `payload`:

- **`role`** (required): discriminator identifying the node's function.
- **`name`** (optional): identity for named nodes (pipelines, declarations, env entries, sources, imports, scripts, sourcemap nodes). Used for lookup, not generation content.

Everything else — metadata, decorators, bindings, source links, body steps — goes inside `payload`.

## Target Shape

Every lowered `.ops` file produces one IR unit. Imported files compile to their own IR units; the resolver connects them through `imports` and package/source provenance instead of merging all files into one object.

```json
{
  "$schema": "shellops-ir",
  "ir_version": "1.0",
  "role": "file",
  "payload": [
    {
      "role": "unit",
      "payload": [
        { "role": "id", "payload": "file:ci.ops" },
        { "role": "path", "payload": "ci.ops" }
      ]
    },
    {
      "role": "source",
      "name": "file:ci.ops",
      "payload": [
        { "role": "origin", "payload": "user" },
        { "role": "path", "payload": "ci.ops" },
        { "role": "content", "payload": "main @release {\n  test()\n}\n" }
      ]
    },
    {
      "role": "source",
      "name": "pkg:shellops-std@1.0.0/log.ops",
      "payload": [
        { "role": "origin", "payload": "package" },
        { "role": "package", "payload": "shellops-std" },
        { "role": "version", "payload": "1.0.0" },
        { "role": "path", "payload": "log.ops" },
        { "role": "digest", "payload": "sha256:..." }
      ]
    },
    { "role": "import", "name": "log", "payload": "pkg:shellops-std@1.0.0/log.ops" },
    { "role": "entrypoint", "payload": "release" },
    {
      "role": "env",
      "payload": [
        {
          "role": "entry",
          "name": "DEPLOY_TARGET",
          "payload": [
            { "role": "type", "payload": "string" },
            { "role": "required", "payload": true },
            { "role": "source", "payload": "#/sourcemap/env.DEPLOY_TARGET" }
          ]
        },
        {
          "role": "entry",
          "name": "TARGETS",
          "payload": [
            {
              "role": "type",
              "payload": [
                { "role": "name", "payload": "string" },
                { "role": "list", "payload": true },
                { "role": "separator", "payload": "space" }
              ]
            },
            { "role": "default", "payload": ["linux", "darwin"] }
          ]
        },
        {
          "role": "entry",
          "name": "DEPLOY_TOKEN",
          "payload": [
            { "role": "type", "payload": "secret" },
            { "role": "required", "payload": true },
            { "role": "source", "payload": "#/sourcemap/env.DEPLOY_TOKEN" }
          ]
        }
      ]
    },
    { "role": "script", "name": "bin/deploy", "payload": "release" },
    {
      "role": "declaration",
      "name": "notice",
      "payload": [
        { "role": "type", "payload": "string" },
        { "role": "value", "payload": "release started" }
      ]
    },
    {
      "role": "pipeline",
      "name": "release",
      "payload": [
        { "role": "entrypoint" },
        { "role": "safety", "payload": "remote" },
        {
          "role": "call",
          "payload": [
            { "role": "target", "payload": "test" },
            { "role": "source", "payload": "#/sourcemap/pipelines.release.0" }
          ]
        },
        {
          "role": "call",
          "payload": [
            { "role": "target", "payload": "deploy" },
            { "role": "args", "payload": [
              { "role": "named", "name": "target", "payload": [{ "role": "var", "payload": "DEPLOY_TARGET" }] }
            ]},
            { "role": "need", "payload": [
              { "role": "var", "payload": "DEPLOY_TOKEN" }
            ]},
            { "role": "export", "payload": "VERSION" },
            { "role": "source", "payload": "#/sourcemap/pipelines.release.1" }
          ]
        }
      ]
    },
    {
      "role": "sourcemap",
      "payload": [
        {
          "role": "node",
          "name": "pipelines.release",
          "payload": [
            { "role": "source", "payload": "file:ci.ops" },
            {
              "role": "span",
              "payload": [
                { "role": "start", "payload": [1, 1, 0] },
                { "role": "end", "payload": [4, 2, 44] }
              ]
            }
          ]
        }
      ]
    }
  ]
}
```

In [OPX serialization](./ir/serialization-format.md):

```
R00t_ 0.file
Un1t_ 1.unit
xK_mQ 2.id:string."file:ci.ops"
Lp3qR 2.path:string."ci.ops"
Sr01_ 1.source
nS01_ 2.name:string."file:ci.ops"
oS01_ 2.origin:string."user"
pS01_ 2.path:string."ci.ops"
cS01_ 2.content:string."main @release {\n  test()\n}\n"
Sr02_ 1.source
nS02_ 2.name:string."pkg:shellops-std@1.0.0/log.ops"
oS02_ 2.origin:string."package"
pkS2_ 2.package:string."shellops-std"
vrS2_ 2.version:string."1.0.0"
ptS2_ 2.path:string."log.ops"
dgS2_ 2.digest:string."sha256:..."
Im01_ 1.import
nI01_ 2.name:string."log"
vI01_ 2.value:string."pkg:shellops-std@1.0.0/log.ops"
En01_ 1.entrypoint:string."release"
Ev01_ 1.env
eE01_ 2.entry
nE01_ 3.name:string."DEPLOY_TARGET"
tE01_ 3.type:string."string"
rE01_ 3.required:bool.true
sE01_ 3.source:string."#/sourcemap/env.DEPLOY_TARGET"
eE02_ 2.entry
nE02_ 3.name:string."TARGETS"
tE02_ 3.type
nT02_ 4.name:string."string"
lT02_ 4.list:bool.true
sT02_ 4.separator:string."space"
dE02_ 3.default
d1E02 4.item:string."linux"
d2E02 4.item:string."darwin"
eE03_ 2.entry
nE03_ 3.name:string."DEPLOY_TOKEN"
tE03_ 3.type:string."secret"
rE03_ 3.required:bool.true
sE03_ 3.source:string."#/sourcemap/env.DEPLOY_TOKEN"
Sc01_ 1.script
nSc1_ 2.name:string."bin/deploy"
vSc1_ 2.value:string."release"
Dc01_ 1.declaration
nDc1_ 2.name:string."notice"
tDc1_ 2.type:string."string"
vDc1_ 2.value:string."release started"
Pl01_ 1.pipeline
nPl1_ 2.name:string."release"
ePl1_ 2.entrypoint
sPl1_ 2.safety:string."remote"
cP01_ 2.call
tC01_ 3.target:string."test"
sC01_ 3.source:string."#/sourcemap/pipelines.release.0"
cP02_ 2.call
tC02_ 3.target:string."deploy"
aC02_ 3.args
nA01_ 4.named
nnA1_ 5.name:string."target"
vnA1_ 5.var:string."DEPLOY_TARGET"
dC02_ 3.need
vD01_ 4.var:string."DEPLOY_TOKEN"
xC02_ 3.export:string."VERSION"
sC02_ 3.source:string."#/sourcemap/pipelines.release.1"
Sm01_ 1.sourcemap
nSm1_ 2.node
nnS1_ 3.name:string."pipelines.release"
snS1_ 3.source:string."file:ci.ops"
spS1_ 3.span
stS1_ 4.start
s1S1_ 5.item:int.1
s2S1_ 5.item:int.1
s3S1_ 5.item:int.0
enS1_ 4.end
e1S1_ 5.item:int.4
e2S1_ 5.item:int.2
e3S1_ 5.item:int.44
```

The file's `payload` is a flat array of all IR items — unit metadata, sources, imports, env, declarations, scripts, pipelines, and sourcemap. No intermediate `objects` wrapper. Consumers query by `role` to find what they need.

## Field Index

| Field | Required | Producer | Consumers | Purpose |
| --- | --- | --- | --- | --- |
| `$schema` | yes | lowering | all IR consumers | Identifies the document as Shellops IR. |
| `ir_version` | yes | lowering | all IR consumers | Two-component IR contract version. |
| `role` | yes | lowering | all IR consumers | Universal discriminator; top-level IR uses `role: "file"`. |
| `payload` | yes | lowering | all IR consumers | Node data: scalar for leaves, array of role-tagged items for composites. |
| `name` | when applicable | lowering | all IR consumers | Identity for named nodes (pipelines, declarations, env entries, sources, etc.). |

## Reusable Node Pieces

The IR is shaped around a small compositional vocabulary rather than bespoke fields for every language feature:

- **`role`** identifies what part a node plays. Every node has one.
- **`payload`** carries the node's data. Leaf roles use a scalar. Composite roles use an array of `{ role, payload }` items. The generator dispatches on role and iterates payload — two cases, uniformly.
- **`call`** identifies an operation invocation — same-file pipeline, imported pipeline, or registry-provided target plus structured args.
- **`need` / `want`** express hard and soft dependencies as payload arrays of `pipeline`, `var`, or `step` refs.
- **`export` / `capture`** describe values produced by a node. The payload is the binding name.
- **`safety`** attaches safety markers (`remote`, `dangerous`, `deprecated`) as payload strings.
- **`source`** links any node back to a sourcemap entry. The payload is the sourcemap path.
- **`when`** conditions on branch clauses. Scalar payload for simple conditions (`"always"`), composite for expressions and calls.
- **`args`** groups invocation arguments: `positional` and `named` sub-roles.

This is intentionally not a full ECS runtime. The goal is a small IR vocabulary where each compiler or emitter component declares the roles it understands and contributes generation/validation for that subset. Query optimization (role indexing, caching) is a consumer concern, not a schema concern.

## Step Representation

Step bodies allow two forms:

- **String shorthand:** `"qualified.op.name"` is allowed only for a bare invocation with no args, modifiers, capture name, block body, condition behavior, or explicit metadata. Consumers must normalize it to a `call` node before semantic analysis.
- **Call node:** required for anything with args, dependencies, bindings, source links, block bodies, or control-flow semantics.

Canonical call node:

```json
{
  "role": "call",
  "payload": [
    { "role": "target", "payload": "qualified.op.name" },
    { "role": "args", "payload": [
      { "role": "positional", "payload": "arg1" },
      { "role": "named", "name": "threshold", "payload": 80 },
      { "role": "named", "name": "overrides", "payload": [
        { "role": "entry", "name": "foo.sh", "payload": 50 }
      ]}
    ]},
    { "role": "need", "payload": [
      { "role": "pipeline", "payload": "dep1" },
      { "role": "var", "payload": "VERSION" }
    ]},
    { "role": "export", "payload": "VERSION" },
    { "role": "capture", "payload": "lint_result" },
    { "role": "source", "payload": "#/sourcemap/pipelines.release.0" }
  ]
}
```

In OPX:

```
cL01_ 0.call
tL01_ 1.target:string."qualified.op.name"
aL01_ 1.args
pA01_ 2.positional:string."arg1"
nA01_ 2.named
nnA1_ 3.name:string."threshold"
vnA1_ 3.value:int.80
nA02_ 2.named
nnA2_ 3.name:string."overrides"
enA2_ 3.entry
neA2_ 4.name:string."foo.sh"
veA2_ 4.value:int.50
dL01_ 1.need
pD01_ 2.pipeline:string."dep1"
vD01_ 2.var:string."VERSION"
xL01_ 1.export:string."VERSION"
cpL1_ 1.capture:string."lint_result"
sL01_ 1.source:string."#/sourcemap/pipelines.release.0"
```

See [pipelines-and-steps](./ir/pipelines-and-steps.md) for group, step, block-op, and naming details.

## Source Maps

IR source maps must be self-contained enough for diagnostics emitted by generated POSIX scripts in CI/runtime. A script should not require the original `.ops`, `.op.sh`, stdlib, or package files to still exist on disk to explain an error.

User source entries should include embedded `content` when allowed. Package, stdlib, or generated sources may omit content only when they include enough provenance to identify the exact source artifact: package name, version, path, and digest.

See [sources-and-provenance](./ir/sources-and-provenance.md) and [posix-emission](./ir/posix-emission.md).

## Secret Invariant

Raw literal values for `@secret` are compile errors. Lowering must reject `@secret TOKEN: "value"` instead of lowering and redacting it later.

Secret values must come from runtime env, CI secrets, secret stores, or provider references. IR may carry secret metadata, type information, provider references, and source spans, but never raw secret values.

See [values-and-types](./ir/values-and-types.md) and [dependency-and-safety](./ir/dependency-and-safety.md).

## Focused IR Docs

- [Sources and provenance](./ir/sources-and-provenance.md)
- [Values and types](./ir/values-and-types.md)
- [Pipelines and steps](./ir/pipelines-and-steps.md)
- [Control flow](./ir/control-flow.md)
- [Dependency and safety](./ir/dependency-and-safety.md)
- [POSIX emission](./ir/posix-emission.md)
- [Versioning and compatibility](./ir/versioning-and-compatibility.md)
- [Tier consumers](./ir/tier-consumers.md)
- [Serialization format](./ir/serialization-format.md)

## Consumer Contract

- **Parser lowering** produces this object from AST plus source spans.
- **Resolver** consumes imports, env, declarations, pipelines, and sourcemap to validate graph shape, env flow, type compatibility, hooks, warnings, and safety gates.
- **Emitter** consumes resolved IR plus source maps to generate POSIX scripts, root/group dispatch, standalone scripts, drill files, CLI help, and runtime diagnostic metadata.
- **Drilling/machine output** consumes stable names, env provenance, graph metadata, and source spans for help text and structured diagnostics.

The full tier map is in [tier-consumers](./ir/tier-consumers.md).
