> For the complete documentation index, see [llms.txt](https://govplane.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://govplane.gitbook.io/docs/documentation/basic-concepts/evaluation.md).

# Evaluation

Evaluation is local, synchronous and offline. The SDK holds a verified bundle in memory and answers from it — no network call, no round trip, no shared state.

```ts
const decision = govplane.evaluate({
  target:  { service: "auth", resource: "login", action: "authenticate" },
  context: { failedAttempts: 6 },
});

if (decision.decision === "deny") return forbidden();
```

### What you pass

Two things: **what** is being attempted, and **what is known** about the attempt.

```ts
{
  target:  { service: string, resource: string, action: string },
  context?: Record<string, unknown>
}
```

`target` selects the rules. `context` is what their conditions read — see Conditional Rules. A rule with no `when` never looks at context; a rule with one cannot decide without it.

Context stays on your machine. It is never transmitted, and traces record only rule identifiers and outcomes, never the values you passed.

### What you get back

```ts
{
  decision: "allow" | "deny" | "kill_switch" | "throttle" | "custom",
  reason:   "rule" | "default",
  policyKey?: string,
  ruleId?:    string,
  // plus the descriptor for throttle, kill_switch and custom
}
```

`reason` tells you *how* the answer was reached, and it is the field most worth reading after `decision`:

| `reason`    | Meaning                                             | `ruleId` |
| ----------- | --------------------------------------------------- | -------- |
| `"rule"`    | A rule matched and fired                            | present  |
| `"default"` | A policy default applied, or nothing matched at all | absent   |

A default decision that carries a `policyKey` came from that policy's default. One with **no** `policyKey` is the global deny-by-default — nothing in the bundle covered the target.

```ts
{ decision: "allow", reason: "default", policyKey: "login-protection" } // the policy's default
{ decision: "deny",  reason: "default" }                               // nothing covered it
```

### How the winner is chosen

Every rule that matches the target and passes its condition contributes a candidate. The engine then picks one:

1. **Effect precedence first** — `kill_switch > deny > throttle > allow > custom`
2. **Priority second**, descending, within the winning effect type
3. **`policyKey` then `ruleId`**, alphabetically, to break remaining ties

Precedence beats priority: a `kill_switch` at priority 1 wins over a `deny` at 100. Among throttles the **strictest** wins — the lowest requests-per-second — not the highest priority.

If no rule fired, each policy that governs the target offers its default, and those compete by the same precedence. If nothing at all applies, the answer is **deny by default**.

{% hint style="info" %}
Deny-by-default is unconditional. No bundle, an unrecognised schema version, or a target nothing covers — all produce `{ decision: "deny", reason: "default" }`. The SDK never fails open.
{% endhint %}

### Shorter answers

Call sites that only branch on the outcome can ask for it directly:

```ts
if (!govplane.evaluate({ target, context }, { asBoolean: true })) return forbidden();

const allowed = govplane.evaluate({ target, context }, { asBinary: true });   // 1 | 0
const effect  = govplane.evaluate({ target, context }, { effectOnly: true }); // "allow" | "deny" | …
```

The return type follows the option, so none of these needs narrowing.

| Decision      | `asBoolean` | `asBinary` |
| ------------- | ----------- | ---------- |
| `allow`       | `true`      | `1`        |
| `custom`      | `true`      | `1`        |
| `deny`        | `false`     | `0`        |
| `kill_switch` | `false`     | `0`        |
| `throttle`    | **`false`** | **`0`**    |

{% hint style="warning" %}
`throttle` reduces to `false` even though it permits the action. A boolean cannot carry the limit, so a caller who saw `true` would proceed at an unbounded rate — failing open on the one effect whose purpose is to bound it.

**If you act on throttles, read the full decision** and use `decision.throttle`.
{% endhint %}

All three shapes are lossy by design: a kill switch's service, a custom effect's value and a throttle's window exist only on the full object. Set at most one — setting two throws `ConfigurationError` rather than choosing for you.

### Evaluating a single rule

A rule can be addressed by name instead of by target:

```ts
govplane.evaluate({ ruleId: "r_deny_after_five", context: { failedAttempts: 6 } });
```

This is for call sites that already know which rule they mean, where restating the service, resource and action is repetition that can drift from the bundle. Target matching is skipped entirely — the rule's own target says what it covers, not whether it was selected — but `context` still applies, because the rule's condition reads it.

**Both ways of asking give the same answer.** If the rule does not fire, its owning policy's default applies, exactly as it would had you named the target:

```ts
// r_deny_after_five fires at >= 5; its policy allows by default
govplane.evaluate({ ruleId: "r_deny_after_five", context: { failedAttempts: 3 } });
// → { decision: "allow", reason: "default", policyKey: "login-protection" }
```

Rule IDs are unique within a policy but **not** across a bundle. Given only a `ruleId`, the first match in bundle order wins. Name the policy to remove the ambiguity:

```ts
govplane.evaluate({ ruleId: "r_deny_large", policyKey: "refund-control", context });
```

If the rule — or the named policy — is not in the bundle, this **throws** `RuleNotFoundError` rather than returning a denial. Naming something absent is a mistake in the calling code, and reporting it as `deny` would leave a typo looking like a policy decision nobody investigates.

### Handling every outcome

A decision is a signal. Acting on it is your application's job:

```ts
const result = govplane.evaluate({ target, context });

switch (result.decision) {
  case "allow":
    break; // proceed

  case "deny":
    return reply.status(403).send({ error: "Forbidden" });

  case "kill_switch":
    logger.error("kill switch active", { service: result.killSwitch.service });
    return reply.status(503).send({ error: "Service Unavailable", reason: result.killSwitch.reason });

  case "throttle":
    // The SDK signals intent; your infrastructure enforces the limit.
    if (!(await rateLimiter.check(result.throttle))) {
      return reply.status(429).send({ error: "Too Many Requests" });
    }
    break;

  case "custom":
    applyVariant(result.parsedValue ?? JSON.parse(result.value));
    break;
}
```

See Effects for each type in detail.

### Understanding a decision

When an answer is not what you expected, ask for a trace rather than guessing:

```ts
import { formatTrace } from "@govplane/runtime-sdk";

const result = govplane.evaluateWithTrace(
  { target, context },
  { level: "full" },
);

console.log(formatTrace(result.trace, { includeDiscarded: true }));
```

```
policies=2 rules=1 matched=0
  winner → policy=login-protection rule=__default__ effect=allow priority=-1
  discarded rules:
  - policy=login-protection rule=r_deny_after_five effect=deny reason=when_false
```

`includeDiscarded` is the important part — without it you see the winner but not the rules that were ruled out, or why. `reason=when_false` means the target matched and the condition did not.

Two markers to recognise: `rule=__default__` and `priority=-1` both mean the decision came from a policy default rather than a rule.

Levels are `off`, `errors`, `sampled` and `full`. Traces carry no context values.

### Context validation

Off unless you configure it. When you pin a context policy, it is enforced:

```ts
await createLocalClient({
  bundlePath: "./policy-bundle.json",
  verify: { algorithm: "ECDSA_SHA_256", publicKey },
  engine: {
    contextPolicy: {
      allowedKeys: ["failedAttempts", "plan", "country"],
      blockLikelyPiiKeys: true,
    },
  },
});
```

Passing a key outside `allowedKeys` throws at evaluation time. `blockLikelyPiiKeys` additionally rejects keys that look like personal data — worth having, since context is the easiest place for an email address to end up by accident.

### Next

* [CLI Installation](/docs/documentation/cli/installation-and-quickstart.md)
* [Installing the Toolkit Extension](/docs/documentation/cli/installing-the-cli-toolkit-extension.md)
* [Node.js — Installation & Quick Start](/docs/documentation/sdk-for-javascript-node.js/installation-and-quick-start.md)
* [Policy Bundle Schema](/docs/documentation/schemas/policy-bundle.md) — the document the SDK loads


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://govplane.gitbook.io/docs/documentation/basic-concepts/evaluation.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
