> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qwedai.com/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` or `PARTIAL / HEURISTIC` to indicate the strength of the underlying check.

* `DETERMINISTIC` guards return reproducible results for supported, structured inputs.
* `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 should be treated as fail-closed: reject or mark the claim unverified rather than accepting it.

## 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 (ISO format or natural language).
</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.
</ParamField>

<ParamField path="tolerance_days" type="int" default="0">
  Allow +/- this many 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`      | `datetime`           | Parsed signing date                                                                                                                           |
| `claimed_deadline`  | `datetime`           | The deadline claimed by the LLM                                                                                                               |
| `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 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 either:

* 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"`).

Recognized time units are `day`/`calendar`, `week`, `month`, and `year`, with optional `business`/`working`/`work` qualifiers for business-day arithmetic.

```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`.

### 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
```

***

## 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 |
| `contract_value` | `Decimal` | The contract value used                          |
| `cap_percentage` | `Decimal` | The percentage used                              |
| `claimed_cap`    | `Decimal` | The cap claimed by the LLM                       |
| `computed_cap`   | `Decimal` | The correct cap computed by the guard            |
| `difference`     | `Decimal` | Absolute difference between claimed and computed |
| `message`        | `str`     | Human-readable verification message              |

### 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     |

### 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 citation
result = guard.verify("Brown v. Board of Education, 347 U.S. 483 (1954)")
print(result.valid)  # True
print(result.parsed_components)
# {'volume': 347, 'reporter': 'U.S.', 'page': '483'}

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

### 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` |

### 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.valid)  # True
print(result.parsed_components)
# {'title': 42, 'code': 'U.S.C.', 'section': '1983'}
```

***

## 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"
)

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>

### 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 |

### 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
```

***

## 6. StatuteOfLimitationsGuard

**Status:** `PARTIAL / HEURISTIC`

**Purpose:** Compute claim limitation periods for supported jurisdictions and claim types using rule tables. 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 - 4 year limit exceeded!
print(result.expiration_date)   # 2024-01-15
print(result.days_remaining)    # -867 (negative = expired)
```

### 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.

```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 (ISO format).
</ParamField>

<ParamField path="filing_date" type="str" required>
  Date the claim was or will be filed (ISO format).
</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 the jurisdiction and claim type are recognized and the filing falls within the limitation period (and matches `claimed_within_period`, if supplied).
</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.
</ResponseField>

<ResponseField name="filing_date" type="Optional[datetime]">
  Parsed filing date, or `None` if date parsing failed.
</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#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["verified"])    # True
print(result["components"])  # {'issue': '...', 'rule': '...', 'application': '...', 'conclusion': '...'}
```

### 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["error"])     # "Failed Reasoned Elaboration. Missing steps: issue, rule, application, conclusion..."
print(result["missing"])   # ['issue', 'rule', 'application', 'conclusion']
```

***

## 8. FairnessGuard

**Status:** `PARTIAL / HEURISTIC`

**Purpose:** Apply counterfactual consistency checks to detect output that changes when protected attributes are swapped. This is a structural fairness check, not a complete fairness proof. Requires an external LLM client.

### 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

### 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"])  # True if decision is consistent
print(result["status"])    # "FAIRNESS_VERIFIED" or "JUDICIAL_BIAS_DETECTED"
```

### How it works

1. **Early exit** - If `protected_attribute_swap` is empty (`{}`), returns immediately with `NO_SWAP_REQUIRED` without calling the LLM
2. **Counterfactual Generation** - Swaps protected attributes (names, pronouns) while preserving case
3. **Re-evaluation** - Runs the modified prompt through the LLM
4. **Deterministic Comparison** - Checks if outcomes match exactly

### Response fields

| Field      | Type   | Description                                                                     |
| ---------- | ------ | ------------------------------------------------------------------------------- |
| `verified` | `bool` | Whether the decision is fair                                                    |
| `status`   | `str`  | `FAIRNESS_VERIFIED` or `NO_SWAP_REQUIRED` (on success)                          |
| `risk`     | `str`  | `JUDICIAL_BIAS_DETECTED` or `LLM_GENERATION_FAILED` (on failure)                |
| `message`  | `str`  | Explanation of the result                                                       |
| `variance` | `dict` | Present when bias detected — contains `original` and `counterfactual` decisions |

### Detection types

| Status / Risk            | Description                                                  |
| ------------------------ | ------------------------------------------------------------ |
| `FAIRNESS_VERIFIED`      | Decision unchanged after attribute swap                      |
| `JUDICIAL_BIAS_DETECTED` | Decision changed based on protected attributes               |
| `NO_SWAP_REQUIRED`       | No protected attributes to swap (empty dict passed)          |
| `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()` will raise a `ValueError`.
</Warning>

***

## 9. ContradictionGuard

**Status:** `PARTIAL / HEURISTIC`

**Purpose:** Detect logical contradictions between modeled clauses using a constraint solver. Coverage is limited to the supported clause categories below — a "consistent" result is **not** a proof of full contract consistency.

### 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               |

### 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`

**Purpose:** Verify AI-generated content carries proper provenance metadata and disclosure markers. All checks are deterministic (SHA-256 hashing, regex pattern matching, datetime validation).

### 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["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 present and non-empty | 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**          | `human_reviewed` is `True` in provenance                                         | If `require_human_review=True` |

### 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 `content_hash` |
| `INCOMPLETE_PROVENANCE` | Required metadata fields missing or empty        |
| `MISSING_DISCLOSURE`    | No AI-generation disclosure found in content     |
| `UNAUTHORIZED_MODEL`    | `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.
</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]: NDA between Acme Corp and Beta Inc...
# CHUNK CONTENT [1/10]: Original chunk text here...
```

### 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
