> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pecta.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Quality gates in Pecta: how they run and fail

> Quality gates are parallel, fail-fast pass/fail checks on agent output that run in-process with no network overhead and no training period needed.

A quality gate is a named function that receives an agent's output and returns a pass/fail verdict with an optional reason string. Gates are the core primitive in Pecta: they run on the very first evaluation with no baseline period, execute in parallel within a shared timeout budget, and stop as soon as one fails (fail-fast). Every gate runs inside your own process — there is no network call on the hot path.

## How gates run

When you call `engine.evaluate()`, the engine starts all configured gates simultaneously using `Promise.all`. A shared `AbortController` enforces the timeout budget (default 50 ms). If any gate fails and `failFast` is enabled (the default), the controller aborts immediately and remaining in-flight gates receive an abort signal and return without doing further work.

```
engine.evaluate(ctx)
│
├─ gate: latency     ──► pass  (1 ms)
├─ gate: filesystem  ──► FAIL  (2 ms) ──► abort signal sent
├─ gate: pii         ──► aborted       (0 ms)
│
└─ EvaluationResult { passed: false, total_latency_ms: ~2 }
```

The `total_latency_ms` reflects wall-clock time, not the sum of individual gate latencies, because gates run concurrently.

## Fail-fast and timeout behaviour

Two configuration options control how the engine handles failures and slow gates:

* **`failFast`** (default `true`) — as soon as one gate returns `passed: false`, the engine sends an abort signal to all other running gates and marks them as aborted in the result.
* **`timeout`** (default `50` ms) — if the wall-clock budget expires before all gates complete, the engine aborts everything. Timed-out gates appear as failed in the result with reason `pecta:timeout`.

For RTB pipelines you can tighten the budget to `15` ms. For MCP proxy use cases, the default `50` ms gives gates enough room to do regex and schema checks comfortably.

## Gate results

Each gate contributes one entry to `result.gates`:

```typescript theme={null}
{
  name: string;         // gate identifier, e.g. "filesystem"
  passed: boolean;
  reason?: string;      // present when passed is false or gate was aborted
  latency_ms: number;   // wall-clock time for this gate only
  skipped?: boolean;    // gate chose not to run; treated as pass
}
```

The top-level `EvaluationResult`:

```typescript theme={null}
{
  evaluation_id: string;   // unique ID per call
  agent_id: string;
  tool?: string;
  passed: boolean;         // true only if every gate passed or skipped
  gates: GateResult[];
  total_latency_ms: number;
  timestamp: string;       // ISO 8601 UTC
}
```

`passed` is `true` only when every gate either passes or explicitly sets `skipped: true`. A single `passed: false` gate makes the entire evaluation fail.

## Built-in gates

### General

| Gate         | Import                     | Description                                                                                               |
| ------------ | -------------------------- | --------------------------------------------------------------------------------------------------------- |
| `latency`    | `gates.latency({ maxMs })` | Fails if `ctx.latency_ms` exceeds the configured threshold.                                               |
| `schema`     | `gates.schema(zodSchema)`  | Validates `ctx.output` against a Zod schema; exposes Zod issues in `details`.                             |
| `filesystem` | `gates.filesystem()`       | Detects destructive commands (`rm -rf`), path traversal (`../`), and references to sensitive directories. |
| `pii`        | `gates.pii()`              | Flags email addresses, SSN-shaped strings, and phone numbers in the output.                               |
| `content`    | `gates.content()`          | Rejects empty output and AI refusal phrases that indicate a failed generation.                            |

### RTB (under `gates.rtb`)

| Gate             | Import                                      | Description                                                                                                                       |
| ---------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `tmaxGuard`      | `gates.rtb.tmaxGuard({ bufferMs })`         | Skips all downstream gates when the OpenRTB `tmax` deadline is already exhausted, saving the remaining budget for bid submission. |
| `impidMatch`     | `gates.rtb.impidMatch()`                    | Verifies that every `bid.impid` in the response matches an `imp.id` in the original request.                                      |
| `adomainVerify`  | `gates.rtb.adomainVerify()`                 | Rejects placeholder or malformed advertiser domain entries.                                                                       |
| `bidSanity`      | `gates.rtb.bidSanity({ maxFloorMultiple })` | Fails bids that are unrealistically far above the floor price (default 50×).                                                      |
| `audienceSafety` | `gates.rtb.audienceSafety()`                | Blocks ad categories that violate COPPA or are mismatched with child-directed inventory.                                          |
| `bcatCompliance` | `gates.rtb.bcatCompliance()`                | Ensures response creatives do not appear in the request's `bcat` blocked-category list.                                           |

<Note>
  RTB gates read standard OpenRTB fields from `ctx.input` (the bid request) and `ctx.output` (the bid response). Pass `tmaxMs` and `startedAt` on the context to enable the `tmaxGuard` gate.
</Note>

## Writing a custom gate

Any object with a `name` string and a `run` function is a valid gate. Check the abort signal at the start of expensive operations.

```typescript theme={null}
const myGate = {
  name: "my.custom",
  run: async (ctx, signal) => {
    if (signal.aborted) return { passed: false, reason: "aborted" };
    // perform your check against ctx.output
    return { passed: true };
  },
};
```

`run` returns `GateOutcome`:

```typescript theme={null}
{
  passed: boolean;
  reason?: string;
  skipped?: boolean;
  details?: unknown;   // structured detail; never include user payloads
}
```

The engine merges in `name` and `latency_ms` before adding the result to `EvaluationResult.gates`.

## Further reading

* [Latency gate](/gates/latency)
* [Schema gate](/gates/schema)
* [Filesystem gate](/gates/filesystem)
* [RTB gates](/gates/rtb/overview)
