DETERMINISTIC, MIXED, or PARTIAL / HEURISTIC to indicate the strength of the underlying check.
DETERMINISTICguards return reproducible, provable results for supported, structured inputs.MIXEDguards 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 / HEURISTICguards 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.
Verification traces and evidence types
As of v0.4.0, every guard returns averification_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.
trace_to_dict() to export a trace into audit logs. Non-serializable input values are stringified (no silent data loss).
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.Structured diagnostics and proof references
Every guard result exposesto_diagnostic(claim_inputs=...), which returns a frozen LegalDiagnosticResult implementing the ecosystem 3-layer diagnostic contract:
- Layer 1 —
agent_message: agent-safe summary (the guard’s existing message). - Layer 2 —
developer_fields:claim_inputs, the fullverification_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 whenstatus == VERIFIED.
VERIFIED / UNVERIFIABLE / BLOCKED. Downstream gates check diagnostic.is_authoritative (equivalent to proof_ref is not None) — see the per-guard mapping table in 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
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 toVERIFIEDbut carries noDETERMINISTICtrace step returnsUNVERIFIABLEwith noproof_ref.ContradictionGuardadapter. A hand-built{"status": "consistent"}dict, or a transported verdict whose trace was stripped, returnsUNVERIFIABLE. Aconsistentverdict with empty or whitespace-onlyclaim_texts(for example[""]) also returnsUNVERIFIABLE.- Blank messages. An empty or non-string
messagefalls back to a status-specific default (for example"Clauses are consistent."or"Clauses are contradictory.") instead of crashing the adapter.
proof_ref.
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
Parameters
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.str
required
The term description (e.g., “30 days”, “30 business days”, “2 weeks”, “3 months”, “1 year”).
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.int
default:"0"
Allow +/- this many whole calendar days when verifying the deadline. Useful for accommodating minor rounding differences.
Response fields
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 because90is an unpaired quantity). - The
business/working/workqualifier 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
beforeorprior to. See Supported term forms and Event-anchored terms.
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.
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.
- 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"
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.
Supported term forms
Starting inqwed-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.
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.
Event-anchored terms
A term that starts its clock at an event other than signing or execution returnsUNVERIFIABLE 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.
- Confirm the event’s date and that the period runs forward from it.
- Pass the event’s date as
signing_date. - Rewrite the term to a bare-relative form, such as
term="30 days".
UNVERIFIABLE.
"before", "prior to") have no supported rewrite. Verify them outside DeadlineGuard.
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
UNVERIFIABLEwith the messageMixed timezone inputs. Supply both dates with a timezone, or both without.
Features
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.
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
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 15M” ❌ (Should be $10M)
- Float precision errors on large amounts
- Tiered liability miscalculations
Constructor parameters
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.The solution
verify_cap parameters
float
required
Total value of the contract.
float
required
Liability cap as a percentage (e.g.,
200 for 200%).float
required
The cap amount claimed by the LLM.
Response fields
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, andmath.nanfor any numeric input. - Finite but extreme magnitudes that overflow
Decimalarithmetic (e.g.,1e308). - Tiered inputs where any tier’s
baseorpercentageis non-finite. The failure message names the offending tier keys. - Empty tier list.
verify_tiered_liability(tiers=[], ...)returnsUNVERIFIABLEwithtotal_computed=Noneand noproof_ref, whetherclaimed_totalis0or any other value. An empty list has no operands to sum, so it cannot prove any total.
Additional methods
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”
The solution
The primarycheck_consistency() method uses text heuristics to detect conflicts. For formal logic verification, use verify_using_z3().
Detection types
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 extracts10(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.
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
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.
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.
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:
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
Supported citation patterns
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
Statute citations
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:
"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
Parameters
list[str]
required
List of ISO country codes for contract parties (e.g.,
["US", "UK"]).str
required
The stated governing law — can be a country code or US state name/abbreviation (e.g.,
"Delaware", "DE", "UK").str
The stated forum or venue for dispute resolution.
JurisdictionType
default:"JurisdictionType.EXCLUSIVE"
Type of jurisdiction clause. Accepts
JurisdictionType.EXCLUSIVE, JurisdictionType.NON_EXCLUSIVE, or JurisdictionType.HYBRID.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.Features
CISG detection
For cross-border parties,verify_choice_of_law() classifies contract_type three ways before deciding whether to raise a CISG warning:
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.
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.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.
JurisdictionResult adds two optional fields:
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
Useverify_forum_selection to validate a forum independently, with optional contract value threshold checks for US federal court diversity jurisdiction:
Convention check
[""] 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
Result status taxonomy
StatuteResult.statusand the fail-closed behaviors below trackqwed-legal0.5.0+. Against0.4.0and earlier,statusis absent — gate onverifiedinstead.
StatuteResult.status disambiguates the four outcomes previously conflated under verified:
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 asUNVERIFIABLE 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.
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()returnsverified=Falsewithjurisdiction_matched=False, all date and period fields set toNone, 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()returnsverified=Falsewithclaim_type_matched=False, and a message listing the supported claim types for that jurisdiction. - Ambiguous or partial dates → unverifiable. If
incident_dateorfiling_dateis 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()returnsstatus="UNVERIFIABLE". This applies in computation-only mode too, when you don’t passclaimed_within_period.incident_date,filing_date,expiration_date, anddays_remainingareNone. The trace keeps the raw strings and records anAMBIGUITY_NOTEDstep. ISO year-first and named-month dates still verify.
Parameters
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.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.str
required
Date the incident occurred. Use ISO
YYYY-MM-DD. Order-ambiguous and partial dates return UNVERIFIABLE.str
required
Date the claim was or will be filed. Use ISO
YYYY-MM-DD. Order-ambiguous and partial dates return UNVERIFIABLE.bool
Optional LLM claim to verify. When provided, the guard checks whether the LLM’s assertion (within/outside period) matches the computed result.
StatuteResult fields
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.str
One of
CLAIM_VERIFIED, CLAIM_INCORRECT, COMPUTED_ONLY, or UNVERIFIABLE. See the status taxonomy above.str
The claim type passed in (echoed back).
str
The jurisdiction passed in (echoed back).
Optional[datetime]
Parsed incident date, or
None if date parsing failed or a date input was order-ambiguous or partial.Optional[datetime]
Parsed filing date, or
None if date parsing failed or a date input was order-ambiguous or partial.Optional[float]
Limitation period applied, or
None if the jurisdiction or claim type is unknown.Optional[datetime]
Computed expiration date, or
None if no limitation period could be determined.Optional[int]
Days between filing date and expiration (negative if expired), or
None if no limitation period could be determined.str
Human-readable result. Unverifiable results are prefixed with
⚠️ UNVERIFIABLE: and list the supported jurisdictions or claim types.bool
default:"True"
False when the jurisdiction is not in the supported list.bool
default:"True"
False when the claim type is not modeled for the given jurisdiction.Supported jurisdictions
12 jurisdictions are supported with periods for 10 claim types.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.
UNVERIFIABLEresult. When either lookup fails,verify()returns aStatuteResultwithverified=False, all date and period fields set toNone, and amessagethat begins with⚠️ UNVERIFIABLEand lists the supported values.- New flags.
StatuteResultexposesjurisdiction_matched: boolandclaim_type_matched: boolso callers can distinguish “claim is time-barred” from “we cannot determine the limit”.
Get limitation period
Look up the limitation period for a specific claim type and jurisdiction without performing a full verification. ReturnsNone when the jurisdiction or claim type is not supported:
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:
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
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.Detection types
Error response
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.
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
How it works
- Input validation — Rejects an empty swap, non-string values, and keys that collide when lowercased (fail-closed
ValueErrororUNVERIFIABLE_FAIRNESS). - Counterfactual generation — Swaps protected attributes (names, pronouns) in a single pass while preserving case.
- Re-evaluation — Runs the modified prompt through the LLM.
- Heuristic comparison — Compares outcomes by string equality. Consistency is reported as
UNVERIFIABLE_FAIRNESS(not proof); a difference is aHEURISTIC_BIAS_SIGNAL.
Response fields
Outcomes
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.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 50,000”
- “Term is exactly 12 months” + “Minimum duration of 24 months”
The solution
Clause structure
TheClause dataclass requires:
Supported categories
Value provenance and word-boundary matching
Each encoded clause records text-derived operands in itsverification_trace step — the solver encodes only operands parsed from clause.text, never the caller’s declared clause.value:
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=1is no longer evidenced by"12 months". Numeric operands must sit on their own token boundary.
Z3 vs ClauseGuard
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).
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
Verification checks
ProvenanceGuard runs up to six checks. The first three always run; the last three are configurable.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.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.
Constructor parameters
bool
default:"True"
Require AI disclosure text in the content (e.g., “AI-generated”, “produced by AI”).
bool
default:"False"
Require
human_reviewed=True in provenance metadata.list[str] | None
default:"None"
Allowlist of model IDs.
None allows all models; an empty list denies all.Generating provenance records
You can also use ProvenanceGuard to generate provenance metadata:ProvenanceRecord fields
Risk classifications
When verification fails, therisk field indicates the type of failure:
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.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
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 CONTENTandDOCUMENT CONTEXTmarkers 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.
Configuration
Methods
SACProcessor requires an LLM client. Generic (automated) summaries outperform expert-guided ones for retrieval.
All-in-one: LegalGuard
For convenience, use the unifiedLegalGuard class:
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.Next steps
- Examples - Real-world scenarios
- Troubleshooting - Common issues