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

# QWED Legal guards for contract verification

> Reference for QWED Legal guards covering deadlines, liability, contradictions, citations, jurisdiction checks, and AI content provenance.

Each guard verifies a specific aspect of legal output. Guards are labeled `DETERMINISTIC`, `MIXED`, or `PARTIAL / HEURISTIC` to indicate the strength of the underlying check.

* `DETERMINISTIC` guards return reproducible, provable results for supported, structured inputs.
* `MIXED` guards run a deterministic computation (date arithmetic, Z3 SAT/UNSAT) over parsed inputs. The computation is provable; the parsed lookup that feeds it is not authority proof.
* `PARTIAL / HEURISTIC` guards apply structural or rule-based checks. A passing result does **not** prove that the underlying legal claim is correct — only that it matched a supported pattern.

When a claim falls outside a guard's supported boundary, the guard fails closed: it rejects or marks the claim unverified rather than accepting it.

## Verification traces and evidence types

As of **v0.4.0**, every guard returns a `verification_trace` — an ordered list of `VerificationStep` records. Each step is tagged with an `evidence_type`, and `VerificationStep.is_proven()` returns `True` **only** for `DETERMINISTIC` steps.

| `evidence_type` | Meaning | `is_proven()` |
| - | - | :-: |
| `DETERMINISTIC` | Proven by math/logic (Z3, date arithmetic, exact compare) | `True` |
| `PARSED` | Read/matched from structure or lookup — not authority proof | `False` |
| `INFERRED` | Pattern/keyword derived — may be wrong on edge cases | `False` |
| `HEURISTIC` | Approximate/statistical signal | `False` |
| `UNSUPPORTED` | Guard cannot model this input — fail-closed | `False` |

```python theme={null}
from qwed_legal import trace_to_dict

# Any guard result exposes .verification_trace
serialized = trace_to_dict(result.verification_trace)  # JSON-safe list of dicts
# each entry carries an explicit "is_proven" flag
```

Use `trace_to_dict()` to export a trace into audit logs. Non-serializable input values are stringified (no silent data loss).

<Info>
  `VerificationStep` and every guard result dataclass are `frozen=True`. Nested evidence containers (`VerificationStep.inputs`, `verification_trace`, `conflicts`, `tiers`, `parsed_components`, `components`) are deep-frozen — mappings become mutation-disabled proxies (in-place union `|=` also blocked), lists become tuples, and sets become sorted tuples. Code that previously mutated a result to flip `DETERMINISTIC → HEURISTIC` after issuance no longer works — the mutation raises rather than silently rewriting authority.
</Info>

## Structured diagnostics and proof references

Every guard result exposes `to_diagnostic(claim_inputs=...)`, which returns a frozen `LegalDiagnosticResult` implementing the ecosystem [3-layer diagnostic contract](/advanced/diagnostics):

* **Layer 1 — `agent_message`**: agent-safe summary (the guard's existing message).
* **Layer 2 — `developer_fields`**: `claim_inputs`, the full `verification_trace`, and a result snapshot.
* **Layer 3 — `proof_ref`**: SHA-256 hash over the RFC 8785 (JCS) canonical JSON of the proof evidence, present only when `status == VERIFIED`.

The status is one of `VERIFIED` / `UNVERIFIABLE` / `BLOCKED`. Downstream gates check `diagnostic.is_authoritative` (equivalent to `proof_ref is not None`) — see [the per-guard mapping table in Overview](/legal/overview) for which guards can ever return `VERIFIED` (`CitationGuard`, `ClauseGuard` heuristic paths, `FairnessGuard`, `JurisdictionGuard`, and `ProvenanceGuard` never do — their strongest positive result is `UNVERIFIABLE`).

### Computing and resolving proof references

```python theme={null}
from qwed_legal import DeadlineGuard
from qwed_legal.diagnostics import (
    LegalDiagnosticStatus,
    compute_proof_ref,
    resolve_proof_ref,
)

claim_inputs = {
    "signing_date": "2026-01-15",
    "term": "30 business days",
    "claimed_deadline": "2026-02-27",
}
result = DeadlineGuard().verify(**claim_inputs)

diag = result.to_diagnostic(claim_inputs=claim_inputs)
assert diag.status is LegalDiagnosticStatus.VERIFIED
assert diag.proof_ref.startswith("sha256:")

# The same evidence recomputes to the same hash.
evidence = {
    "claim_inputs": diag.developer_fields["claim_inputs"],
    "trace":        diag.developer_fields["verification_trace"],
    "result":       diag.developer_fields["result"],
}
assert compute_proof_ref(evidence) == diag.proof_ref
assert resolve_proof_ref(diag.proof_ref, evidence) is True
```

Any consumer that retains the evidence dict can detect **post-issuance tampering** — trace mutations, claim alteration, or `evidence_type` flips — by recomputing the hash. `resolve_proof_ref` returns `False` on any mismatch and on any canonicalization failure (fail-closed).

### No proof without a deterministic step

`LegalDiagnosticResult.verified()` only mints a `proof_ref` when the evidence `trace` contains at least one step with `evidence_type == "DETERMINISTIC"`. Called directly over evidence without one, it raises `ValueError`. The guard adapters demote instead of raising, so you always get a diagnostic back:

* **`to_diagnostic()` on any guard result.** A result that would map to `VERIFIED` but carries no `DETERMINISTIC` trace step returns `UNVERIFIABLE` with no `proof_ref`.
* **`ContradictionGuard` adapter.** A hand-built `{"status": "consistent"}` dict, or a transported verdict whose trace was stripped, returns `UNVERIFIABLE`. A `consistent` verdict with empty or whitespace-only `claim_texts` (for example `[""]`) also returns `UNVERIFIABLE`.
* **Blank messages.** An empty or non-string `message` falls back to a status-specific default (for example `"Clauses are consistent."` or `"Clauses are contradictory."`) instead of crashing the adapter.

Real deterministic verdicts, such as a Z3-consistent result with a real claim, still mint a `proof_ref`.

<Warning>
  The RFC 8785 canonicalizer only accepts the JSON data-model subset the guards emit: `str`, `int`, `bool`, `null`, `list`, `dict`. **Floats fail closed** rather than hashing loosely — the diagnostic adapter coerces trace floats to deterministic strings before hashing. Integers outside the IEEE 754 safe range and lone surrogates in strings also raise, and non-string mapping keys are rejected instead of coerced (so `{1: "a"}` and `{"1": "a"}` no longer collide).
</Warning>

## 1. DeadlineGuard

**Status:** `DETERMINISTIC`

**Purpose:** Verify date calculations in contracts for structured, unambiguous inputs.

### The problem

LLMs frequently miscalculate deadlines:

* Confuse **business days** vs **calendar days**
* Ignore **leap years**
* Forget **jurisdiction-specific holidays**

### The solution

```python theme={null}
from qwed_legal import DeadlineGuard

guard = DeadlineGuard(country="US", state="CA")

result = guard.verify(
    signing_date="2026-01-15",
    term="30 business days",
    claimed_deadline="2026-02-14"
)

print(result.verified)           # False
print(result.computed_deadline)  # 2026-02-27
print(result.difference_days)    # 13
```

### Parameters

<ParamField path="signing_date" type="str" required>
  The date the contract was signed. Use an ISO `YYYY-MM-DD` date. Named-month dates such as `"January 15, 2026"` also work. Order-ambiguous numeric dates (`"03/04/2026"`) and partial dates (`"March 2024"`, `"Friday"`) return `UNVERIFIABLE`.
</ParamField>

<ParamField path="term" type="str" required>
  The term description (e.g., "30 days", "30 business days", "2 weeks", "3 months", "1 year").
</ParamField>

<ParamField path="claimed_deadline" type="str" required>
  The deadline claimed by the LLM. The same date rules as `signing_date` apply. If you include a timezone, include one on `signing_date` too.
</ParamField>

<ParamField path="tolerance_days" type="int" default="0">
  Allow +/- this many whole calendar days when verifying the deadline. Useful for accommodating minor rounding differences.
</ParamField>

### Response fields

| Field | Type | Description |
| - | - | - |
| `verified` | `bool` | Whether the claimed deadline matches the computed deadline. Always `False` when `is_computable` is `False`. |
| `signing_date` | `Optional[datetime]` | Parsed signing date. `None` when a date input is order-ambiguous or partial. |
| `claimed_deadline` | `Optional[datetime]` | The deadline claimed by the LLM. `None` when a date input is order-ambiguous or partial. |
| `computed_deadline` | `Optional[datetime]` | The correct deadline computed by the guard. `None` when the term is ambiguous and the guard fails closed. |
| `term_parsed` | `str` | The original term string, or `"ERROR"` when date parsing fails |
| `difference_days` | `Optional[int]` | Absolute difference in calendar days between claimed and computed. `None` when no deadline could be computed. |
| `message` | `str` | Human-readable verification message. Starts with `⚠️ UNVERIFIABLE` when the term is ambiguous. |
| `is_computable` | `bool` | `True` when the term parsed into an explicit quantity and unit. `False` when the input was ambiguous, unparseable, or the dates were invalid. |
| `verification_mode` | `str` | Always `"SYMBOLIC"` for legal verification |

### Fail-closed behavior on ambiguous terms

`DeadlineGuard` does **not** invent deadlines from vague legal language. If the term cannot be parsed into a deterministic `(quantity, unit)` pair, the guard returns a fail-closed result with `verified=False`, `is_computable=False`, and `computed_deadline=None`.

A term is treated as `UNVERIFIABLE` when any of the following apply:

* It contains **no numeric quantity** (e.g., `"forthwith"`, `"promptly after notice"`, `"within a reasonable period"`, `"as soon as practicable"`, `"without undue delay"`).
* It contains a number but **no recognized time unit** (e.g., `"30"` alone, `"15 intervals"`).
* It contains **more than one time expression** (e.g., `"30 days and 2 months"`, `"30 or 60 days"`). Compound terms are ambiguous and require human interpretation.
* It contains a **numeric token not adjacent to a time unit** (e.g., a clause reference like `"pursuant to section 4.2, within 10 days"` may still parse, but `"30 days per section 90"` fails closed because `90` is an unpaired quantity).
* The `business`/`working`/`work` qualifier is **not adjacent** to the unit (e.g., `"30 business days"` parses; `"30 days business"` does not).
* The parsed quantity exceeds an internal bound (\~100,000 units after normalization) or produces an out-of-range date. This prevents crashes and unbounded business-day loops on inputs like `"999999999 business days"`.
* The quantity is a digit run longer than 18 characters. The guard rejects it before converting it to a number.
* The term is **anchored to an event** rather than the signing date, or uses **directional wording** such as `before` or `prior to`. See [Supported term forms](#supported-term-forms) and [Event-anchored terms](#event-anchored-terms).

Recognized time units are `day`/`calendar`, `week`, `month`, and `year`, with optional `business`/`working`/`work` qualifiers for business-day arithmetic. Business/working qualifiers are only valid on `day` and `week` units (`week` counts as 5 business days) — `"6 business months"` fails closed rather than silently computing a calendar period.

```python theme={null}
from qwed_legal import DeadlineGuard

guard = DeadlineGuard()

# Ambiguous term — no numeric quantity
result = guard.verify(
    signing_date="2026-01-01",
    term="within a reasonable period",
    claimed_deadline="2026-01-31",
)

print(result.verified)          # False
print(result.is_computable)     # False
print(result.computed_deadline) # None
print(result.difference_days)   # None
print(result.message)
# ⚠️ UNVERIFIABLE: Term 'within a reasonable period' does not contain a
# provable time quantity and unit. Cannot compute a deterministic deadline...
```

<Warning>
  Always check `result.is_computable` before relying on `computed_deadline` or `difference_days`. Ambiguous legal language requires human legal interpretation and is never silently coerced into a 30-day default.
</Warning>

Date parsing failures are also fail-closed: if `signing_date` or `claimed_deadline` cannot be parsed, the guard returns `verified=False` and `is_computable=False`.

### Fail-closed behavior on ambiguous and partial dates

`DeadlineGuard` refuses to guess what a date means. It returns `UNVERIFIABLE` (`verified=False`, `is_computable=False`) for two classes of date input:

* **Order-ambiguous numeric dates.** `"03/04/2026"` is March 4 when read month-first and April 3 when read day-first. The guard no longer picks the month-first reading.
* **Partial dates.** `"March 2024"`, `"Friday"`, and `"23:00"` leave out a year, month, or day. The parser would fill the gap from the system clock, so the same input could verify today and fail tomorrow.

These dates still verify:

* ISO year-first dates: `"2026-04-03"`, `"2026/04/03"`
* Named-month dates: `"April 3, 2026"`, `"3 April 2026"`
* Numeric dates that read the same either way: `"04/04/2026"`, `"13/04/2026"`
* Complete dates with a time of day: `"2026-01-15 23:00"`

In both rejection cases, `signing_date`, `claimed_deadline`, and `computed_deadline` are `None`. The verification trace keeps the raw input strings and ends with an `AMBIGUITY_NOTED` step that names the offending fields.

```python theme={null}
from qwed_legal import DeadlineGuard

guard = DeadlineGuard()

result = guard.verify(
    signing_date="03/04/2026",
    term="30 days",
    claimed_deadline="2026-04-03",
)

# Before: "03/04/2026" was read as March 4, and the claim verified.
# After:
print(result.verified)       # False
print(result.is_computable)  # False
print(result.signing_date)   # None
print(result.message)
# ⚠️ UNVERIFIABLE: signing_date is order-ambiguous (reads differently
# month-first vs day-first). Supply an ISO year-leading date (YYYY-MM-DD)
# or an unambiguous written date.

# Fix: supply the intended date in ISO form.
result = guard.verify(
    signing_date="2026-03-04",
    term="30 days",
    claimed_deadline="2026-04-03",
)
print(result.verified)       # True
```

<Tip>
  Normalize every date to ISO `YYYY-MM-DD` before you pass it to a QWED-Legal guard. ISO dates are never order-ambiguous or partial.
</Tip>

### Supported term forms

Starting in `qwed-legal` 0.5.1, `DeadlineGuard` uses an allowlist. It computes a deadline from `signing_date` only when the term is measured forward from the signing date. Every other term returns `UNVERIFIABLE` (`verified=False`, `is_computable=False`, `computed_deadline=None`). The trace records an `UNSUPPORTED` step. This check runs before any date arithmetic, so rejected business-day terms never consult the holiday calendar.

| Term form | Examples | Result |
| - | - | - |
| Bare-relative | `"30 days"`, `"10 business days"`, `"3 months"` | Computed from `signing_date` |
| Anchored to signing or execution | `"30 days from signing"`, `"15 days after execution"` | Computed from `signing_date` |
| Directional | `"30 days before signing"`, terms using `prior to`, `preceding`, `ahead of`, or `in advance of` | `UNVERIFIABLE` |
| Anchored to another event | `"30 days after receipt of notice"`, `"30 days after closing"`, `"within 30 days of request"` | `UNVERIFIABLE` |

Directional wording wins over a signing anchor. `"30 days before signing"` measures backward, so a forward computation would certify the wrong date. Before 0.5.1, the guard computed these terms forward from signing and could return `verified=True`.

```python theme={null}
result = guard.verify(
    signing_date="2026-01-01",
    term="30 days before signing",
    claimed_deadline="2026-01-31",
)

print(result.verified)           # False
print(result.computed_deadline)  # None
print(result.message)
# ⚠️ UNVERIFIABLE: Term '30 days before signing' uses directional wording
# (e.g. 'before', 'prior to', 'ahead of') that measures the period backward
# or against a reference the signing-date computation does not apply. Supply
# a term measured forward from the signing date (e.g. '30 days',
# '30 days from signing'), or provide the anchor event's date.
```

### Event-anchored terms

A term that starts its clock at an event other than signing or execution returns `UNVERIFIABLE` instead of a deadline counted from signing. The guard has no date for the event, so any computed deadline would be wrong. This covers any event, including closing, completion, request, receipt, notice, delivery, invoice, breach, termination, and payment. A term that negates the signing date (`"not from signing"`) and names no supported replacement also fails closed. Conditional wording about acceptance, approval, or payment (`"subject to approval"`) does not count as an anchor.

```python theme={null}
result = guard.verify(
    signing_date="2026-01-01",
    term="30 days after receipt of notice",
    claimed_deadline="2026-01-31",
)

print(result.verified)           # False
print(result.computed_deadline)  # None
print(result.message)
# ⚠️ UNVERIFIABLE: Term '30 days after receipt of notice' is anchored to an
# event ('receipt'), not to the signing date. The real deadline is unknowable
# from these inputs — supply the anchor event's date.
```

To verify a deadline that runs forward from another event:

1. Confirm the event's date and that the period runs forward from it.
2. Pass the event's date as `signing_date`.
3. Rewrite the term to a bare-relative form, such as `term="30 days"`.

Changing the date alone is not enough. The original event-anchored term still returns `UNVERIFIABLE`.

```python theme={null}
# Contract: "Payment is due 30 days after closing." Closing occurred on 2026-03-02.
result = guard.verify(
    signing_date="2026-03-02",   # confirmed closing date
    term="30 days",              # rewritten from "30 days after closing"
    claimed_deadline="2026-04-01",
)
print(result.verified)  # True
```

Terms measured backward (`"before"`, `"prior to"`) have no supported rewrite. Verify them outside `DeadlineGuard`.

<Warning>
  The result reports the date you passed as `signing_date` — it cannot tell an event date from a real signing. If you store or share results, keep the contract's actual signing date separately so the record does not misstate when the contract was signed.
</Warning>

### Date-granularity comparison

`DeadlineGuard` compares the claimed and computed deadlines by calendar date. A time of day on the claim does not count as a mismatch. `tolerance_days` stays in whole calendar days.

* When both dates carry a timezone, the guard converts the claimed deadline to the computed deadline's timezone before it compares calendar dates. The same moment written in a different offset is not a mismatch.
* When one date carries a timezone and the other doesn't, the guard returns `UNVERIFIABLE` with the message `Mixed timezone inputs`. Supply both dates with a timezone, or both without.

```python theme={null}
result = guard.verify(
    signing_date="2026-01-01",
    term="30 days",
    claimed_deadline="2026-01-31 17:00",
)
print(result.verified)  # True: same calendar date as the computed deadline
```

### Features

| Feature | Description |
| - | - |
| **Business vs Calendar** | Automatically detects "business days" vs "days" |
| **Holiday Support** | 200+ countries via `python-holidays` |
| **Leap Years** | Handles Feb 29 correctly |
| **Natural Language** | Parses "2 weeks", "3 months", "1 year" |
| **Fail-closed parsing** | Rejects ambiguous legal language ("reasonable period", "promptly") instead of inventing a deadline |

### Fail-closed behavior on ambiguous terms

`DeadlineGuard` only computes a deadline when the term contains both an explicit numeric quantity **and** a recognized time unit (`day`, `business day`, `calendar day`, `week`, `month`, `year`). When either is missing, the guard fails closed: it returns `verified=False`, `is_computable=False`, and a `computed_deadline` of `None` instead of guessing a default.

This protects against silent acceptance of subjective legal language such as `"within a reasonable period"`, `"promptly"`, `"as soon as practicable"`, `"without undue delay"`, or `"forthwith"`. Resolving these terms requires human legal interpretation.

```python theme={null}
from qwed_legal import DeadlineGuard

guard = DeadlineGuard(country="US")

result = guard.verify(
    signing_date="2026-01-01",
    term="within a reasonable period",
    claimed_deadline="2026-01-31",
)

print(result.verified)           # False
print(result.is_computable)      # False
print(result.computed_deadline)  # None
print(result.difference_days)    # None
print(result.message)
# ⚠️ UNVERIFIABLE: Term 'within a reasonable period' does not contain a
# provable time quantity and unit. Cannot compute a deterministic deadline.
# Ambiguous legal language (e.g., 'reasonable period', 'promptly') requires
# human legal interpretation.
```

Always check `is_computable` before relying on `computed_deadline` or `difference_days`. When `is_computable` is `False`, route the contract clause to a human reviewer.

### Calculate business days between dates

```python theme={null}
guard = DeadlineGuard(country="US")

business_days = guard.calculate_business_days_between(
    start_date="2026-01-15",
    end_date="2026-02-14"
)

print(business_days)  # Number of business days excluding weekends and holidays
```

`calculate_business_days_between()` raises `ValueError` when either date is order-ambiguous (`"03/04/2026"`) or partial (`"March 2024"`). Pass ISO `YYYY-MM-DD` dates.

***

## 2. LiabilityGuard

**Status:** `DETERMINISTIC`

**Purpose:** Verify liability cap and indemnity calculations for supported numeric inputs.

### The problem

LLMs get percentage math wrong:

* "200% of $5M = $15M" ❌ (Should be \$10M)
* Float precision errors on large amounts
* Tiered liability miscalculations

### Constructor parameters

<ParamField path="tolerance_percent" type="float" default="0.01">
  Tolerance for floating-point comparison as a percentage. For example, `0.01` means 0.01% tolerance. Adjust for stricter or more lenient verification.
</ParamField>

### The solution

```python theme={null}
from qwed_legal import LiabilityGuard

guard = LiabilityGuard()

result = guard.verify_cap(
    contract_value=5_000_000,
    cap_percentage=200,
    claimed_cap=15_000_000
)

print(result.verified)      # False
print(result.computed_cap)  # 10,000,000
print(result.difference)    # 5,000,000
```

### `verify_cap` parameters

<ParamField path="contract_value" type="float" required>
  Total value of the contract.
</ParamField>

<ParamField path="cap_percentage" type="float" required>
  Liability cap as a percentage (e.g., `200` for 200%).
</ParamField>

<ParamField path="claimed_cap" type="float" required>
  The cap amount claimed by the LLM.
</ParamField>

### Response fields

| Field | Type | Description |
| - | - | - |
| `verified` | `bool` | Whether the claimed cap matches the computed cap. Always `False` on `UNVERIFIABLE` inputs. |
| `contract_value` | `Optional[Decimal]` | The contract value used. `None` when the input was non-finite, out of range, or malformed. |
| `cap_percentage` | `Optional[Decimal]` | The percentage used. `None` on unverifiable inputs. |
| `claimed_cap` | `Optional[Decimal]` | The cap claimed by the LLM. `None` on unverifiable inputs. |
| `computed_cap` | `Optional[Decimal]` | The correct cap computed by the guard. `None` on unverifiable inputs. |
| `difference` | `Optional[Decimal]` | Absolute difference between claimed and computed. `None` on unverifiable inputs. |
| `message` | `str` | Human-readable verification message. Starts with `⚠️ UNVERIFIABLE` when inputs cannot be evaluated. |

### Fail-closed behavior on non-finite and out-of-range inputs

`verify_cap`, `verify_indemnity_limit`, and `verify_tiered_liability` reject inputs that cannot be safely quantized. All numeric fields on `LiabilityResult` and `TieredLiabilityResult` are now `Optional[Decimal]` — the guard returns `None` for those fields instead of fabricating zeros when it cannot compute. (Shipped in `0.5.0` — on `0.4.0` and earlier these inputs raise instead of fail-closed.)

Inputs that fail closed as `UNVERIFIABLE`:

* **Non-finite values.** `math.inf`, `-math.inf`, and `math.nan` for any numeric input.
* **Finite but extreme magnitudes** that overflow `Decimal` arithmetic (e.g., `1e308`).
* **Tiered inputs** where any tier's `base` or `percentage` is non-finite. The failure message names the offending tier keys.
* **Empty tier list.** `verify_tiered_liability(tiers=[], ...)` returns `UNVERIFIABLE` with `total_computed=None` and no `proof_ref`, whether `claimed_total` is `0` or any other value. An empty list has no operands to sum, so it cannot prove any total.

```python theme={null}
import math
from qwed_legal import LiabilityGuard

guard = LiabilityGuard()

result = guard.verify_cap(
    contract_value=math.inf,
    cap_percentage=200,
    claimed_cap=15_000_000,
)

print(result.verified)      # False
print(result.computed_cap)  # None
print(result.difference)    # None
print(result.message)       # "⚠️ UNVERIFIABLE: ..."

if not result.verified:
    # Fail-closed gate: reject this input / route to human review.
    # ValueError (catchable per-request), not SystemExit (kills the host).
    # Never reach computed_cap / difference here (they are None).
    raise ValueError(result.message)
```

<Warning>
  Callers that previously assumed `computed_cap`, `difference`, or `total_computed` were always populated must handle `None`. Non-finite inputs previously raised `decimal.InvalidOperation`; they now fail closed with populated echo of the original inputs where possible and `None` for computed fields.
</Warning>

### Additional methods

```python theme={null}
# Tiered liability
result = guard.verify_tiered_liability(
    tiers=[
        {"base": 1_000_000, "percentage": 100},
        {"base": 500_000, "percentage": 50},
    ],
    claimed_total=1_250_000  # ✅ Correct: 1M + 250K
)

# Indemnity limit (3x annual fee)
result = guard.verify_indemnity_limit(
    annual_fee=100_000,
    multiplier=3,
    claimed_limit=300_000  # ✅ Correct
)
```

***

## 3. ClauseGuard

**Status:** `PARTIAL / HEURISTIC`

**Purpose:** Detect a limited set of contradictory clauses using text heuristics, with optional Z3-based satisfiability checks. A "consistent" result is **not** a proof of full contractual consistency.

### The problem

LLMs miss logical contradictions:

* "Seller may terminate with 30 days notice"
* "Neither party may terminate before 90 days"

These clauses **conflict** for days 30-90!

### The solution

The primary `check_consistency()` method uses text heuristics to detect conflicts. For formal logic verification, use `verify_using_z3()`.

```python theme={null}
from qwed_legal import ClauseGuard

guard = ClauseGuard()

result = guard.check_consistency([
    "Seller may terminate with 30 days notice",
    "Neither party may terminate before 90 days",
    "Seller may terminate immediately upon breach"
])

print(result.consistent)  # False
print(result.conflicts)
# [(0, 1, "Termination notice (30 days) conflicts with minimum term (90 days)")]
```

### Detection types

| Conflict Type | Description |
| - | - |
| **Termination** | Notice period vs minimum term |
| **Permission/Prohibition** | "May" vs "May not" |
| **Exclusivity** | Multiple exclusive rights |

### Day-count extraction

The internal `_extract_days` helper picks the day-expression closest to the context word (`notice`, `before`, etc.). It first tries directional patterns (e.g., `"within 30 days after signing"`), and if none match, applies a **proximity fallback**: any day-expression within 40 characters of the context word. This resolves clauses that would previously return `None`:

* `"give notice within 10 days of discovery"` now extracts `10` (the older directional-only extractor missed it).
* Multiple linked durations with different values (e.g., `"breach notice within 10 days; termination notice within 120 days"`) remain unresolved on purpose — the guard refuses to pick between them rather than silently choosing document order.

Broader extraction is coverage, not proof — ClauseGuard remains a `PARTIAL / HEURISTIC` guard.

### Input validation

`check_consistency()` fails closed on inputs it cannot analyze. All of these cases return `consistent=False`, `status="invalid_input"`, and a message that begins with `INVALID INPUT (LIMITED COVERAGE)`:

* Empty list `[]`
* Non-list values (dict, string, `None`)
* Lists containing non-string elements
* Lists containing empty or whitespace-only strings
* Lists with more than 200 clauses
* Lists containing a clause longer than 4,096 characters

A **single-clause** list returns `consistent=False` and `status="insufficient_input"`: one clause has no pair to compare, so it is not evidence of consistency. Note that this branch is only reached for a non-empty list — an empty list is caught above as `invalid_input` ("No clauses were provided"), not `insufficient_input`.

Downstream consumers must not treat an `invalid_input` or `insufficient_input` result as verified consistency.

When a clause contains a day count with more than 18 digits, the guard can't read that value. The result is `status="heuristic_pass_limited"` instead of `"consistent"`.

A `contradiction` result lists at most 100 conflict pairs. When the guard stops at that cap, the message ends with `Conflict list truncated at 100 pairs — additional conflicts may exist.` Treat a truncated list as incomplete.

<Warning>
  The QWED Legal GitHub Action gates every block on `to_diagnostic().status == VERIFIED`. Only deterministic deadline and liability exact matches keep the action green. Heuristic `check_consistency()` results never pass the action, including plain `"consistent"`. Contradictions, `invalid_input`, `insufficient_input`, and `heuristic_pass_limited` results also fail. Citation runs never pass either; to branch on `results.citation.valid` from the `results` output, mark the QWED Legal step `continue-on-error: true` and gate on `steps.<id>.outputs.results` rather than the exit code — a failed step both skips the later steps *and* fails the job, so `if: always()` alone lets a later step read the output but cannot make the run pass. Unknown modes and runs that execute zero checks because inputs are missing fail with `verified=false` and a non-zero exit. Z3 clause proofs attest only through the Python API (`verify_using_z3()`), not through the Action.
</Warning>

```python theme={null}
result = guard.check_consistency([])

if not result.consistent:
    # Fail-closed gate: reject this input / route to human review.
    # ValueError (catchable per-request), not SystemExit (kills the host).
    raise ValueError(result.message)
```

### Z3-based verification

When you need to define precise logical constraints, `verify_using_z3()` only accepts explicit Z3 `BoolRef` expressions — it does **not** parse free-form text. You must model the legal meaning yourself.

```python theme={null}
from z3 import Bool, Implies, Not
from qwed_legal import ClauseGuard

guard = ClauseGuard()

can_terminate_early = Bool("can_terminate_early")
min_term_satisfied = Bool("min_term_satisfied")

result = guard.verify_using_z3([
    Implies(can_terminate_early, Not(min_term_satisfied)),
    can_terminate_early,
    min_term_satisfied,
])

print(result.consistent)  # False - constraints are unsatisfiable
print(result.message)     # "CONTRADICTION: Provided Z3 constraints are unsatisfiable..."
```

#### Fail-closed behavior

`verify_using_z3()` is fail-closed: it returns `consistent=False` for any input it cannot prove satisfiable. The following inputs are rejected as `UNVERIFIABLE` rather than silently passing:

| Input | Result | Message prefix |
| - | - | - |
| Empty list `[]` | `consistent=False` | `UNVERIFIABLE: verify_using_z3 requires explicit Z3 constraint expressions...` |
| Non-`BoolRef` values (e.g. strings, ints) | `consistent=False` | `UNVERIFIABLE: verify_using_z3 only accepts explicit Z3 Boolean expressions...` |
| Z3 returns `unknown` | `consistent=False` | `UNVERIFIABLE: Z3 returned unknown for the provided constraints.` |
| Satisfiable (`sat`) | `consistent=True` | `VERIFIED: Provided Z3 constraints are satisfiable.` |
| Unsatisfiable (`unsat`) | `consistent=False` | `CONTRADICTION: Provided Z3 constraints are unsatisfiable...` |

<Warning>
  Passing raw strings or other non-Z3 values is rejected. The guard reports the 1-based position of each invalid constraint so callers can identify the offending entries.
</Warning>

***

## 4. CitationGuard

**Status:** `PARTIAL / HEURISTIC`

**Purpose:** Validate that legal citations match a supported format. CitationGuard does **not** prove that a cited authority exists or is controlling — it only checks structural shape against supported reporters.

### The problem

The **Mata v. Avianca** scandal: Lawyers used ChatGPT, which cited **6 fake court cases**. They were fined \$5,000 and sanctioned.

### The solution

```python theme={null}
from qwed_legal import CitationGuard

guard = CitationGuard()

# Valid format
result = guard.verify("Brown v. Board of Education, 347 U.S. 483 (1954)")
print(result.format_valid)  # True
print(result.status)        # "unverifiable_authority" - format ok, authority unknown
print(result.verified)      # False - authority is never proven by format
print(result.parsed_components)
# {'volume': 347, 'reporter': 'U.S.', 'page': '483'}

# Invalid format (fake reporter)
result = guard.verify("Smith v. Jones, 999 FAKE 123 (2020)")
print(result.format_valid)  # False
print(result.status)        # "format_invalid"
print(result.issues)        # ["Unknown reporter"]
```

<Warning>
  `CitationGuard` checks **format only**. `result.verified` is **always** `False`, and a format-valid citation has `status="unverifiable_authority"`. A well-formatted citation can still refer to a case that does not exist — confirming authority requires an external legal database, which this guard does not have. Use `format_valid` to check shape; never treat it as proof of authority.
</Warning>

### Supported citation patterns

| Pattern | Format | Example |
| - | - | - |
| **US Supreme Court** | `volume U.S. page` | `347 U.S. 483` |
| **US Federal** | `volume F./F.2d/F.3d page` | `500 F.3d 120` |
| **UK Neutral** | `[year] court number` | `[2023] UKSC 10` |
| **India AIR** | `AIR year court page` | `AIR 2020 SC 100` |

The US citation patterns are written to avoid catastrophic regex backtracking, so adversarial input such as long runs of digits or whitespace no longer slows `verify()` down.

### Batch verification

```python theme={null}
result = guard.verify_batch([
    "Brown v. Board, 347 U.S. 483 (1954)",
    "Fake v. Case, 999 X.Y.Z. 123",
])

print(result.total)    # 2
print(result.valid)    # 1
print(result.invalid)  # 1
```

### Statute citations

```python theme={null}
result = guard.check_statute_citation("42 U.S.C. § 1983")
print(result.format_valid)  # True (format only — not proof the statute exists or applies)
print(result.parsed_components)
# {'title': 42, 'code': 'U.S.C.', 'section': '1983'}
```

The `US_CODE` pattern accepts an **optional** section symbol. Both of the following are format-valid — the `§` is legitimate drafting when present and legitimate drafting when omitted:

```python theme={null}
guard.check_statute_citation("12 U.S.C. § 2605")  # format_valid=True
guard.check_statute_citation("12 U.S.C. 2605")    # format_valid=True
```

The section identifier must still start with a digit. Prose like `"12 U.S.C. provides that..."` is rejected, as are non-`U.S.C.` codes.

***

## 5. JurisdictionGuard

**Status:** `PARTIAL / HEURISTIC`

**Purpose:** Apply structured checks around governing law and forum selection clauses for modeled combinations. Results should not be treated as authoritative legal opinions on choice-of-law conflicts.

### The problem

LLMs miss jurisdiction conflicts:

* Governing law in one country, forum in another
* Missing CISG applicability warnings
* Cross-border legal system mismatches

### The solution

```python theme={null}
from qwed_legal import JurisdictionGuard

guard = JurisdictionGuard()

result = guard.verify_choice_of_law(
    parties_countries=["US", "UK"],
    governing_law="Delaware",
    forum="London",
    contract_type="services",
)

print(result.verified)   # False - mismatch detected
print(result.conflicts)  # ["Governing law 'Delaware' (US state) but forum 'London' is non-US..."]
```

### Parameters

<ParamField path="parties_countries" type="list[str]" required>
  List of ISO country codes for contract parties (e.g., `["US", "UK"]`).
</ParamField>

<ParamField path="governing_law" type="str" required>
  The stated governing law — can be a country code or US state name/abbreviation (e.g., `"Delaware"`, `"DE"`, `"UK"`).
</ParamField>

<ParamField path="forum" type="str">
  The stated forum or venue for dispute resolution.
</ParamField>

<ParamField path="jurisdiction_type" type="JurisdictionType" default="JurisdictionType.EXCLUSIVE">
  Type of jurisdiction clause. Accepts `JurisdictionType.EXCLUSIVE`, `JurisdictionType.NON_EXCLUSIVE`, or `JurisdictionType.HYBRID`.
</ParamField>

<ParamField path="contract_type" type="str | None" default="None">
  Subject matter of the contract, used for the CISG check (for example `"sale of goods"` or `"services"`). Leaving it unset on a cross-border contract produces a partial-coverage warning. See [CISG detection](#cisg-detection).
</ParamField>

### Features

| Feature | Description |
| - | - |
| **Choice of Law** | Validates governing law makes sense for parties |
| **Forum Selection** | Checks forum vs governing law alignment |
| **CISG Detection** | Warns about international sale of goods conventions |
| **Convention Check** | Verifies Hague, NY Convention applicability |
| **Legal System Mismatch** | Detects cross-border Common Law vs Civil Law conflicts |

### CISG detection

For cross-border parties, `verify_choice_of_law()` classifies `contract_type` three ways before deciding whether to raise a CISG warning:

| Classification | Example `contract_type` values | Result |
| - | - | - |
| `goods` | `"sale of goods"`, `"sale-of-goods"`, `"international_sale_of_goods"`, `"goods"`, `"supply of goods"`, `"purchase of goods"` | CISG warning: "International sale of goods may be subject to CISG unless expressly excluded." |
| `declared_non_goods` | `"services"`, `"consulting"`, `"license"`, `"employment"`, `"nda"`, `"lease"`, `"loan"`, `"construction"`, `"settlement"` | No CISG warning |
| `unclassified` | `None`, `""`, whitespace, bare `"sale"`, `"msa"`, `"sow"`, any unrecognized value | Partial-coverage warning. CISG applicability is not evaluated, so the result is not verified. |

Matching ignores case, hyphens, underscores, and extra whitespace. A bare `"sale"` is `unclassified` because it does not declare what is sold, and CISG excludes some sales (such as ships, electricity, and shares). Agreement-form labels like `"msa"` and `"sow"` are `unclassified` because they describe the form of the contract, not its subject matter.

The "Consider CISG applicability" suggestion on Common Law vs Civil Law warnings also appears only for `goods` contracts. Both `contract_type` and `contract_classification` are recorded in the trace inputs.

```python theme={null}
from qwed_legal import JurisdictionGuard

guard = JurisdictionGuard()

# Goods: CISG warning
result = guard.verify_choice_of_law(
    parties_countries=["US", "DE"],
    governing_law="Delaware",
    contract_type="sale-of-goods",
)
print(result.warnings)
# [..., "International sale of goods may be subject to CISG unless expressly excluded.", ...]

# Declared services: no CISG warning
result = guard.verify_choice_of_law(
    parties_countries=["US", "DE"],
    governing_law="Delaware",
    contract_type="services",
)

# Missing classification: partial-coverage warning, not verified
result = guard.verify_choice_of_law(
    parties_countries=["US", "DE"],
    governing_law="Delaware",
)
print(result.verified)  # False
# "Contract classification absent or unrecognized (None): CISG
#  convention-applicability was not evaluated, so cross-border coverage
#  is partial for this contract."
```

<Note>
  Previously, any US and non-US party pair triggered the CISG warning regardless of `contract_type`, and an unrecognized `contract_type` skipped the CISG check silently. Pass `contract_type` explicitly to get a definitive CISG result.
</Note>

#### TypeScript (npm)

The `@qwed-ai/legal` npm wrapper (0.5.x) accepts the contract type as an optional fourth argument to `JurisdictionVerifier.verifyChoiceOfLaw(partiesCountries, governingLaw, forum?, contractType?)`. The wrapper passes it to the Python guard as `contract_type`, so the same three-way classification applies. The wrapper requires Python 3.10+ with `qwed-legal` installed.

```typescript theme={null}
import { JurisdictionVerifier } from '@qwed-ai/legal';

const jurisdiction = new JurisdictionVerifier();

try {
  // Pass undefined for forum to set only the contract type
  const result = await jurisdiction.verifyChoiceOfLaw(
      ['US', 'DE'],
      'Delaware',
      undefined,
      'sale-of-goods'
  );

  console.log(result.contractType);           // "sale-of-goods"
  console.log(result.contractClassification); // "goods"
  console.log(result.warnings);               // [..., "International sale of goods may be subject to CISG unless expressly excluded.", ...]
} catch (err) {
  console.error('Choice-of-law verification failed:', err);
}
```

`JurisdictionResult` adds two optional fields:

| Field | Description |
| - | - |
| `contractType` | The contract type you passed, echoed back. `null` when omitted. |
| `contractClassification` | The guard's classification from the verification trace: `goods`, `declared_non_goods`, or `unclassified`. `null` when the trace has no classification. |

Use `contractClassification` to tell a `goods` warning apart from an `unclassified` partial-coverage warning without parsing messages. If you omit `contractType`, behavior is unchanged: the result has a partial-coverage warning and is not verified.

### Verify forum selection

Use `verify_forum_selection` to validate a forum independently, with optional contract value threshold checks for US federal court diversity jurisdiction:

```python theme={null}
result = guard.verify_forum_selection(
    forum="Delaware",
    contract_value=50_000,
    parties_countries=["US", "DE"]
)

print(result.verified)   # True
print(result.warnings)   # ["Contract value $50,000 may not meet diversity jurisdiction threshold..."]
```

### Convention check

```python theme={null}
result = guard.check_convention_applicability(
    parties_countries=["US", "DE"],
    convention="CISG"
)
print(result.verified)  # True - both are CISG members
```

A blank party entry, such as `[""]` or `["US", ""]`, carries no party data. `check_convention_applicability()` returns `verified=False` with an `UNVERIFIABLE` message for it. A genuine negative, such as `["XX"]`, still returns a deterministic "does not apply" result.

***

## 6. StatuteOfLimitationsGuard

**Status:** `MIXED`

**Purpose:** Compute claim limitation periods for supported jurisdictions and claim types using rule tables. The limitation-period lookup is `PARSED`; the date arithmetic over it (expiration, days remaining) is `DETERMINISTIC`. Coverage is limited to the modeled jurisdictions and claim types listed below.

### The problem

LLMs don't track jurisdiction-specific limitation periods:

* California breach of contract: 4 years
* New York breach of contract: 6 years
* Different periods for negligence, fraud, etc.

### The solution

```python theme={null}
from qwed_legal import StatuteOfLimitationsGuard

guard = StatuteOfLimitationsGuard()

result = guard.verify(
    claim_type="breach_of_contract",
    jurisdiction="California",
    incident_date="2020-01-15",
    filing_date="2026-06-01"
)

print(result.verified)          # False - claim not supplied, so verified is False by contract
print(result.expiration_date)   # 2024-01-15
print(result.days_remaining)    # -867 (negative = expired)

# On 0.5.0+, result.status == "COMPUTED_ONLY" here
# (period computed, no claim to verify against). On 0.4.0 and earlier
# there is no status field — gate on verified instead.
```

### Result status taxonomy

> `StatuteResult.status` and the fail-closed behaviors below track `qwed-legal` 0.5.0+. Against `0.4.0` and earlier, `status` is absent — gate on `verified` instead.

`StatuteResult.status` disambiguates the four outcomes previously conflated under `verified`:

| `status` | Meaning | `verified` |
| - | - | - |
| `CLAIM_VERIFIED` | Caller supplied `claimed_within_period` and it matches the computed result | `True` |
| `CLAIM_INCORRECT` | Caller supplied `claimed_within_period` and it contradicts the computed result | `False` |
| `COMPUTED_ONLY` | Period and expiration were computed; no `claimed_within_period` was supplied | `False` (by contract — no claim to verify) |
| `UNVERIFIABLE` | Input class rejection (unknown jurisdiction/claim type, unparseable, order-ambiguous, or partial dates, impossible timeline) | `False` |

`verified` is reserved for claim comparison. An expired-but-correctly-evaluated claim is `COMPUTED_ONLY` — it is no longer indistinguishable from an unverifiable result. Branch on `status` when you need to distinguish "we computed the answer and you did not ask us to compare" from "we could not compute the answer".

### Impossible timelines fail closed

A filing date that precedes the incident date is a factually impossible timeline. The guard rejects it as `UNVERIFIABLE` before the period computation runs — a time-travel claim can no longer be recorded as within-period.

* Same-day filing (date-only inputs, both parse to midnight) is still allowed.
* When both inputs carry a time-of-day, the guard compares full timestamps: a 09:00 filing against a 15:00 incident on the same calendar date is impossible and fails closed.

```python theme={null}
result = guard.verify(
    claim_type="breach_of_contract",
    jurisdiction="California",
    incident_date="2020-06-01",
    filing_date="2019-01-15",  # before the incident
)

print(result.status)     # "UNVERIFIABLE"
print(result.verified)   # False
print(result.message)    # "⚠️ UNVERIFIABLE: Filing timestamp ... precedes incident timestamp ..."

if not result.verified:
    # Fail-closed gate: reject this input / route to human review.
    # ValueError (catchable per-request), not SystemExit (kills the host).
    raise ValueError(result.message)
```

### Fail-closed behavior

`StatuteOfLimitationsGuard` is **fail-closed**: it never fabricates a limitation period for jurisdictions or claim types that are not in its rule tables.

* **Exact match only.** Jurisdiction lookup uses exact string equality (case-insensitive, trimmed). Partial matches such as `"CALIF"` for `"CALIFORNIA"` or `"NEW"` for `"NEW YORK"` are rejected.
* **Unknown jurisdiction → unverifiable.** If the jurisdiction is not in the supported list, `verify()` returns `verified=False` with `jurisdiction_matched=False`, all date and period fields set to `None`, and a message listing the supported jurisdictions.
* **Unknown claim type → unverifiable.** If the jurisdiction is supported but the claim type is not modeled for it, `verify()` returns `verified=False` with `claim_type_matched=False`, and a message listing the supported claim types for that jurisdiction.
* **Ambiguous or partial dates → unverifiable.** If `incident_date` or `filing_date` is an order-ambiguous numeric date (`"03/04/2026"`) or a partial date that the parser would complete from the system clock (`"March 2024"`, `"Friday"`, `"23:00"`), `verify()` returns `status="UNVERIFIABLE"`. This applies in computation-only mode too, when you don't pass `claimed_within_period`. `incident_date`, `filing_date`, `expiration_date`, and `days_remaining` are `None`. The trace keeps the raw strings and records an `AMBIGUITY_NOTED` step. ISO year-first and named-month dates still verify.

```python theme={null}
result = guard.verify(
    claim_type="breach_of_contract",
    jurisdiction="California",
    incident_date="03/04/2022",
    filing_date="2026-01-15",
)

# Before: read as March 4, 2022, and returned COMPUTED_ONLY.
# After:
print(result.status)         # "UNVERIFIABLE"
print(result.incident_date)  # None
print(result.message)
# ⚠️ UNVERIFIABLE: incident_date is order-ambiguous (reads differently
# month-first vs day-first). Supply an ISO year-leading date (YYYY-MM-DD)
# or an unambiguous written date.
```

```python theme={null}
result = guard.verify(
    claim_type="breach_of_contract",
    jurisdiction="Mars",
    incident_date="2020-01-15",
    filing_date="2026-06-01"
)

print(result.verified)              # False
print(result.jurisdiction_matched)  # False
print(result.limitation_period_years)  # None
print(result.message)
# ⚠️ UNVERIFIABLE: Jurisdiction 'Mars' is not in the supported jurisdiction list.
# Cannot determine applicable statute of limitations. Supported: AUSTRALIA, CALIFORNIA, ...
```

### Parameters

<ParamField path="claim_type" type="str" required>
  Type of legal claim (e.g., `"breach_of_contract"`, `"negligence"`, `"fraud"`). Must exactly match one of the supported claim types for the given jurisdiction; unknown values produce an unverifiable result.
</ParamField>

<ParamField path="jurisdiction" type="str" required>
  State or country name (e.g., `"California"`, `"New York"`, `"UK"`). Matched case-insensitively against the supported jurisdictions list — partial or substring matches are not accepted.
</ParamField>

<ParamField path="incident_date" type="str" required>
  Date the incident occurred. Use ISO `YYYY-MM-DD`. Order-ambiguous and partial dates return `UNVERIFIABLE`.
</ParamField>

<ParamField path="filing_date" type="str" required>
  Date the claim was or will be filed. Use ISO `YYYY-MM-DD`. Order-ambiguous and partial dates return `UNVERIFIABLE`.
</ParamField>

<ParamField path="claimed_within_period" type="bool">
  Optional LLM claim to verify. When provided, the guard checks whether the LLM's assertion (within/outside period) matches the computed result.
</ParamField>

### StatuteResult fields

<ResponseField name="verified" type="bool">
  `True` only when `claimed_within_period` was supplied and matches the computed result (`status="CLAIM_VERIFIED"`). `False` in all other cases — including `COMPUTED_ONLY`, where the period was computed but there was no claim to verify against. Always branch on `status` when you need the underlying reason.
</ResponseField>

<ResponseField name="status" type="str">
  One of `CLAIM_VERIFIED`, `CLAIM_INCORRECT`, `COMPUTED_ONLY`, or `UNVERIFIABLE`. See the status taxonomy above.
</ResponseField>

<ResponseField name="claim_type" type="str">
  The claim type passed in (echoed back).
</ResponseField>

<ResponseField name="jurisdiction" type="str">
  The jurisdiction passed in (echoed back).
</ResponseField>

<ResponseField name="incident_date" type="Optional[datetime]">
  Parsed incident date, or `None` if date parsing failed or a date input was order-ambiguous or partial.
</ResponseField>

<ResponseField name="filing_date" type="Optional[datetime]">
  Parsed filing date, or `None` if date parsing failed or a date input was order-ambiguous or partial.
</ResponseField>

<ResponseField name="limitation_period_years" type="Optional[float]">
  Limitation period applied, or `None` if the jurisdiction or claim type is unknown.
</ResponseField>

<ResponseField name="expiration_date" type="Optional[datetime]">
  Computed expiration date, or `None` if no limitation period could be determined.
</ResponseField>

<ResponseField name="days_remaining" type="Optional[int]">
  Days between filing date and expiration (negative if expired), or `None` if no limitation period could be determined.
</ResponseField>

<ResponseField name="message" type="str">
  Human-readable result. Unverifiable results are prefixed with `⚠️ UNVERIFIABLE:` and list the supported jurisdictions or claim types.
</ResponseField>

<ResponseField name="jurisdiction_matched" type="bool" default="True">
  `False` when the jurisdiction is not in the supported list.
</ResponseField>

<ResponseField name="claim_type_matched" type="bool" default="True">
  `False` when the claim type is not modeled for the given jurisdiction.
</ResponseField>

### Supported jurisdictions

12 jurisdictions are supported with periods for 10 claim types.

| Jurisdiction | Breach of Contract | Negligence | Fraud |
| - | - | - | - |
| California | 4 years | 2 years | 3 years |
| New York | 6 years | 3 years | 6 years |
| Texas | 4 years | 2 years | 4 years |
| Delaware | 3 years | 2 years | 3 years |
| Florida | 5 years | 4 years | 4 years |
| Illinois | 5 years | 2 years | 5 years |
| UK/England | 6 years | 6 years | 6 years |
| Germany | 3 years | 3 years | 10 years |
| France | 5 years | 5 years | 5 years |
| Australia | 6 years | 6 years | 6 years |
| India | 3 years | 3 years | 3 years |
| Canada | 2 years | 2 years | 6 years |

### Supported claim types

`breach_of_contract`, `breach_of_warranty`, `negligence`, `professional_malpractice`, `fraud`, `personal_injury`, `property_damage`, `employment`, `product_liability`, `defamation`

### Fail-closed on unknown jurisdictions and claim types

`StatuteOfLimitationsGuard` only computes limitation periods for the jurisdictions and claim types it has explicit rules for. Anything outside that table fails closed instead of returning a fabricated period.

* **Exact jurisdiction match only.** Inputs are uppercased and trimmed before lookup. Substring matches like `"CALIF"` no longer resolve to `"CALIFORNIA"`, and a misspelled or unsupported jurisdiction never falls back to a generic default rule table.
* **Exact claim type match only.** Claim types are lowercased with spaces converted to underscores before lookup. Unknown claim types no longer silently default to a 3-year period.
* **`UNVERIFIABLE` result.** When either lookup fails, `verify()` returns a `StatuteResult` with `verified=False`, all date and period fields set to `None`, and a `message` that begins with `⚠️ UNVERIFIABLE` and lists the supported values.
* **New flags.** `StatuteResult` exposes `jurisdiction_matched: bool` and `claim_type_matched: bool` so callers can distinguish "claim is time-barred" from "we cannot determine the limit".

```python theme={null}
result = guard.verify(
    claim_type="breach_of_contract",
    jurisdiction="Atlantis",
    incident_date="2023-01-15",
    filing_date="2026-06-01",
)

print(result.verified)              # False
print(result.jurisdiction_matched)  # False
print(result.claim_type_matched)    # False
print(result.limitation_period_years)  # None
print(result.expiration_date)       # None
print(result.message)
# ⚠️ UNVERIFIABLE: Jurisdiction 'Atlantis' is not in the supported
# jurisdiction list. Cannot determine applicable statute of limitations.
# Supported: AUSTRALIA, CALIFORNIA, CANADA, DELAWARE, FLORIDA, FRANCE,
# GERMANY, ILLINOIS, INDIA, NEW YORK, TEXAS, UK.
```

```python theme={null}
result = guard.verify(
    claim_type="cybersquatting",
    jurisdiction="California",
    incident_date="2023-01-15",
    filing_date="2026-06-01",
)

print(result.jurisdiction_matched)  # True
print(result.claim_type_matched)    # False
# Message lists the supported claim types for California.
```

<Warning>
  This is a breaking change. `get_limitation_period()` now returns `Optional[float]` and `StatuteResult` date and period fields are `Optional`. Callers that previously assumed a numeric result must handle `None` and treat unverifiable inputs as a hard failure rather than a passing check. See the [changelog entry](/changelog-archive#qwed-legal-—-statuteoflimitationsguard-fail-closed-on-unknown-jurisdictions-and-claim-types) for migration notes.
</Warning>

### Get limitation period

Look up the limitation period for a specific claim type and jurisdiction without performing a full verification. Returns `None` when the jurisdiction or claim type is not supported:

```python theme={null}
years = guard.get_limitation_period("fraud", "Germany")
print(years)  # 10.0

unknown = guard.get_limitation_period("fraud", "Atlantis")
print(unknown)  # None

unsupported_claim = guard.get_limitation_period("cybersquatting", "California")
print(unsupported_claim)  # None
```

### Compare jurisdictions

`compare_jurisdictions()` returns `Dict[str, Optional[float]]`. Unsupported jurisdictions map to `None` so you can surface them in the UI rather than mixing them with real periods:

```python theme={null}
comparison = guard.compare_jurisdictions(
    "breach_of_contract",
    ["California", "New York", "Delaware", "Atlantis"]
)
# {'California': 4.0, 'New York': 6.0, 'Delaware': 3.0, 'Atlantis': None}
```

Unsupported jurisdictions map to `None` rather than a default value.

***

## 7. IRACGuard

**Status:** `PARTIAL / HEURISTIC`

**Purpose:** Check that legal reasoning follows the IRAC framework (Issue, Rule, Application, Conclusion). IRACGuard verifies structure and surface-level consistency only — it is **not** a proof of correct legal reasoning.

### The problem

LLMs produce legal advice that lacks structured reasoning:

* Missing clear identification of the legal issue
* No citation of applicable rules or statutes
* Conclusions without proper application of law to facts

### The solution

```python theme={null}
from qwed_legal import IRACGuard

guard = IRACGuard()

llm_output = """
Issue: Whether the defendant breached the employment contract.
Rule: Under California Labor Code § 2922, employment is presumed at-will.
Application: The defendant terminated employment without the 30-day notice 
required by the contract, which modified the at-will presumption.
Conclusion: The defendant breached the employment contract.
"""

result = guard.verify_structure(llm_output)

print(result["structure_valid"])  # True - all four IRAC sections present and coherent
print(result["status"])           # "unverifiable_reasoning"
print(result["verified"])         # False - reasoning correctness is never proven
print(result["components"])       # {'issue': '...', 'rule': '...', 'application': '...', 'conclusion': '...'}
```

<Info>
  `result["verified"]` is **always** `False` for `IRACGuard` — structural validity is not proof of correct legal reasoning. Branch on `structure_valid` and `status` instead, and route `unverifiable_reasoning` results to a human reviewer.
</Info>

### Detection types

| Check | Description |
| - | - |
| **Structure** | Verifies all 4 IRAC components are present |
| **Logical Disconnect** | Detects when Application doesn't reference the Rule |
| **Missing Steps** | Identifies which IRAC components are missing |

### Error response

```python theme={null}
result = guard.verify_structure("The defendant should pay damages.")

print(result["verified"])         # False
print(result["status"])           # "structure_invalid"
print(result["structure_valid"])  # False
print(result["error"])
# "STRUCTURE INVALID: Missing IRAC section(s): issue, rule, application,
#  conclusion. Legal analysis must contain Issue, Rule, Application, and Conclusion."
print(result["missing"])          # ['issue', 'rule', 'application', 'conclusion']
```

<Warning>
  `IRACGuard` checks **structure and surface-level coherence only**. A passing result has `status="unverifiable_reasoning"` — it confirms the four IRAC sections are present and structurally coherent, **not** that the cited rule exists or that the reasoning is legally sound. In the `verification_trace`, structure steps are `INFERRED` and the reasoning conclusion is `UNSUPPORTED` — never `DETERMINISTIC`.
</Warning>

***

## 8. FairnessGuard

**Status:** `HEURISTIC / FAIL-CLOSED`

**Purpose:** Apply a counterfactual consistency check that flags when output changes after protected attributes are swapped. This is a **heuristic signal**, not a fairness proof. Requires an external LLM client.

<Warning>
  As of **v0.4.0**, `FairnessGuard` **never returns `verified=True`** (issue #18). Legal fairness cannot be proven by text substitution and string equality, so the guard does not claim it. A consistent outcome is reported as `UNVERIFIABLE_FAIRNESS`; a differing outcome is a `HEURISTIC_BIAS_SIGNAL` that warrants human review. This is a breaking change from earlier versions that returned `FAIRNESS_VERIFIED`.
</Warning>

### The problem

AI legal systems can exhibit bias based on protected attributes:

* Different sentencing recommendations based on gender
* Inconsistent contract assessments based on party names
* Discriminatory loan approval reasoning

A single counterfactual swap with string-equality comparison cannot *prove* fairness — equivalent outcomes may differ in wording, and a single swap does not cover all relevant dimensions. So the result is always treated as a signal, never a pass.

### The solution

```python theme={null}
from qwed_legal import FairnessGuard

# Requires an LLM client for counterfactual generation
guard = FairnessGuard(llm_client=my_llm)

result = guard.verify_decision_fairness(
    original_prompt="Should John Smith receive parole given his rehabilitation record?",
    original_decision="Parole recommended based on positive rehabilitation.",
    protected_attribute_swap={"John": "Jane", "his": "her"},
)

print(result["verified"])  # Always False — fairness is never "proven"
print(result["status"])    # "UNVERIFIABLE_FAIRNESS"
print(result.get("risk"))  # "HEURISTIC_BIAS_SIGNAL" when outcomes differ
```

### How it works

1. **Input validation** — Rejects an empty swap, non-string values, and keys that collide when lowercased (fail-closed `ValueError` or `UNVERIFIABLE_FAIRNESS`).
2. **Counterfactual generation** — Swaps protected attributes (names, pronouns) in a single pass while preserving case.
3. **Re-evaluation** — Runs the modified prompt through the LLM.
4. **Heuristic comparison** — Compares outcomes by string equality. Consistency is reported as `UNVERIFIABLE_FAIRNESS` (not proof); a difference is a `HEURISTIC_BIAS_SIGNAL`.

### Response fields

| Field | Type | Description |
| - | - | - |
| `verified` | `bool` | **Always `False`.** Fairness is never proven by this guard. |
| `status` | `str` | `UNVERIFIABLE_FAIRNESS` or `LLM_GENERATION_FAILED` |
| `risk` | `str` | `HEURISTIC_BIAS_SIGNAL` (outcomes differed) or `LLM_GENERATION_FAILED`. Present only when applicable. |
| `message` | `str` | Explanation of the result |
| `variance` | `dict` | Present only when outcomes differ — contains `original` and `counterfactual` decisions |
| `verification_trace` | `list` | `VerificationStep` records; steps are `HEURISTIC`/`UNSUPPORTED`, never `DETERMINISTIC` |

### Outcomes

| status / risk | Meaning |
| - | - |
| `UNVERIFIABLE_FAIRNESS` (consistent) | Outcomes matched under one swap — **not** proof of fairness |
| `HEURISTIC_BIAS_SIGNAL` (differing) | Outcome changed under swap — route to human review |
| `UNVERIFIABLE_FAIRNESS` (empty swap) | No protected attributes provided — fail-closed |
| `LLM_GENERATION_FAILED` | The LLM client returned `None` for the counterfactual prompt |

<Warning>
  `FairnessGuard` requires an LLM client at initialization. Without it, `verify_decision_fairness()` raises a `ValueError`. Malformed `protected_attribute_swap` input (non-string values, case-colliding keys) also raises a `ValueError` — the guard never silently processes ambiguous input.
</Warning>

<Info>
  **Migration from earlier versions:** if your code branched on `result["verified"] == True` or `status == "FAIRNESS_VERIFIED"`, update it to treat the output as a signal. Consume `status` / `risk` and route `HEURISTIC_BIAS_SIGNAL` (and consistent-but-unverifiable) results to a human reviewer.
</Info>

***

## 9. ContradictionGuard

**Status:** `MIXED`

**Purpose:** Detect logical contradictions between modeled clauses using a Z3 constraint solver. The SAT/UNSAT result is `DETERMINISTIC`; clause categorization from text is `PARSED`. Coverage is limited to the supported clause categories below — a "consistent" result is **not** a proof of full contract consistency, and unmodeled clauses fail closed.

### The problem

Contracts can contain mathematically impossible combinations:

* "Liability capped at $10,000" + "Minimum penalty of $50,000"
* "Term is exactly 12 months" + "Minimum duration of 24 months"

Text-based heuristics (ClauseGuard) miss these formal logic conflicts.

### The solution

```python theme={null}
from qwed_legal import ContradictionGuard, Clause

guard = ContradictionGuard()

clauses = [
    Clause(id="1", text="Liability capped at 10000", category="LIABILITY", value=10000),
    Clause(id="2", text="Penalty shall be 50000", category="LIABILITY", value=50000),
]

result = guard.verify_consistency(clauses)

print(result["verified"])  # False
print(result["message"])   # "❌ LOGIC CONTRADICTION: Clauses are mutually exclusive..."
```

### Clause structure

The `Clause` dataclass requires:

| Field | Type | Description |
| - | - | - |
| `id` | `str` | Unique clause identifier |
| `text` | `str` | Human-readable clause text |
| `category` | `str` | `DURATION`, `LIABILITY`, or `TERMINATION` |
| `value` | `int` | Normalized numeric value (days, dollars, etc.) |

### Supported categories

| Category | Detects |
| - | - |
| `DURATION` | Conflicting term lengths (exact vs min/max) |
| `LIABILITY` | Cap vs penalty contradictions |

### Value provenance and word-boundary matching

Each encoded clause records text-derived operands in its `verification_trace` step — the solver encodes only operands parsed from `clause.text`, never the caller's declared `clause.value`:

| Trace field | Meaning |
| - | - |
| `value_provenance: "parsed_from_text"` | Legacy constant tag — the numeric operand was extracted from `clause.text` (e.g., `"capped at 10000"` → `10000`). Encoded step is `DETERMINISTIC`. |
| `caller_value` / `caller_value_agrees` | The caller's declared `clause.value` and whether any text-derived operand matches it. Recorded for disclosure only — it never enters the Z3 model. |

A `sat` result with unmodeled clauses or unsupported categories is reported as `partial_coverage` with `verified=False`. Since only text-evidenced operands are encoded, `Clause(text="exactly 1 month", value=999)` encodes `var==1` (not `999`) — the solver can never "prove" consistency of an invented caller number. The trace also records `disagreed_values` when the caller declaration differs from the text.

Keyword and value matching are both **word-boundary aware**:

* Keyword matching: `"cap"` no longer matches inside `"capability"`, so unmodeled prose falls through to partial coverage instead of encoding a spurious liability cap.
* Value token matching: `value=1` is no longer evidenced by `"12 months"`. Numeric operands must sit on their own token boundary.

A numeric operand longer than 18 digits can't be read. The guard treats the whole clause as unmodeled instead of dropping that operand.

### Z3 vs ClauseGuard

| Feature | ClauseGuard | ContradictionGuard |
| - | - | - |
| **Input** | Raw text strings | Structured `Clause` objects |
| **Method** | Text heuristics | Z3 SMT Solver |
| **Detects** | Permission conflicts | Mathematical impossibilities |
| **Use Case** | Quick checks | Formal verification |

***

## 10. ProvenanceGuard

**Status:** `DETERMINISTIC` (self-declared assurance only)

**Purpose:** Verify the internal consistency of a caller-supplied AI-generated content declaration. All checks are deterministic (SHA-256 hashing, regex pattern matching, datetime validation).

<Warning>
  **Trust model: self-declared.** Every field in the provenance record — including `human_reviewed` and `reviewer_id` — is caller-supplied. A well-formed record with fabricated review metadata will pass every check. Every `ProvenanceGuard` result carries `assurance: "SELF_DECLARED"` to reflect this. External assurance that a human actually reviewed the content requires out-of-band reviewer signatures, which are outside this guard's scope. Use this guard to catch tampering, missing disclosures, and unauthorized models; do not use it as proof that a human review occurred.
</Warning>

### The problem

AI transparency regulations (California CAITA 2026, EU AI Act Article 50) require AI-generated legal content to carry proper attribution. Without verification:

* Content may lack required AI-generation disclosures
* Provenance metadata can be incomplete or tampered with
* Unauthorized models may generate legal documents without audit trails

### The solution

```python theme={null}
from qwed_legal import ProvenanceGuard

guard = ProvenanceGuard(
    require_disclosure=True,
    require_human_review=False,
    allowed_models=["gpt-4", "claude-3-opus"]
)

content = "This AI-generated document reviews the contract terms..."
provenance = {
    "content_hash": "a1b2c3...",  # SHA-256 of content
    "model_id": "gpt-4",
    "generation_timestamp": "2026-03-24T12:00:00+00:00",
}

result = guard.verify_provenance(content, provenance)

print(result["verified"])        # True or False
print(result["assurance"])       # "SELF_DECLARED" — every result carries this
print(result["checks_passed"])   # ["metadata_completeness", "hash_integrity", ...]
print(result["checks_failed"])   # []
print(result["risk"])            # "" if verified, e.g. "CONTENT_TAMPERED" if not
```

### Verification checks

ProvenanceGuard runs up to six checks. The first three always run; the last three are configurable.

| Check | Description | Always runs |
| - | - | - |
| **Metadata completeness** | `content_hash`, `model_id`, and `generation_timestamp` are non-empty strings. `None`, `0`, `False`, `[]`, and whitespace-only values fail. | Yes |
| **Hash integrity** | SHA-256 of the content matches `content_hash` in provenance | Yes |
| **Timestamp validity** | ISO-8601 format, not in the future | Yes |
| **Disclosure compliance** | Content includes an AI-generation disclosure statement | If `require_disclosure=True` |
| **Model allowlist** | `model_id` is in the approved list | If `allowed_models` is set |
| **Human review (declared)** | `human_reviewed` is exactly `True` in provenance. Reported as `human_review_declared` in `checks_passed` / `checks_failed`. | If `require_human_review=True` |

<Info>
  The human-review check is named `human_review_declared` to make the trust boundary explicit: the guard only checks that the caller declared `human_reviewed=True`, not that a review actually happened. Truthy non-Boolean values (`"true"`, `1`) fail closed — only the literal `True` passes. The legacy `human_review` token is emitted alongside `human_review_declared` in `checks_passed` / `checks_failed` during a deprecation window; migrate consumers to the new name.
</Info>

When a required field is not a non-empty string, the hash, timestamp, and model allowlist checks cannot run. The guard records them in `checks_failed` as `hash_integrity_skipped`, `timestamp_valid_skipped`, and `model_allowed_skipped` (when `allowed_models` is set) instead of silently omitting them. These skip entries keep the risk at `INCOMPLETE_PROVENANCE`. They never produce `CONTENT_TAMPERED` or `UNAUTHORIZED_MODEL` for a value that was never compared.

```python theme={null}
result = guard.verify_provenance(content, {
    "content_hash": None,
    "model_id": 0,
    "generation_timestamp": "  ",
})
print(result["verified"])       # False
print(result["checks_failed"])  # ["metadata_completeness", "hash_integrity_skipped", "timestamp_valid_skipped", "model_allowed_skipped"]
print(result["risk"])           # "INCOMPLETE_PROVENANCE"
```

### Constructor parameters

<ParamField path="require_disclosure" type="bool" default="True">
  Require AI disclosure text in the content (e.g., "AI-generated", "produced by AI").
</ParamField>

<ParamField path="require_human_review" type="bool" default="False">
  Require `human_reviewed=True` in provenance metadata.
</ParamField>

<ParamField path="allowed_models" type="list[str] | None" default="None">
  Allowlist of model IDs. `None` allows all models; an empty list denies all.
</ParamField>

### Generating provenance records

You can also use ProvenanceGuard to generate provenance metadata:

```python theme={null}
from qwed_legal import ProvenanceGuard

guard = ProvenanceGuard()

record = guard.generate_provenance(
    content="This AI-generated contract summary...",
    model_id="gpt-4",
    disclosure_text="This document was generated by AI.",
    human_reviewed=True,
    reviewer_id="lawyer-42"
)

print(record.content_hash)           # SHA-256 hash
print(record.generation_timestamp)   # ISO-8601 UTC timestamp
print(record.human_reviewed)         # True
```

### ProvenanceRecord fields

| Field | Type | Description |
| - | - | - |
| `content_hash` | `str` | SHA-256 hash of the AI-generated content |
| `model_id` | `str` | Identifier of the model that generated the content |
| `generation_timestamp` | `str` | ISO-8601 timestamp of generation |
| `disclosure_text` | `str` | Human-readable AI disclosure statement |
| `human_reviewed` | `bool` | Whether a human has reviewed the content |
| `reviewer_id` | `str \| None` | Identifier of the human reviewer |

### Risk classifications

When verification fails, the `risk` field indicates the type of failure:

| Risk | Trigger |
| - | - |
| `CONTENT_TAMPERED` | Hash mismatch between content and a valid string `content_hash` |
| `INCOMPLETE_PROVENANCE` | Required metadata fields missing, empty, whitespace-only, or not strings. Also applies when checks are recorded as `*_skipped`. |
| `MISSING_DISCLOSURE` | No AI-generation disclosure found in content |
| `UNAUTHORIZED_MODEL` | A valid string `model_id` not in the allowed models list |
| `UNREVIEWED_CONTENT` | `human_reviewed` is not `True` |
| `INVALID_TIMESTAMP` | Timestamp is malformed or in the future |

<Info>
  ProvenanceGuard is fully deterministic — no LLM calls required. All checks use SHA-256 hashing, regex pattern matching, and datetime validation. The guard verifies the internal consistency of a caller-supplied declaration; every result carries `assurance="SELF_DECLARED"`. External assurance (that a human actually reviewed, that the recorded model actually produced the content) requires out-of-band reviewer signatures.
</Info>

***

## SACProcessor (RAG helper) 📄

**Purpose:** Prevent Document-Level Retrieval Mismatch (DRM) in legal RAG systems.

### The problem

Standard RAG chunking causes >95% retrieval mismatch in legal databases because:

* Legal documents share nearly identical boilerplate
* Chunk-level embeddings lose document context
* NDAs, contracts, and agreements look alike at the chunk level

### The solution

```python theme={null}
from qwed_legal import SACProcessor

sac = SACProcessor(llm_client=my_llm)

# Your existing chunks
chunks = naive_split(contract_text)

# Augment with document fingerprint
augmented = sac.generate_sac_chunks(
    document_text=contract_text,
    chunks=chunks,
    document_id="NDA-2026-001"
)

# Each chunk now includes global context
print(augmented[0])
# DOCUMENT CONTEXT [NDA-2026-001] (deterministic hash: 3f9c…) [AI-GENERATED SUMMARY — UNTRUSTED CONTENT]: NDA between Acme Corp and Beta Inc...
# CHUNK CONTENT [1/10]: Original chunk text here...
```

### Untrusted fingerprint labeling

The document-fingerprint header is labeled `[AI-GENERATED SUMMARY — UNTRUSTED CONTENT]` and is always paired with the deterministic SHA-256 document hash — even when a caller-supplied `document_id` is provided. The hash is computable and verifiable; the LLM summary is not.

The LLM summary is sanitized before it is embedded in every chunk:

* Flattened to a single line (newlines and control characters stripped).
* Forged `CHUNK CONTENT` and `DOCUMENT CONTEXT` markers are removed, so a compromised summarizer cannot forge chunk structure.
* Raw responses are size-capped before sanitization.
* If a summary sanitizes to empty (for example, an all-marker response), the fingerprint falls back to the deterministic hash rather than embedding an empty summary.

<Warning>
  Treat the summary as untrusted content in retrieval and downstream prompts. The deterministic hash is the only fingerprint field with an authenticity guarantee.
</Warning>

### Configuration

| Parameter | Default | Description |
| - | - | - |
| `target_summary_length` | 150 | Character limit for document fingerprint |
| `preview_chars` | 5000 | Max chars sent to LLM for summarization |

### Methods

| Method | Description |
| - | - |
| `generate_sac_chunks()` | Augment all chunks with document fingerprint |
| `generate_fingerprint_only()` | Get just the fingerprint for caching |

<Note>
  SACProcessor requires an LLM client. Generic (automated) summaries outperform expert-guided ones for retrieval.
</Note>

***

## All-in-one: LegalGuard

For convenience, use the unified `LegalGuard` class:

```python theme={null}
from qwed_legal import LegalGuard

# Optional: provide llm_client for FairnessGuard
guard = LegalGuard(
    llm_client=my_llm,
    provenance_config={
        "require_disclosure": True,
        "require_human_review": False,
        "allowed_models": ["gpt-4", "claude-3-opus"],
    }
)

# All 10 guards available
guard.verify_deadline(...)
guard.verify_liability_cap(...)
guard.check_clause_consistency(...)          # ClauseGuard (text heuristics)
guard.verify_citation(...)
guard.verify_jurisdiction(...)
guard.verify_statute_of_limitations(...)
guard.verify_irac_structure(...)             # v0.3.0
guard.verify_fairness(...)                   # v0.3.0 (requires llm_client)
guard.verify_contradiction(...)              # v0.3.0 (Z3 SMT Solver)
guard.verify_provenance(content, provenance) # NEW in v0.4.0
```

<Info>
  `LegalGuard` is a convenience wrapper. It does not change the verification boundaries of the underlying guards. `DeadlineGuard`, `LiabilityGuard`, and `ProvenanceGuard` are deterministic for supported inputs; the remaining guards are partial or heuristic. Only `verify_fairness()` requires an LLM client.
</Info>

***

## Next steps

* [Examples](/legal/examples) - Real-world scenarios
* [Troubleshooting](/legal/troubleshooting) - Common issues


## Related topics

- [QWED Legal examples for contract verification](/legal/examples.md)
- [QWED Legal: deterministic verification guards for legal AI](/legal/overview.md)
- [QWED Tax guards for AI payroll and tax verification](/tax/guards.md)
- [QWED Legal troubleshooting](/legal/troubleshooting.md)
- [QWED MCP examples for verified tool execution](/mcp/examples.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.