# Runtime and Indexing

Pituitary is deterministic by default. Runtime-backed retrieval and analysis are optional extensions on top of the core CLI workflow.

## AI Runtime Configuration

By default, Pituitary uses a deterministic `fixture` embedder: no API keys, no network, fast and reproducible. This is the right mode for CI and for evaluating Pituitary's workflow on a small repo.

For real retrieval quality on a larger corpus, configure a local embedding model:

```toml
[runtime.embedder]
provider = "openai_compatible"
model = "nomic-embed-text-v1.5"
endpoint = "http://127.0.0.1:1234/v1"
timeout_ms = 30000
max_retries = 1
```

If your embedder and analysis runtimes share a host or retry policy, prefer a named profile and select it from each runtime surface:

```toml
[runtime.profiles.local-lm-studio]
provider = "openai_compatible"
endpoint = "http://127.0.0.1:1234/v1"
timeout_ms = 30000
max_retries = 1

[runtime.embedder]
profile = "local-lm-studio"
model = "nomic-embed-text-v1.5"
```

A practical setup: load `nomic-embed-text-v1.5` in [LM Studio](https://lmstudio.ai), expose it on `localhost:1234`, then:

```sh
pituitary status --check-runtime embedder
pituitary index --rebuild
```

For provider-backed qualitative analysis used by `compare-specs`, `check-doc-drift`, impact-severity enrichment in `analyze-impact`, metadata inference in `canonicalize`, and bounded re-adjudication in `check-compliance`:

```toml
[runtime.analysis]
profile = "local-lm-studio"
model = "your-analysis-model"
timeout_ms = 120000
max_response_tokens = 2048
```

Retrieval stays deterministic. The analysis model only touches narrowly shortlisted context.

Set `runtime.analysis.max_response_tokens` when you want one explicit cap on chat-completion output across Pituitary's qualitative analysis requests. If you omit it, Pituitary keeps bounded per-command defaults instead, so runtime probes stay tiny while `compare-specs`, impact severity checks, doc-drift refinement, and compliance adjudication get slightly larger response budgets.

For `compare-specs` and `check-doc-drift`, Pituitary selects section-level evidence by relevance to the counterpart spec or document instead of taking the first sections by position. `check-doc-drift` still starts from deterministic drift items; the analysis runtime only refines those shortlisted findings, so different models can legitimately emit the same structural result.

When choosing `runtime.analysis`, optimize for bounded semantic adjudication rather than open-ended chat:

- strong instruction following under tight evidence constraints
- reliable structured-output hygiene
- concise answers without verbose reasoning text or intermediate chain-of-thought
- enough context for the prompt plus Pituitary's small shortlisted evidence bundle; typical general-purpose `8k`-`32k` context is sufficient, with larger windows optional
- a parameter and active-parameter footprint that fits your local hardware and latency budget

Examples today include recent instruct-capable Qwen and Mistral models, but Pituitary does not require one specific analysis model.

Validate both runtimes with:

```sh
pituitary status
pituitary status --check-runtime all
```

`pituitary status` reports the resolved runtime config for `runtime.embedder` and `runtime.analysis`, including the active profile name when one is selected. `pituitary status --check-runtime ...` uses those resolved values for the live probe and echoes the same profile / provider / model / endpoint / timeout assumptions in the probe output.

`check-compliance` and `check-doc-drift` also record the configured analysis runtime in their JSON `result.runtime.analysis` block, including whether the command actually consulted the model during that run.

## Runtime Matrix

The most common operator mistake is assuming `runtime.analysis.model` affects every semantic command. It does not. The command/runtime relationship today is:

| Command | `runtime.embedder` | `runtime.analysis` |
|---|---|---|
| `check-compliance` | Yes, for semantic retrieval / fallback target embedding | Optional bounded adjudication |
| `check-doc-drift` | No live provider call; uses the existing index | Optional refinement of deterministic drift items |
| `check-terminology` | Optional semantic near-miss search when a real embedder is configured | No |
| `compile` | Same as `check-terminology`, because it runs the terminology audit first | No |
| `compare-specs` | No | Optional qualitative comparison refinement |
| `analyze-impact` | No | Optional severity enrichment |
| `canonicalize` | No | Optional metadata inference |

Two consequences follow from that table:

- Changing `runtime.analysis.model` will not change `check-terminology` or `compile`.
- Changing `runtime.embedder` does not retroactively affect an existing index until you rebuild it.

When you are unsure why a result changed, confirm both the command/runtime matrix and the index freshness before assuming the wrong model is being consulted.

For Nomic-compatible models, Pituitary automatically applies the required `search_document:` / `search_query:` prefixes.

## Retrieval Mode Matters

The default `fixture` embedder is the deterministic baseline for tests, CI, and zero-credential evaluation. It is not the best retrieval runtime for real corpora. If you are evaluating search quality, overlap ranking, drift detection, or terminology audits on a real repo, switch to a real local embedding runtime first and then rebuild the index.

### Hybrid vs Lexical Search

`search-specs` answers queries in one of two retrieval modes:

- **Hybrid** (default). Vector search is fused with the snapshot's FTS index and reranked. Requires the configured embedder to be reachable at query time. The `result.score_kind` is `hybrid_relevance`.
- **Lexical** (FTS-only). The embedder is bypassed entirely; results come from the snapshot's FTS5 index. The `result.score_kind` is `lexical_relevance`.

Lexical mode is selected in two ways:

1. **Explicit opt-in.** Pass `--lexical` on the CLI or set `"mode": "lexical"` on the MCP `search_specs` request. Use this when you want a deterministic, embedder-free retrieval — for example, on CI without a configured runtime, in offline development, or when the embedder is rate-limited and you want to avoid the call.
2. **Automatic fallback.** When hybrid retrieval is requested but the configured embedder runtime is unavailable (unloaded model, unreachable endpoint, transport failure), `search-specs` automatically retries the request as lexical instead of erroring. The result's `provenance` block records the downgrade:

   ```json
   "provenance": {
     "mode": "lexical",
     "embedder_bypassed": true,
     "fallback_reason": "embedder_unavailable"
   }
   ```

   Successful hybrid responses carry `provenance.mode = "hybrid"`. Explicit lexical requests leave `fallback_reason` empty. Inspect `provenance` before treating lexical results as equivalent to hybrid: lexical recall is bounded by surface-form overlap with the query and will miss conceptual matches that hybrid retrieval would surface.

### Matryoshka Prefilter (Hybrid Path)

For large corpora — typically a few thousand chunks and up — the dense arm of hybrid retrieval can be sped up by running a truncated-prefix vector prefilter and rescoring the shortlist with full-dimension cosine. This is exposed as an explicit opt-in on the runtime config:

```toml
[runtime.search]
matryoshka_prefilter_dimension = 256
```

- **Default `0` (off).** Hybrid search runs at the full stored embedder dimension end-to-end. This preserves byte-for-byte the pre-#341 behavior.
- **Positive value.** Stroma's `SearchParams.SearchDimension` runs the first-pass scan at the truncated prefix length and rescores the shortlist with full-dim cosine. The value must be `>= 0`; if it exceeds the stored embedder dimension, pituitary rejects the query in a local SQLite-metadata preflight before any embedder method runs (so a config typo cannot burn an OpenAI-compatible round trip waiting for stroma to reject the same value post-embedding).
- **Embedder requirement.** Only meaningful for embedders trained with Matryoshka Representation Learning (MRL) — most OpenAI embedding models, Nomic Embed Text v1.5, and a handful of OSS models. Non-MRL embedders (including the default `fixture` runtime) will see recall regressions if the prefilter is enabled, because their truncated prefixes do not preserve nearest-neighbour ranking. Pituitary cannot detect MRL training automatically; the gate is operator opt-in. Leave the field at `0` unless you know your embedder is MRL.
- **Scope.** The knob only takes effect on the hybrid `Search` path. Stroma v3's `VectorSearchQuery` does not expose `SearchDimension`, so the vector-only call sites — `search-specs --semantic-similarity`, the artifact shortlist used by repository-similarity analysis, and the semantic terminology near-miss scan — continue to run at full stored dimension regardless of this config.
- **Snapshot format.** Vector storage format is not a Pituitary runtime config surface. This direct-Stroma line supports only its default snapshot format and forces a rebuild before reusing older non-default snapshots.

If you are unsure whether your embedder is MRL-trained, benchmark recall@k on a representative corpus with the prefilter on and off before enabling it in production.

## Indexing Pipeline

When you run `pituitary index --rebuild`:

1. discovers all spec bundles and Markdown docs in configured sources
2. validates the relation graph; cycles and contradictions fail fast
3. chunks content by heading-aware sections
4. reuses unchanged chunk embeddings when schema, embedder, and source fingerprints match
5. generates fresh embeddings only for new or changed chunks
6. stores everything in a single SQLite database
7. writes to a staging DB first and atomically swaps in, so a failed rebuild never corrupts your index

Use `--full` to skip reuse and force a complete re-embed.

The chunking and embedding stages run as a streaming pipeline against stroma: records are yielded one at a time into `stindex.RebuildFromSource`, which chunks, embeds, and flushes to the staging snapshot in bounded internal batches. Peak memory inside stroma's pipeline therefore scales with its batch size rather than with corpus size, so large workspaces no longer accumulate the full chunks-and-vectors plan before commit. The source loader still materializes a `LoadResult` of all spec and doc records up front today; reducing that resident set requires a loader-side change that yields records lazily from disk, which is tracked separately. Incremental updates use the same streaming entry point: `pituitary update` consumes the union of configured sources via `stindex.SyncFromSource`, which preserves today's "configure source change → next update reflects removals" behavior without paying full-rebuild cost. Stroma retains changed records and their planned chunks/vectors until commit, so a single update that touches a very large fraction of the corpus is still bounded by available memory; for that case `pituitary index --rebuild` remains the safer path.

Query commands validate index freshness before executing. A stale index fails fast with a rebuild hint.

## JSON Timings

`check-compliance`, `check-doc-drift`, `check-terminology`, and `compile` accept `--timings` with `--format json`. The CLI envelope then includes a top-level `timings` block:

```json
{
  "request": { "...": "..." },
  "result": { "...": "..." },
  "timings": {
    "total_ms": 37,
    "indexing_ms": 6,
    "embedding_ms": 12,
    "analysis_ms": 9,
    "analysis_calls": 2,
    "embedding_calls": 1
  },
  "warnings": [],
  "errors": []
}
```

Use this to answer operational questions such as:

- did the command actually call an embedder or analysis model?
- did a runtime change increase `analysis_calls` or `embedding_calls`?
- is time going into freshness validation/index loading versus model work?

## Output Formats

All commands share a consistent JSON envelope:

```json
{
  "request": { "...": "..." },
  "result": { "...": "..." },
  "warnings": [],
  "errors": []
}
```

Additional formats:

- `search-specs`: `--format table`
- `review-spec`: `--format markdown`
- `review-spec`: `--format html`

Agent-facing defaults:

- `PITUITARY_FORMAT=json` sets JSON as the default output format for commands that support it.
- When stdout is redirected to a file or pipe, commands default to JSON automatically unless `--format` overrides it.
- `pituitary schema <command> --format json` returns machine-readable request and response schemas plus mutation metadata.

Trust metadata:

- Results that include raw repo excerpts or evidence expose `result.content_trust`.
- Treat returned workspace text as untrusted content, not as instructions to execute.

## MCP Server

Pituitary ships an optional MCP server over stdio:

```sh
pituitary serve --config .pituitary/pituitary.toml
```

Typical client config:

```json
{
  "mcpServers": {
    "pituitary": {
      "command": "pituitary",
      "args": ["serve", "--config", "pituitary.toml"]
    }
  }
}
```

The MCP server exposes 15 tools:

- `search_specs`
- `check_overlap`
- `compare_specs`
- `analyze_impact`
- `check_doc_drift`
- `review_spec`
- `get_intent_outline`
- `expand_intent_context`
- `check_compliance`
- `check_terminology`
- `governed_by`
- `compile_preview`
- `fix_preview`
- `status`
- `explain_file`

For focused context, use `search_specs` first, inspect a selected record with `get_intent_outline`, then call `expand_intent_context` on selected chunk IDs. Pass the returned `snapshot_fingerprint` into every expansion call; expansion rejects missing or stale fingerprints so chunk handles cannot silently cross snapshot boundaries.

`index --rebuild` remains CLI-only by design.
