Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Project Overview

Geval is a **decision orchestration and reconciliation** tool for AI systems. It consumes **signals** (JSON) and **policy** (YAML), evaluates rules in priority order, and produces a deterministic decision: **PASS**, **REQUIRE_APPROVAL**, or **BLOCK**. It does not run evals, call APIs, or compute metrics—it only reconciles your rules against your signals.
Geval is a **decision orchestration and reconciliation** tool for AI systems. It consumes **signals** (JSON) and **policy** (YAML), evaluates **all** rules (unique priorities; **1** = highest), surfaces every match, applies the **best-priority** winner per policy, and merges policies/contracts with **`worst_case`** (BLOCK > REQUIRE_APPROVAL > PASS). It does not run evals, call APIs, or compute metrics—it only reconciles your rules against your signals.

**Core Philosophy**: Geval has no “brain.” You provide signals and rules; Geval applies the rules and returns one outcome. Same inputs + same policy = same outcome.

Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/geval.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Geval Decision Check - run the Rust decision engine in CI
# Use this workflow to evaluate signals against policy on pull requests.
# Geval Decision Check - run `cargo test`, build release binary, evaluate example signals on PRs.

name: Geval Decision Check

Expand All @@ -24,6 +23,9 @@ jobs:
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
shell: bash

- name: Test Geval
run: cargo test --manifest-path geval/Cargo.toml

- name: Build Geval
run: cargo build --release --manifest-path geval/Cargo.toml

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ You can add labels like `component` or `system` if you need them. [Full example

### Step 2: Your contract and policies

A **contract** is a YAML file that lists one or more **policy** files and a **combination rule** (how to merge their outcomes). Each **policy** file contains ordered rules: **When** [condition on signals], **then** [pass / block / require_approval].
A **contract** is a YAML file that lists one or more **policy** files and a **combination rule** (how to merge their outcomes). Each **policy** file contains rules with **unique** priorities: **When** [condition on signals], **then** [pass / block / require_approval].

**Prefer a form instead of writing YAML by hand?** Use **[config.geval.io](https://config.geval.io)** to generate Geval-compatible `contract.yaml` and policy files (download or copy), then validate with `geval validate-contract` and run `geval check` as below.

Expand All @@ -113,7 +113,7 @@ Example contract — save as `contract.yaml`:
```yaml
name: my-gate
version: "1.0.0"
combine: all_pass
combine: worst_case
policies:
- path: policy.yaml
```
Expand Down Expand Up @@ -143,7 +143,7 @@ policy:
action: pass
```

**Combine rules:** `all_pass` = PASS only if every policy passes; `any_block_blocks` = any policy BLOCK → overall BLOCK. **Operators:** `>`, `<`, `>=`, `<=`, `==`, `presence`. **Actions:** `pass`, `block`, `require_approval`.
**Combine (`worst_case`):** any **BLOCK** wins; else any **require_approval**; else **pass**. **Rule priorities** must be **unique** per policy; **1** = highest; Geval records every match and the **best** priority wins. **Operators:** `>`, `<`, `>=`, `<=`, `==`, `presence`. **Actions:** `pass`, `block`, `require_approval`.

[Full example →](geval/examples/contract.yaml) and [policy →](geval/examples/policy.yaml)

Expand Down
2 changes: 1 addition & 1 deletion geval/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ See **[Installation](docs/installation.md)** for download links, build-from-sour
## Principles

- **Local, deterministic** – single binary, no external services
- **Rule-based** – priority-ordered rules; first match wins; no scoring or ML
- **Rule-based** – unique priorities per policy (**1** = highest); all matches shown; best priority wins; no scoring or ML
- **Auditable** – policy and signal hashes (SHA256), immutable decision artifacts

## Quick start
Expand Down
16 changes: 8 additions & 8 deletions geval/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Geval is the **thin layer** that sits between **evidence** (signals) and **polic

```mermaid
flowchart LR
subgraph legacyBefore [Before Geval]
subgraph beforeGeval [Before Geval]
direction TB
mixed[Non-uniform signals — numbers flags presence labels components AB KPIs]
discuss[Slack and meetings — people interpret and debate]
Expand Down Expand Up @@ -244,24 +244,24 @@ flowchart TB
| **Contract** | YAML file: `name`, `version`, `combine` (rule), and list of policy paths. The unit of evaluation. |
| **Policy** | YAML file: optional `name`/`version`, `environment`, and ordered `rules`. Each rule has `when` (conditions) and `then` (action: pass / block / require_approval). |
| **Signals** | JSON: optional `name`/`version`, and array of signal objects (metric, value, component, etc.). Facts fed into the engine. |
| **Combination rule** | How to merge outcomes from multiple policies: `all_pass` or `any_block_blocks`. |
| **Combination rule** | How to merge outcomes from multiple policies/contracts: **`worst_case`** — BLOCK > REQUIRE_APPROVAL > PASS. |

## Data flow

1. **Load contract** – Parse contract YAML; resolve policy paths relative to the contract file; load each policy.
2. **Load signals** – Parse signals JSON; build an in-memory signal graph (metric → value lookup).
3. **Evaluate each policy** – For each policy, evaluate rules in priority order; first matching rule gives that policy’s outcome (PASS / REQUIRE_APPROVAL / BLOCK).
4. **Combine (policies)** – Apply the contract’s combination rule to the list of policy outcomes → one combined decision **per contract**.
5. **Combine (contracts)** – If multiple contract files are passed (`geval check -c a.yaml -c b.yaml`), apply **`--combine-contracts`** to each contract’s combined outcome → one **overall** PR-level decision (same rule vocabulary: `all_pass`, `any_block_blocks`).
6. **Artifact** – Write `.geval/decisions/<timestamp>.json` (v3) with `bundle_hash`, each contract block, `contracts_combine_rule`, and overall outcome + hashes.
3. **Evaluate each policy** – For each policy, **every** rule is checked against the signal graph. **All** matches are recorded; the **winning** rule is the one with the **best** priority (**1** = highest; larger numbers are lower). That rule’s action is the policy outcome (PASS / REQUIRE_APPROVAL / BLOCK). **Priorities must be unique** within a policy (validated at load).
4. **Combine (policies)** – Apply **`worst_case`** merging to the list of policy outcomes → one combined decision **per contract**.
5. **Combine (contracts)** – If multiple contract files are passed (`geval check -c a.yaml -c b.yaml`), apply **`--combine-contracts`** (same **`worst_case`** semantics) to each contract’s combined outcome → one **overall** PR-level decision.
6. **Artifact** – Write `.geval/decisions/<timestamp>.json` (v4) with `bundle_hash`, each contract block, `contracts_combine_rule`, per-policy `matching_rules`, and overall outcome + hashes.

## Module layout

```
geval/src/
contract/ # Contract = multiple policies + combine rule
model.rs # ContractDef, PolicyRef
combine.rs # CombineRule (all_pass, any_block_blocks), apply_combine_rule
combine.rs # CombineRule (worst_case), apply_combine_rule
loader.rs # load_contract, load_contract_and_policies, parse_contract_str
runner.rs # run_contract, load_run_contracts → ContractResult / MultiContractRun
policy/ # Single policy model and parser
Expand All @@ -272,7 +272,7 @@ geval/src/
signal_graph/ # Build lookup from signals for rule matching
signals/ # Load signals JSON (name, version, signals array)
hashing/ # SHA256 for contract, policy, signals, contract bundle (audit)
artifact/ # write_multi_contract_artifact (v3: multi-contract + overall)
artifact/ # write_multi_contract_artifact (v4: multi-contract + overall)
explanation/ # explain_contract_result, explain_multi_contract_result, explain_decision
approval/ # Approval/rejection artifact (versioned)
cli/ # Commands: check, init, demo, explain, validate-contract, approve, reject
Expand Down
12 changes: 6 additions & 6 deletions geval/docs/auditing.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Geval is designed so **nothing is unversioned**: every decision and every action

- **Why was this deployed?** – Decision report and matched rule (and optional approval reason).
- **Who approved it?** – `geval approve` writes an artifact with `approved_by`, `reason`, and artifact `version`.
- **What policy (contract) was used?** – Artifact stores each contract’s identity, `contract_hash`, per-policy hashes, and (v3) `bundle_hash` for the ordered set of contracts.
- **What policy (contract) was used?** – Artifact stores each contract’s identity, `contract_hash`, per-policy hashes, and `bundle_hash` for the ordered set of contracts.
- **What signals were used?** – Artifact stores `signals_name`, `signals_version`, and `signals_hash` (SHA256).
- **Which Geval binary?** – Artifact stores `geval_version`.

Expand All @@ -17,22 +17,22 @@ Geval is designed so **nothing is unversioned**: every decision and every action
Each `geval check` run writes:

- **Path:** `.geval/decisions/<timestamp>.json`
- **Contents (artifact_version 3, multi-contract):**
- `artifact_version` – schema version (`"3"`)
- **Contents (artifact_version 4, multi-contract):**
- `artifact_version` – schema version (`"4"`)
- `geval_version` – binary version that produced the decision
- `bundle_hash` – SHA256 over the ordered list of `(contract_path, contract_hash)` (audit the exact contract set)
- `contracts_combine_rule` – how each contract’s **combined** outcome was merged (e.g. `all_pass`, `any_block_blocks`)
- `contracts_combine_rule` – how each contract’s **combined** outcome was merged (for example `worst_case`)
- `contracts` – array of blocks, each with:
- `contract_path`, `contract_name`, `contract_version`, `contract_hash`
- `combine_rule` (policies within that contract)
- `policy_results` – `{ policy_path, policy_name?, policy_version?, policy_hash, outcome, matched_rule? }[]`
- `policy_results` – `{ policy_path, policy_name?, policy_version?, policy_hash, outcome, matched_rule?, matching_rules? }[]` (`matching_rules` lists every rule whose `when` matched, in priority order; `matched_rule` is the winner)
- `combined_decision`, `combined_matched_rule`, `combined_reason` (outcome for that contract)
- `overall_combined_decision`, `overall_matched_rule`, `overall_reason` – PR-level outcome after `contracts_combine_rule`
- `signals_name`, `signals_version`, `signals_hash` – signals identity and content hash
- `timestamp` – ISO8601
- `approval` – optional; set when an approval is recorded for this decision

Older tooling may still reference **artifact_version 2** (single flat contract); Geval now writes v3 only.
Older tooling may still reference **artifact_version 2** or **3**; current Geval writes **v4** (adds `matching_rules` per policy).

### Approval artifact

Expand Down
12 changes: 6 additions & 6 deletions geval/docs/customer-demo-feature.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ These are **inputs** (your pipeline or eval harness writes one `signals.json` pe

## 3. Policies — meaningful split (and what to say)

Use **separate policy files** so **ownership** is clear (security vs product vs business). The **contract** lists them and sets **`combine: all_pass`** so: *every policy must pass; any BLOCK wins; REQUIRE_APPROVAL without BLOCK means “needs a human”.*
Use **separate policy files** so **ownership** is clear (security vs product vs business). The **contract** lists them and sets **`combine: worst_case`** so outcomes merge by severity: *any **BLOCK** wins; else any **REQUIRE_APPROVAL**; else **PASS**.*

| Policy file | Owner (story) | Why separate |
|-------------|---------------|--------------|
Expand All @@ -47,7 +47,7 @@ Use **separate policy files** so **ownership** is clear (security vs product vs

## 4. Rules customers actually write (examples + why)

Rules are **ordered**; **first match wins**. Priorities below are **intentional** (stop fast on catastrophes, then quality, then business).
Rules use **unique** priorities (**`1`** = highest precedence). Geval shows **every** rule whose condition matched; the **winning** rule is the one with the **best** priority. The table below orders rules by priority on purpose (catastrophes first, then quality, then business).

### 4.1 `policies/safety.yaml`

Expand Down Expand Up @@ -78,24 +78,24 @@ Rules are **ordered**; **first match wins**. Priorities below are **intentional*
```yaml
name: support-copilot-release-gate
version: "1.0.0"
combine: all_pass
combine: worst_case
policies:
- path: policies/safety.yaml
- path: policies/product_quality.yaml
- path: policies/business_risk.yaml
```

**Customer line:**
*`all_pass`* means: every policy must end in PASS for an overall PASS; any policy BLOCK → overall BLOCK; if no BLOCK but something needs approval → overall REQUIRE_APPROVAL.
*`worst_case`* means: merge policy outcomes by severity — any **BLOCK** → overall **BLOCK**; else any **REQUIRE_APPROVAL** → overall **REQUIRE_APPROVAL**; else **PASS**.

---

## 6. End-to-end: what happens (no implementation jargon)

1. **CI (or a human) runs** your evals and **writes `signals.json`** with the metrics above (and optional `name` / `version` on the file for audit).
2. **Geval loads** the contract → loads the three policies → builds a small **lookup** from signals.
3. **Per policy**, rules run in **priority order**; the **first** rule whose `when` matches decides that policy’s outcome.
4. **Per policy outcomes** are merged with the contract’s **`combine`** rule → **one** outcome for the run.
3. **Per policy**, **every** rule is checked. **All** matches are listed; the rule with the **best** priority (**`1`** highest) **wins** and sets that policy’s outcome.
4. **Per policy outcomes** are merged with **`worst_case`** (same severity order: BLOCK > REQUIRE_APPROVAL > PASS) → **one** outcome for the contract run.
5. **Geval exits** with 0 / 1 / 2 and can write a **decision artifact** (who/what/when + hashes).

**Customer line:**
Expand Down
14 changes: 5 additions & 9 deletions geval/docs/extending.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,11 @@ Run: `cargo test --manifest-path geval/Cargo.toml`.
- Bump version in `geval/Cargo.toml` and, if needed, `DECISION_ARTIFACT_VERSION` or `APPROVAL_ARTIFACT_VERSION`.
- Note breaking changes (e.g. CLI now requires `--contract` instead of `--policy`) in release notes.

## Adding a new combination rule

1. **contract/combine.rs**
- Add a variant to `CombineRule` with `#[serde(rename = "snake_case")]` (or explicit rename).
- Implement `Default` if it should be the default when omitted in YAML.
- In `apply_combine_rule`, add a `match` branch that implements the new semantics.
- Implement `Display` and `FromStr` for CLI/artifact string.
2. **Tests** – Add tests in `contract/combine::tests` for the new rule (e.g. N outcomes → expected combined outcome).
3. **Docs** – Update [signals-and-rules.md](signals-and-rules.md) or [versioning.md](versioning.md) to describe the new rule.
## Combination rules

Today there is **one** merge semantics: **`worst_case`** (BLOCK > REQUIRE_APPROVAL > PASS), implemented in `contract/combine.rs`.

To add a **different** combination mode in the future: add a `CombineRule` variant, implement it in `apply_combine_rule`, extend `Display` / `FromStr`, add tests, and bump `DECISION_ARTIFACT_VERSION` if artifact strings change.

## Adding a new policy or contract field

Expand Down
2 changes: 1 addition & 1 deletion geval/docs/github-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
--env prod
```

Repeat `--contract` for each gate YAML attached to the PR. Optional: `--combine-contracts all_pass` (default) or `any_block_blocks`.
Repeat `--contract` for each gate YAML attached to the PR. Optional: `--combine-contracts worst_case` (default).

## Option B: Download released binary

Expand Down
4 changes: 4 additions & 0 deletions geval/docs/signals-and-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ So:
- **Signals with scores** use the usual comparison operators. Example: “If `accuracy` &lt; 0.9 → block.”
- You can combine both in one policy: some rules key off presence, others off numeric thresholds.

### Priorities within a policy

Each rule has a numeric **`priority`**. **Lower numbers are higher precedence: `1` is the highest.** Every priority must be **unique** within a policy (Geval rejects duplicate values when loading YAML). Geval evaluates **every** rule, records **all** that match, and the **winning** rule is the match with the **best** (numerically smallest) priority; that rule’s `then` action is the policy outcome.

## Example: mixed signals

**signals.json:**
Expand Down
2 changes: 1 addition & 1 deletion geval/docs/versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Example:
```yaml
name: release-gate
version: "2.1.0"
combine: all_pass
combine: worst_case
policies:
- path: policies/security.yaml
- path: policies/quality.yaml
Expand Down
7 changes: 3 additions & 4 deletions geval/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,17 @@ With the example data, the policy matches `business_block`: `engagement_drop` 0.

- **name**, **version** – Identify the contract for audit; bump version when you change policies or combine rule.
- **combine** – How to merge outcomes from multiple policies:
- **all_pass** – PASS only if every policy passes; any BLOCK → BLOCK; any REQUIRE_APPROVAL (no BLOCK) → REQUIRE_APPROVAL.
- **any_block_blocks** – Any policy BLOCK → overall BLOCK; else any REQUIRE_APPROVAL → REQUIRE_APPROVAL; else PASS.
- **worst_case** – Any BLOCK wins; else any REQUIRE_APPROVAL; else PASS.
- **policies** – List of policy file paths (relative to the contract file): e.g. `policy.yaml` or `policies/security.yaml`.

## Policy format

Each policy file has optional **name** and **version**, and **policy** with:

- **environment** – optional.
- **rules** – priority, name, when (metric, component, operator, threshold), then (action, reason).
- **rules** – unique **priority** (**1** = highest), name, when (metric, component, operator, threshold), then (action, reason).

First matching rule wins; no match → PASS.
Every rule is evaluated; all matches are recorded; the **best** (lowest) priority wins; no match → PASS.

## Signals format

Expand Down
2 changes: 1 addition & 1 deletion geval/examples/contract-b.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Second example contract: same policy file, distinct contract identity (multi-contract PR demo).
name: demo-secondary
version: "1.0.0"
combine: all_pass
combine: worst_case
policies:
- path: policy.yaml
2 changes: 1 addition & 1 deletion geval/examples/contract.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@

name: demo
version: "1.0.0"
combine: all_pass
combine: worst_case
policies:
- path: policy.yaml
Loading
Loading