> ## Documentation Index
> Fetch the complete documentation index at: https://bintzgavin-apastra-14.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Prompt resolver

> How apastra resolves prompt IDs to actual prompt content through a four-level chain — local override, workspace, git ref, and packaged artifact.

The resolver is the component that turns a prompt ID (like `summarize-v1`) into actual prompt content your harness can use. It works through four levels in strict priority order — the first level to find a match wins.

```
local override → workspace → git ref → packaged artifact
```

This ordering means you can always override any pinned version locally for development without changing your consumption manifest or your CI configuration.

***

## The `ResolverChain` class

The resolution chain is implemented in `promptops/resolver/chain.py`:

```python theme={null}
from promptops.resolver.local import LocalResolver
from promptops.resolver.workspace import WorkspaceResolver
from promptops.resolver.git_ref import GitRefResolver
from promptops.resolver.packaged import PackagedResolver
from promptops.manifests.consumption import validate_manifest

class ResolverChain:
    def resolve(self, prompt_id, manifest):
        if hasattr(manifest, 'data') and manifest.data:
            validate_manifest(manifest.data)

        rules = manifest.get_rules(prompt_id) if hasattr(manifest, 'get_rules') else {}
        target_id = rules.get('id', prompt_id)

        if rules and 'override' in rules:
            return LocalResolver().resolve(target_id, rules['override'])

        workspace_result = WorkspaceResolver().resolve(target_id)
        if workspace_result is not None:
            return workspace_result

        if rules and 'pin' in rules:
            pin = rules['pin']
            if pin.startswith('sha256:') or pin.startswith('https://') or pin.startswith('oci://') or pin.startswith('npm:') or pin.startswith('pypi:') or pin.startswith('github-release:'):
                return PackagedResolver().resolve(target_id, pin)
            else:
                return GitRefResolver().resolve(target_id, pin)

        raise NotImplementedError("Resolution fallback not yet implemented")
```

The chain reads resolution rules from the consumption manifest. If the manifest has an `override` for a prompt ID, it goes straight to `LocalResolver`. Otherwise it tries `WorkspaceResolver` first (no manifest entry needed), then falls back to `GitRefResolver` or `PackagedResolver` based on the `pin` format.

***

## Level 1 — Local override

`LocalResolver` reads a prompt from a local file path. This is the highest-priority level and is always checked first when an `override` is present in the consumption manifest.

**When to use:** local development against a separate prompt repo without publishing.

**Configure in `consumption.yaml`:**

```yaml theme={null}
version: "1.0"
prompts:
  summarize-v1:
    id: summarize-v1
    override: "../prompt-repo/promptops/prompts/summarize-v1.yaml"
```

The resolver supports `.yaml`, `.yml`, and `.json` files. It caches results by `mtime` to avoid re-reading unchanged files.

If the override path does not exist, `LocalResolver` raises `FileNotFoundError`. This prevents silent fallthrough to a stale pinned version when the override file is missing.

<Tip>
  Use local overrides during rapid iteration. When you're done, remove the `override` key and rely on the git ref pin for reproducibility.
</Tip>

***

## Level 2 — Workspace

`WorkspaceResolver` finds prompts in the local `promptops/` directory — no manifest entry needed. This is how same-repo prompts work without any publishing step.

**Paths tried (in order):**

```text theme={null}
promptops/prompts/<prompt-id>.yaml
promptops/prompts/<prompt-id>.json
promptops/prompts/<prompt-id>/prompt.yaml
promptops/prompts/<prompt-id>/prompt.json
promptops/evals/<prompt-id>.yaml
promptops/evals/<prompt-id>.json
```

**Minimal mode:** if your project has three or fewer prompt files, `WorkspaceResolver` also checks the shorter `prompts/` and `evals/` paths (without the `promptops/` prefix). This is auto-detected by counting files matching `promptops/prompts/*.yaml` and similar globs.

**Quick eval resolution:** when the resolver finds a file in `evals/`, it checks for a `prompt` field. If present, it returns a synthetic prompt spec:

```python theme={null}
{"id": prompt_id, "template": data["prompt"], "variables": {}}
```

If `WorkspaceResolver.resolve()` returns `None`, the chain continues to level 3 or 4.

***

## Level 3 — Git ref

`GitRefResolver` retrieves prompt content from a specific git commit, tag, or semver range. This is the default pinning mechanism for separate prompt repos — zero publishing overhead.

**Paths tried inside the git tree (in order):**

```text theme={null}
promptops/prompts/<prompt-id>.yaml
promptops/prompts/<prompt-id>.json
promptops/prompts/<prompt-id>/prompt.yaml
promptops/prompts/<prompt-id>/prompt.json
promptops/evals/<prompt-id>.yaml
promptops/evals/<prompt-id>.json
```

### Pin formats for git ref

| Pin format           | Example                                         | Description                                     |
| -------------------- | ----------------------------------------------- | ----------------------------------------------- |
| Commit SHA           | `abc123def456...`                               | Pins to an exact commit                         |
| Git tag              | `v1.2.3`                                        | Pins to a named tag                             |
| Semver range (local) | `semver:^1.2.0`                                 | Resolves to the highest matching local tag      |
| Remote git URL       | `git+https://github.com/org/repo#v1.2.3`        | Fetches from a remote repo at a specific ref    |
| Remote semver        | `git+https://github.com/org/repo#semver:^1.0.0` | Resolves the highest matching tag on the remote |

**Configure in `consumption.yaml`:**

```yaml theme={null}
version: "1.0"
prompts:
  summarize-v1:
    id: summarize-v1
    pin: "v1.2.3"
```

For remote repos:

```yaml theme={null}
version: "1.0"
prompts:
  summarize-v1:
    id: summarize-v1
    pin: "git+https://github.com/your-org/prompt-repo#abc123"
```

**How semver resolution works:** `GitRefResolver` runs `git tag --list` (or `git ls-remote --tags` for remote repos) and finds all tags that satisfy the semver range using its own `match_semver()` implementation. It selects the highest matching stable version. Prerelease versions (e.g., `v1.2.3-rc1`) are excluded unless the range explicitly selects them.

**Fallback for `git archive`:** if the remote server disables `git archive`, the resolver falls back to a full `git clone` + `git checkout` in a temporary directory and reads the file from there.

All results are cached in memory by `(prompt_id, pin)` key.

***

## Level 4 — Packaged artifact

`PackagedResolver` retrieves prompts from published artifacts — GitHub Releases, OCI registries, npm packages, or PyPI wheels. This level is triggered when the `pin` starts with a packaged-artifact prefix.

### Pin formats for packaged artifacts

| Pin format     | Example                               | Description                                                    |
| -------------- | ------------------------------------- | -------------------------------------------------------------- |
| SHA-256 digest | `sha256:<64 hex chars>`               | Content-addressed artifact (most reproducible)                 |
| HTTPS URL      | `https://example.com/promptops.json`  | Direct URL to a JSON artifact                                  |
| OCI artifact   | `oci://registry.example.com/repo:tag` | OCI image manifest (pulls first layer)                         |
| npm package    | `npm:@org/package@1.2.3`              | npm registry package (reads `package.json:promptops`)          |
| PyPI wheel     | `pypi:package==1.2.3`                 | PyPI wheel (reads first `.json` file with `promptops` in name) |
| GitHub Release | `github-release:owner/repo@v1.2.3`    | GitHub Release asset (first `.json` asset)                     |

**Configure in `consumption.yaml`:**

```yaml theme={null}
version: "1.0"
prompts:
  summarize-v1:
    id: summarize-v1
    pin: "github-release:your-org/prompt-repo@v1.2.3"
```

**Resolution flow for packaged artifacts:**

1. If the pin is a `sha256:` digest, `PackagedResolver` fetches the artifact, validates it against `prompt-package.schema.json`, and finds the prompt spec with the matching `id` inside the package's `specs` array.
2. For all other URL/registry formats, the resolver fetches the artifact, validates it against `provider-artifact.schema.json`, verifies the optional signature, then reads the `package_digest` field and calls `resolve()` recursively with that digest (treating it as a level-4 sha256 lookup).

**Caching:** fetched artifacts are cached to `~/.promptops/cache/` using a URL-safe filename derived from the ref. If a network fetch fails but a cached copy exists, the resolver uses the cache. If no cache is available, it raises `RuntimeError`.

***

## How the consumption manifest drives resolution

The consumption manifest (`promptops/manifests/consumption.yaml`) is the single config file that controls how the resolver behaves for your app.

```yaml theme={null}
version: "1.0"
prompts:
  summarize-v1:
    id: summarize-v1
    pin: "abc123"           # commit SHA, tag, semver, or packaged ref
  classify-v2:
    id: classify-v2
    override: "./local-prompts/classify-v2.yaml"  # local dev override
  translate-v1:
    id: translate-v1
    pin: "github-release:org/prompts@v2.1.0"
    model: "claude-opus-4"  # override default model for this prompt
defaults:
  model: "claude-sonnet-4"
  provider: "anthropic"
```

The `ResolverChain.resolve(prompt_id, manifest)` call:

1. Validates the manifest against `consumption-manifest.schema.json`.
2. Looks up the resolution rules for `prompt_id` (`override`, `pin`, `id` mapping).
3. Applies the four-level chain based on those rules.

***

## Pin format decision guide

<Tabs>
  <Tab title="Development">
    Use a local override when you are iterating on a prompt in a checked-out repo and don't want to publish or commit.

    ```yaml theme={null}
    pin: ~
    override: "../prompt-repo/promptops/prompts/my-prompt.yaml"
    ```
  </Tab>

  <Tab title="Same-repo">
    No pin needed. `WorkspaceResolver` finds prompts in `promptops/prompts/` automatically.

    ```yaml theme={null}
    prompts:
      summarize-v1:
        id: summarize-v1
        # no pin required — resolved from workspace
    ```
  </Tab>

  <Tab title="Pinned to commit">
    Use a full commit SHA for exact reproducibility. Immune to tag moves.

    ```yaml theme={null}
    pin: "abc123def456abc123def456abc123def456abc123"
    ```
  </Tab>

  <Tab title="Pinned to tag">
    Use a tag for human-readable pinning. The tag must exist in the repo.

    ```yaml theme={null}
    pin: "v1.2.3"
    ```
  </Tab>

  <Tab title="Semver range">
    Use `semver:` prefix for floating-within-range resolution. Resolves to the highest matching stable tag.

    ```yaml theme={null}
    pin: "semver:^1.2.0"
    ```
  </Tab>

  <Tab title="Governed release">
    Use `github-release:` for releases with supply-chain guarantees (SLSA provenance, immutable assets).

    ```yaml theme={null}
    pin: "github-release:your-org/prompt-repo@v1.2.3"
    ```
  </Tab>
</Tabs>
