rag-wright 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. rag_wright/__init__.py +13 -0
  2. rag_wright/api/__init__.py +33 -0
  3. rag_wright/api/config.py +59 -0
  4. rag_wright/api/discover.py +70 -0
  5. rag_wright/api/documents.py +39 -0
  6. rag_wright/api/ids.py +31 -0
  7. rag_wright/api/invoke.py +99 -0
  8. rag_wright/api/kg.py +61 -0
  9. rag_wright/api/mcp.py +94 -0
  10. rag_wright/api/usage.py +30 -0
  11. rag_wright/api/workspace.py +85 -0
  12. rag_wright/capabilities/__init__.py +8 -0
  13. rag_wright/capabilities/answer_generator.py +427 -0
  14. rag_wright/capabilities/ard.py +286 -0
  15. rag_wright/capabilities/assertion_extraction.py +79 -0
  16. rag_wright/capabilities/chunk_read.py +58 -0
  17. rag_wright/capabilities/chunk_write.py +163 -0
  18. rag_wright/capabilities/claim_extraction.py +153 -0
  19. rag_wright/capabilities/clause_exception_linking.py +117 -0
  20. rag_wright/capabilities/compliance_judgment.py +322 -0
  21. rag_wright/capabilities/compliance_store.py +87 -0
  22. rag_wright/capabilities/contract_kg_serve.py +156 -0
  23. rag_wright/capabilities/contract_kg_store.py +251 -0
  24. rag_wright/capabilities/dg_extraction.py +585 -0
  25. rag_wright/capabilities/disambiguation.py +163 -0
  26. rag_wright/capabilities/document_parse.py +87 -0
  27. rag_wright/capabilities/document_scope.py +49 -0
  28. rag_wright/capabilities/embedding.py +164 -0
  29. rag_wright/capabilities/embedding_profiles.py +43 -0
  30. rag_wright/capabilities/entity_resolution.py +154 -0
  31. rag_wright/capabilities/fusion.py +64 -0
  32. rag_wright/capabilities/graph_extraction.py +243 -0
  33. rag_wright/capabilities/graph_query.py +73 -0
  34. rag_wright/capabilities/graph_storage.py +111 -0
  35. rag_wright/capabilities/highlight_serve.py +142 -0
  36. rag_wright/capabilities/hybrid_search.py +65 -0
  37. rag_wright/capabilities/invoke.py +31 -0
  38. rag_wright/capabilities/jev_decision.py +38 -0
  39. rag_wright/capabilities/manifests.py +872 -0
  40. rag_wright/capabilities/okf_navigate.py +456 -0
  41. rag_wright/capabilities/parsing.py +286 -0
  42. rag_wright/capabilities/property_boosted_retrieval.py +125 -0
  43. rag_wright/capabilities/query_function_classifier.py +94 -0
  44. rag_wright/capabilities/query_understanding.py +109 -0
  45. rag_wright/capabilities/registry.py +262 -0
  46. rag_wright/capabilities/remote_encoders.py +94 -0
  47. rag_wright/capabilities/requirement_extraction.py +247 -0
  48. rag_wright/capabilities/reranking.py +123 -0
  49. rag_wright/capabilities/retrieval_core.py +126 -0
  50. rag_wright/capabilities/rlm_chunking.py +808 -0
  51. rag_wright/capabilities/rlm_synthesis.py +316 -0
  52. rag_wright/capabilities/scan_quality.py +136 -0
  53. rag_wright/capabilities/span_relevance_judgment.py +191 -0
  54. rag_wright/capabilities/vision_to_text.py +85 -0
  55. rag_wright/capabilities/vlm_ocr.py +85 -0
  56. rag_wright/contracts/__init__.py +6 -0
  57. rag_wright/contracts/chunk.py +79 -0
  58. rag_wright/contracts/compliance.py +303 -0
  59. rag_wright/contracts/contract_meta.py +27 -0
  60. rag_wright/contracts/extraction.py +130 -0
  61. rag_wright/contracts/function.py +167 -0
  62. rag_wright/contracts/function_routing.py +91 -0
  63. rag_wright/contracts/highlight.py +74 -0
  64. rag_wright/contracts/identifiers.py +153 -0
  65. rag_wright/contracts/jurisdiction.py +96 -0
  66. rag_wright/contracts/ontology.py +142 -0
  67. rag_wright/contracts/property.py +201 -0
  68. rag_wright/contracts/provenance.py +78 -0
  69. rag_wright/contracts/query_intent.py +53 -0
  70. rag_wright/contracts/span.py +76 -0
  71. rag_wright/contracts/value_match.py +84 -0
  72. rag_wright/corpus/__init__.py +0 -0
  73. rag_wright/corpus/canonicalize.py +116 -0
  74. rag_wright/corpus/cuad.py +153 -0
  75. rag_wright/corpus/cuad_ingestion.py +72 -0
  76. rag_wright/corpus/document_parser.py +299 -0
  77. rag_wright/corpus/edgar.py +231 -0
  78. rag_wright/corpus/gcs_ingestion.py +120 -0
  79. rag_wright/corpus/http.py +110 -0
  80. rag_wright/corpus/selection.py +152 -0
  81. rag_wright/mcp/__init__.py +11 -0
  82. rag_wright/mcp/compliance_server.py +299 -0
  83. rag_wright/mcp/intra_document_qa_server.py +170 -0
  84. rag_wright/mcp/relational_qa_server.py +171 -0
  85. rag_wright/mcp/session_store.py +64 -0
  86. rag_wright/mcp/typed_property_retrieval_server.py +191 -0
  87. rag_wright/models/__init__.py +8 -0
  88. rag_wright/models/profiles.py +331 -0
  89. rag_wright/models/seam.py +497 -0
  90. rag_wright/models/tag_structured.py +285 -0
  91. rag_wright/models/tracing.py +179 -0
  92. rag_wright/models/usage.py +102 -0
  93. rag_wright/okf/__init__.py +11 -0
  94. rag_wright/okf/compile.py +292 -0
  95. rag_wright/okf/document.py +47 -0
  96. rag_wright/okf/enrich.py +176 -0
  97. rag_wright/okf/links.py +190 -0
  98. rag_wright/okf/lint.py +105 -0
  99. rag_wright/ontology/__init__.py +6 -0
  100. rag_wright/ontology/_generated_template_meta.py +60 -0
  101. rag_wright/ontology/_generated_vocab.py +52 -0
  102. rag_wright/ontology/clause_template.py +964 -0
  103. rag_wright/ontology/codegen.py +84 -0
  104. rag_wright/ontology/compliance_bridge.ttl +186 -0
  105. rag_wright/ontology/contract_bridge.ttl +2685 -0
  106. rag_wright/ontology/contract_taxonomy.py +24 -0
  107. rag_wright/ontology/derive.py +58 -0
  108. rag_wright/ontology/loader.py +435 -0
  109. rag_wright/ontology/packs/ftc_16cfr255.ttl +29 -0
  110. rag_wright/ontology/registry.py +87 -0
  111. rag_wright/ontology/template_introspect.py +100 -0
  112. rag_wright/py.typed +0 -0
  113. rag_wright/reference/__init__.py +2 -0
  114. rag_wright/reference/compliance.py +41 -0
  115. rag_wright/reference/contract_seam.py +123 -0
  116. rag_wright/skills/__init__.py +7 -0
  117. rag_wright/skills/claim_extraction/SKILL.md +47 -0
  118. rag_wright/skills/claim_extraction/__init__.py +1 -0
  119. rag_wright/skills/claim_extraction/template.py +50 -0
  120. rag_wright/skills/compliance_judgment/SKILL.md +59 -0
  121. rag_wright/skills/corpus_ingest/SKILL.md +106 -0
  122. rag_wright/skills/extraction_semantic_judge/SKILL.md +51 -0
  123. rag_wright/skills/extraction_semantic_judge/__init__.py +1 -0
  124. rag_wright/skills/generation/SKILL.md +64 -0
  125. rag_wright/skills/generation/__init__.py +1 -0
  126. rag_wright/skills/generic_compliance_judgment/SKILL.md +58 -0
  127. rag_wright/skills/okf_navigate/SKILL.md +137 -0
  128. rag_wright/skills/requirement_extraction/SKILL.md +47 -0
  129. rag_wright/skills/requirement_extraction/__init__.py +1 -0
  130. rag_wright/skills/requirement_extraction/template.py +50 -0
  131. rag_wright/skills/rlm/SKILL.md +186 -0
  132. rag_wright/skills/rlm/__init__.py +31 -0
  133. rag_wright/skills/rlm/agent.py +292 -0
  134. rag_wright/skills/span_relevance_judgment/SKILL.md +67 -0
  135. rag_wright/skills/vision_to_text/SKILL.md +36 -0
  136. rag_wright/skills/vision_to_text/__init__.py +1 -0
  137. rag_wright/spans/__init__.py +1 -0
  138. rag_wright/spans/boundary.py +78 -0
  139. rag_wright/spans/clause_function_classifier.py +490 -0
  140. rag_wright/spans/clause_kg_extractor.py +337 -0
  141. rag_wright/spans/cuad_labels.py +81 -0
  142. rag_wright/spans/dim_classifier.py +158 -0
  143. rag_wright/spans/dim_fleet.json +411 -0
  144. rag_wright/spans/function_classifier.py +77 -0
  145. rag_wright/spans/function_families.py +62 -0
  146. rag_wright/spans/hybrid_classifier.py +103 -0
  147. rag_wright/spans/legalbert_classifier.py +83 -0
  148. rag_wright/spans/model_capabilities.py +107 -0
  149. rag_wright/spans/new_function_labels.py +111 -0
  150. rag_wright/spans/page_map.py +68 -0
  151. rag_wright/spans/property_extractor.py +365 -0
  152. rag_wright/spans/property_grounding.py +182 -0
  153. rag_wright/spans/reclassify.py +77 -0
  154. rag_wright/spans/scarce_function_labels.py +105 -0
  155. rag_wright/spans/segment.py +341 -0
  156. rag_wright/spans/semantic_judge.py +197 -0
  157. rag_wright/spans/symbolic_validation.py +131 -0
  158. rag_wright/spans/tag_clause_extractor.py +182 -0
  159. rag_wright/store/__init__.py +6 -0
  160. rag_wright/store/arcadedb.py +1135 -0
  161. rag_wright/store/chunk_text.py +66 -0
  162. rag_wright/store/seam.py +213 -0
  163. rag_wright/subgraphs/__init__.py +0 -0
  164. rag_wright/subgraphs/async_ingestion.py +204 -0
  165. rag_wright/subgraphs/compliance_check.py +1042 -0
  166. rag_wright/subgraphs/compliance_ingestion.py +306 -0
  167. rag_wright/subgraphs/contract_ingestion_pipeline.py +999 -0
  168. rag_wright/subgraphs/graph_extraction.py +102 -0
  169. rag_wright/subgraphs/intra_document_qa.py +328 -0
  170. rag_wright/subgraphs/observability.py +140 -0
  171. rag_wright/subgraphs/query_constraint_extraction.py +73 -0
  172. rag_wright/subgraphs/relational_qa.py +165 -0
  173. rag_wright/subgraphs/requirement_extraction.py +137 -0
  174. rag_wright/subgraphs/scaffold.py +65 -0
  175. rag_wright/subgraphs/semantic_chunking.py +183 -0
  176. rag_wright/subgraphs/typed_clause_extraction.py +172 -0
  177. rag_wright/subgraphs/typed_property_retrieval.py +278 -0
  178. rag_wright/util/__init__.py +1 -0
  179. rag_wright/util/concurrent.py +153 -0
  180. rag_wright/util/spacy_model.py +45 -0
  181. rag_wright-0.1.0.dist-info/METADATA +168 -0
  182. rag_wright-0.1.0.dist-info/RECORD +184 -0
  183. rag_wright-0.1.0.dist-info/WHEEL +4 -0
  184. rag_wright-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,964 @@
1
+ """
2
+ Closed-vocab bridge: FOLIO clause types + ODRL rights/duties + PROV-O provenance.
3
+
4
+ Key entities:
5
+ - Clause (root): One operative contract clause.
6
+ - Jurisdiction: The governing-law jurisdiction(s): the name plus whether one or multiple laws govern.
7
+
8
+ Key relationships:
9
+ - Clause --BOUNDED_BY--> TemporalConstraint
10
+ - Clause --HAS_CAPS--> CapConstraint
11
+ - Clause --GOVERNED_BY--> Jurisdiction
12
+
13
+ Generated by docling-graph template generation from src/rag_wright/ontology/contract_bridge.ttl.
14
+
15
+ AUTHORITATIVE ARTIFACT — do NOT blindly regenerate (ADR-0037). This file was generated ONCE as a bootstrap and
16
+ is now HAND-MAINTAINED. It carries load-bearing EXTRACTION-PROMPT ENGINEERING that the ontology/spec cannot
17
+ express and that regeneration would clobber: the LLM-guiding field descriptions, brevity instructions, `examples`,
18
+ `max_length` guards, and the `_normalize_enum` validators. It may also DEVIATE from the ontology for extraction
19
+ reasons (see `document_reference` below). The `.ttl`/spec are the ontology (domain) source of truth and the
20
+ one-time bootstrap; this `.py` is the source of truth for the extraction template. If you must regenerate,
21
+ regenerate to a scratch file and hand-port every refinement here.
22
+ """
23
+
24
+ import logging
25
+ import re
26
+ from enum import Enum
27
+ from typing import Any, List, Optional, Type
28
+
29
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
30
+ from typing_extensions import Self
31
+
32
+ from rag_wright.ontology._generated_template_meta import DESCRIPTIONS, EXAMPLES
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ def _d(key: str) -> str:
38
+ """ADR-0066 P1b-2 (Rule 3): the field's LOOK-FOR description, GENERATED from the ttl (empty string if the ttl
39
+ has no definition for it -- the 10 TODO gaps, filled later in P1b-3). Edit the ttl, not this file."""
40
+ return DESCRIPTIONS.get(key, "")
41
+
42
+
43
+ def _ex(key: str) -> list[str]:
44
+ """ADR-0066 P1b-2 (Rule 3): the field's examples, GENERATED from the ttl."""
45
+ return list(EXAMPLES.get(key, ()))
46
+
47
+
48
+ # -----------------------------------------------------------------------------
49
+ # Docling Graph edge helper
50
+ # -----------------------------------------------------------------------------
51
+ def edge(
52
+ label: str,
53
+ default: Any = None,
54
+ *,
55
+ reference: bool = False,
56
+ closed_catalog: bool = False,
57
+ **kwargs: Any,
58
+ ) -> Any:
59
+ """
60
+ Declare a field as a graph edge for Docling-Graph via json_schema_extra.
61
+
62
+ ``reference=True`` marks an identity-ONLY link (graph_reference): the field
63
+ carries id-only references to entities described in full elsewhere in the
64
+ schema. In dense extraction these references are filled by the PARENT's own
65
+ fill call (never discovered separately), which preserves per-parent
66
+ membership lists and avoids phantom parents.
67
+
68
+ ``closed_catalog=True`` (reference_closed_catalog) declares that the targets
69
+ form a CLOSED catalog defined elsewhere in the document: a target that only
70
+ exists through this field (never instantiated or referenced otherwise) is a
71
+ hallucinated member — the converter then drops the edge (and the orphaned
72
+ target), unless that would wipe more than half of the target class.
73
+ """
74
+ json_schema_extra = dict(kwargs.pop("json_schema_extra", {}) or {})
75
+ json_schema_extra["edge_label"] = label
76
+ if reference:
77
+ json_schema_extra["graph_reference"] = True
78
+ if closed_catalog:
79
+ json_schema_extra["reference_closed_catalog"] = True
80
+
81
+ if "default_factory" in kwargs:
82
+ default_factory = kwargs.pop("default_factory")
83
+ return Field(default_factory=default_factory, json_schema_extra=json_schema_extra, **kwargs)
84
+
85
+ return Field(default, json_schema_extra=json_schema_extra, **kwargs)
86
+
87
+
88
+ # -----------------------------------------------------------------------------
89
+ # Helpers
90
+ # -----------------------------------------------------------------------------
91
+ _KEYWORD_MIN_LEN = 5 # issue 0033: a value token must be this long to be matched as a substring (avoid noise)
92
+
93
+
94
+ def _match_enum_keyword(enum_cls: Type[Enum], key: str) -> Any:
95
+ """issue 0033: a keyword fallback for a value-bearing LIST enum -- when the model echoed the clause
96
+ verbatim ("Except in respect of the Supplier's INDEMNIFICATION obligations...") instead of the canonical
97
+ token, map on the member VALUE appearing as a substring of the collapsed input. The LONGEST value wins (so
98
+ a specific value beats a prefix of it), OTHER is never a keyword, and short tokens (< 5 chars) are ignored
99
+ to avoid spurious hits. Returns the member or None (caller then falls back to OTHER)."""
100
+ best = None
101
+ best_len = 0
102
+ for member in enum_cls:
103
+ if member.name == "OTHER":
104
+ continue
105
+ token = re.sub(r"[^A-Za-z0-9]+", "", str(member.value)).lower()
106
+ if len(token) >= _KEYWORD_MIN_LEN and token in key and len(token) > best_len:
107
+ best, best_len = member, len(token)
108
+ return best
109
+
110
+
111
+ def _normalize_enum(enum_cls: Type[Enum], v: Any, *, keyword_fallback: bool = False) -> Any:
112
+ """
113
+ Accept enum instances, value strings, or member names.
114
+ Handles various formats: 'VALUE', 'value', 'Value', 'VALUE_NAME'.
115
+ Falls back to the OTHER member instead of rejecting (never raises).
116
+
117
+ issue 0033: with `keyword_fallback=True` (the value-bearing LIST dims -- carve_out / covered_subject /
118
+ damage_type), an input that is not an exact match but CONTAINS a canonical value token maps to that value
119
+ before falling back to OTHER. This recovers the document-side case where the extractor quotes the clause
120
+ (e.g. an indemnification carve-out stated as a full "Except in respect of ..." phrase) instead of emitting
121
+ the bare token as it does on the short query side. These dims are lexically grounded (ADR-0028), so a
122
+ spurious keyword hit whose cue is absent from the text is still downgraded by the grounding gate.
123
+ """
124
+ if isinstance(v, enum_cls):
125
+ return v
126
+ if isinstance(v, str):
127
+ key = re.sub(r"[^A-Za-z0-9]+", "", v).lower()
128
+ mapping: dict[str, Any] = {}
129
+ for member in enum_cls:
130
+ mapping[re.sub(r"[^A-Za-z0-9]+", "", member.name).lower()] = member
131
+ mapping[re.sub(r"[^A-Za-z0-9]+", "", str(member.value)).lower()] = member
132
+ if key in mapping:
133
+ return mapping[key]
134
+ if keyword_fallback:
135
+ hit = _match_enum_keyword(enum_cls, key)
136
+ if hit is not None:
137
+ return hit
138
+ if "OTHER" in enum_cls.__members__:
139
+ logger.warning("Unmapped enum value %r for %s; falling back to OTHER", v, enum_cls.__name__)
140
+ return enum_cls.OTHER
141
+ return v
142
+
143
+
144
+ _OPEN_LIST_MAX_CHARS = 120 # a subject/carve-out/damage value is a short phrase; longer = leaked prose -> dropped
145
+
146
+
147
+ def _clean_open_list(v: Any) -> list[str]:
148
+ """issue 0037: capture the OPEN descriptive list-dims (covered_subject / carve_out / damage_type) VERBATIM
149
+ rather than collapsing out-of-vocab items to OTHER (which discarded them). Coerce an enum member to its value,
150
+ strip, drop empties / leaked prose (too long or containing an XML tag). Canonicalization onto the closed vocab
151
+ happens at the assertion boundary (`clause_kg_extractor`), which keeps the verbatim phrase when nothing matches."""
152
+ items = v if isinstance(v, list) else [v]
153
+ out: list[str] = []
154
+ for item in items:
155
+ s = (str(item.value) if isinstance(item, Enum) else str(item)).strip()
156
+ if s and len(s) <= _OPEN_LIST_MAX_CHARS and "<" not in s:
157
+ out.append(s)
158
+ return out
159
+
160
+
161
+ # -----------------------------------------------------------------------------
162
+ # Enums
163
+ # -----------------------------------------------------------------------------
164
+ class Subject(str, Enum):
165
+ """Controlled vocabulary for subject values. Unmapped values normalize to OTHER."""
166
+
167
+ IP_INFRINGEMENT = "ip_infringement"
168
+ TRADEMARK = "trademark"
169
+ COPYRIGHT = "copyright"
170
+ VIOLATION_OF_LAW = "violation_of_law"
171
+ FRAUD = "fraud"
172
+ GROSS_NEGLIGENCE = "gross_negligence"
173
+ OTHER = "Other"
174
+
175
+
176
+ class PartyScope(str, Enum):
177
+ """Controlled vocabulary for party_scope values. Unmapped values normalize to OTHER."""
178
+
179
+ AFFILIATES = "affiliates"
180
+ LICENSOR_AFFILIATES = "licensor_affiliates"
181
+ LICENSEE_AFFILIATES = "licensee_affiliates"
182
+ OTHER = "Other"
183
+
184
+
185
+ class ExceptionModel(str, Enum):
186
+ """Controlled vocabulary for exception_model values. Unmapped values normalize to OTHER."""
187
+
188
+ INDEMNIFICATION = "indemnification"
189
+ CONFIDENTIALITY = "confidentiality"
190
+ THIRD_PARTY_IP_INFRINGEMENT = "third_party_ip_infringement"
191
+ FRAUD = "fraud"
192
+ GROSS_NEGLIGENCE = "gross_negligence"
193
+ WILLFUL_MISCONDUCT = "willful_misconduct"
194
+ BODILY_INJURY = "bodily_injury"
195
+ APPLICABLE_LAW = "applicable_law"
196
+ OTHER = "Other"
197
+
198
+
199
+ class AssignmentConsent(str, Enum):
200
+ """Controlled vocabulary for assignment_consent values. Unmapped values normalize to OTHER."""
201
+
202
+ CONSENT_REQUIRED = "consent_required"
203
+ NOTICE_ONLY = "notice_only"
204
+ FREE = "free"
205
+ OTHER = "Other"
206
+
207
+
208
+ class Asymmetry(str, Enum):
209
+ """Controlled vocabulary for asymmetry values. Unmapped values normalize to OTHER."""
210
+
211
+ SYMMETRIC = "symmetric"
212
+ DIFFERENT_PER_PARTY = "different_per_party"
213
+ OTHER = "Other"
214
+
215
+
216
+ class ClaimScope(str, Enum):
217
+ """Controlled vocabulary for claim_scope values. Unmapped values normalize to OTHER."""
218
+
219
+ FIRST_PARTY = "first_party"
220
+ THIRD_PARTY = "third_party"
221
+ BROAD_BASED = "broad_based"
222
+ OTHER = "Other"
223
+
224
+
225
+ class CocConsent(str, Enum):
226
+ """Controlled vocabulary for coc_consent values. Unmapped values normalize to OTHER."""
227
+
228
+ CONSENT_REQUIRED = "consent_required"
229
+ NOTICE_ONLY = "notice_only"
230
+ UNRESTRICTED = "unrestricted"
231
+ OTHER = "Other"
232
+
233
+
234
+ class EscrowReleaseTrigger(str, Enum):
235
+ """Controlled vocabulary for escrow_release_trigger values. Unmapped values normalize to OTHER."""
236
+
237
+ BANKRUPTCY = "bankruptcy"
238
+ BREACH = "breach"
239
+ DISCONTINUANCE = "discontinuance"
240
+ OTHER = "Other"
241
+
242
+
243
+ class ExclusivityType(str, Enum):
244
+ """Controlled vocabulary for exclusivity_type values. Unmapped values normalize to OTHER."""
245
+
246
+ EXCLUSIVE = "exclusive"
247
+ SOLE = "sole"
248
+ NON_EXCLUSIVE = "non_exclusive"
249
+ OTHER = "Other"
250
+
251
+
252
+ class Favorability(str, Enum):
253
+ """Controlled vocabulary for favorability values. Unmapped values normalize to OTHER."""
254
+
255
+ BUYER_FAVORABLE = "buyer_favorable"
256
+ SELLER_FAVORABLE = "seller_favorable"
257
+ OTHER = "Other"
258
+
259
+
260
+ class IpOwnership(str, Enum):
261
+ """Controlled vocabulary for ip_ownership values. Unmapped values normalize to OTHER."""
262
+
263
+ ASSIGNED = "assigned"
264
+ JOINT = "joint"
265
+ RETAINED = "retained"
266
+ OTHER = "Other"
267
+
268
+
269
+ class MfnScope(str, Enum):
270
+ """Controlled vocabulary for mfn_scope values. Unmapped values normalize to OTHER."""
271
+
272
+ PRICE = "price"
273
+ TERMS = "terms"
274
+ PRICE_AND_TERMS = "price_and_terms"
275
+ OTHER = "Other"
276
+
277
+
278
+ class Mutuality(str, Enum):
279
+ """Controlled vocabulary for mutuality values. Unmapped values normalize to OTHER."""
280
+
281
+ MUTUAL = "mutual"
282
+ UNILATERAL = "unilateral"
283
+ OTHER = "Other"
284
+
285
+
286
+ class RenewalMechanism(str, Enum):
287
+ """Controlled vocabulary for renewal_mechanism values. Unmapped values normalize to OTHER."""
288
+
289
+ AUTO = "auto"
290
+ REQUIRES_NOTICE = "requires_notice"
291
+ OTHER = "Other"
292
+
293
+
294
+ class RestrictionScope(str, Enum):
295
+ """Controlled vocabulary for restriction_scope values. Unmapped values normalize to OTHER."""
296
+
297
+ GEOGRAPHIC = "geographic"
298
+ ACTIVITY = "activity"
299
+ GEOGRAPHIC_AND_ACTIVITY = "geographic_and_activity"
300
+ OTHER = "Other"
301
+
302
+
303
+ class RightOfFirstType(str, Enum):
304
+ """Controlled vocabulary for right_of_first_type values. Unmapped values normalize to OTHER."""
305
+
306
+ ROFR = "rofr"
307
+ ROFO = "rofo"
308
+ ROFN = "rofn"
309
+ OTHER = "Other"
310
+
311
+
312
+ class TerminationRight(str, Enum):
313
+ """Controlled vocabulary for termination_right values. Unmapped values normalize to OTHER."""
314
+
315
+ EITHER_PARTY = "either_party"
316
+ ONE_PARTY = "one_party"
317
+ OTHER = "Other"
318
+
319
+
320
+ class WarrantyScope(str, Enum):
321
+ """Controlled vocabulary for warranty_scope values. Unmapped values normalize to OTHER."""
322
+
323
+ IMPLIED = "implied"
324
+ EXPRESS = "express"
325
+ AS_IS = "as_is"
326
+ NON_RELIANCE = "non_reliance"
327
+ OTHER = "Other"
328
+
329
+
330
+ class DamageType(str, Enum):
331
+ """Controlled vocabulary for damage_type values. Unmapped values normalize to OTHER."""
332
+
333
+ INDIRECT = "indirect"
334
+ CONSEQUENTIAL = "consequential"
335
+ INCIDENTAL = "incidental"
336
+ PUNITIVE = "punitive"
337
+ SPECIAL = "special"
338
+ OTHER = "Other"
339
+
340
+
341
+ class NonsolicitTarget(str, Enum):
342
+ """Controlled vocabulary for nonsolicit_target values. Unmapped values normalize to OTHER."""
343
+
344
+ EMPLOYEES = "employees"
345
+ CUSTOMERS = "customers"
346
+ OTHER = "Other"
347
+
348
+
349
+ class ProceduralDuty(str, Enum):
350
+ """Controlled vocabulary for procedural_duty values. Unmapped values normalize to OTHER."""
351
+
352
+ DUTY_TO_DEFEND = "duty_to_defend"
353
+ CONTROL_OF_DEFENSE = "control_of_defense"
354
+ OTHER = "Other"
355
+
356
+
357
+ class CapBasis(str, Enum):
358
+ """Controlled vocabulary for cap_basis values. Unmapped values normalize to OTHER."""
359
+
360
+ FIXED_FEE = "fixed_fee"
361
+ MULTIPLE_OF_FEES = "multiple_of_fees"
362
+ CAP_OTHER = "cap_other"
363
+ OTHER = "Other"
364
+
365
+
366
+ class LawMultiplicity(str, Enum):
367
+ """Controlled vocabulary for law_multiplicity values. Unmapped values normalize to OTHER."""
368
+
369
+ SINGLE = "single"
370
+ MULTIPLE = "multiple"
371
+ OTHER = "Other"
372
+
373
+
374
+ # -----------------------------------------------------------------------------
375
+ # Components
376
+ # -----------------------------------------------------------------------------
377
+ class CapConstraint(BaseModel):
378
+ """
379
+ The liability cap: its basis plus the open quantum (e.g. '12 months of fees', '$1,000,000').
380
+ """
381
+
382
+ model_config = ConfigDict(is_entity=False)
383
+
384
+ cap_basis: CapBasis = Field(
385
+ CapBasis.OTHER,
386
+ description=_d("CapConstraint.cap_basis"), # TODO(docling-graph): add a 1-3 sentence LOOK-FOR description
387
+ )
388
+
389
+ cap_operator: Optional[str] = Field(
390
+ None,
391
+ max_length=40,
392
+ description=_d("CapConstraint.cap_operator"),
393
+ )
394
+
395
+ cap_quantum: Optional[str] = Field(
396
+ None,
397
+ max_length=160, # ADR-0081 caps tightening: allow phrase-form caps ('the greater of X or Y', 'the fees paid ...')
398
+ description=_d("CapConstraint.cap_quantum"),
399
+ )
400
+
401
+ @field_validator("cap_basis", mode="before")
402
+ @classmethod
403
+ def _normalize_cap_basis(cls, v: Any) -> Any:
404
+ """Map free-text values onto CapBasis members (falls back to OTHER)."""
405
+ return _normalize_enum(CapBasis, v)
406
+
407
+ def __str__(self) -> str:
408
+ parts = [self.cap_operator, self.cap_quantum]
409
+ return " ".join(str(p) for p in parts if p) or "Unknown"
410
+
411
+
412
+ class TemporalConstraint(BaseModel):
413
+ """A time bound on the clause: a term/duration or a notice period."""
414
+
415
+ model_config = ConfigDict(is_entity=False)
416
+
417
+ temporal_duration: Optional[str] = Field(
418
+ None,
419
+ max_length=60,
420
+ description=_d("TemporalConstraint.temporal_duration"),
421
+ )
422
+
423
+ temporal_kind: Optional[str] = Field(
424
+ None,
425
+ max_length=40,
426
+ description=_d("TemporalConstraint.temporal_kind"),
427
+ )
428
+
429
+ temporal_operator: Optional[str] = Field(
430
+ None,
431
+ max_length=40,
432
+ description=_d("TemporalConstraint.temporal_operator"), # TODO(docling-graph): add a 1-3 sentence LOOK-FOR description
433
+ )
434
+
435
+ def __str__(self) -> str:
436
+ parts = [self.temporal_duration, self.temporal_kind]
437
+ return " ".join(str(p) for p in parts if p) or "Unknown"
438
+
439
+
440
+ # -----------------------------------------------------------------------------
441
+ # Entities
442
+ # -----------------------------------------------------------------------------
443
+ class Jurisdiction(BaseModel):
444
+ """The governing-law jurisdiction(s): the name plus whether one or multiple laws govern."""
445
+
446
+ model_config = ConfigDict(graph_id_fields=["jurisdiction_name"])
447
+
448
+ jurisdiction_name: str = Field(
449
+ ...,
450
+ max_length=100,
451
+ description=_d("Jurisdiction.jurisdiction_name"),
452
+ examples=_ex("Jurisdiction.jurisdiction_name"),
453
+ )
454
+
455
+ law_multiplicity: LawMultiplicity = Field(
456
+ LawMultiplicity.OTHER,
457
+ description=_d("Jurisdiction.law_multiplicity"), # TODO(docling-graph): add a 1-3 sentence LOOK-FOR description
458
+ )
459
+
460
+ @field_validator("law_multiplicity", mode="before")
461
+ @classmethod
462
+ def _normalize_law_multiplicity(cls, v: Any) -> Any:
463
+ """Map free-text values onto LawMultiplicity members (falls back to OTHER)."""
464
+ return _normalize_enum(LawMultiplicity, v)
465
+
466
+ def __str__(self) -> str:
467
+ parts = [self.jurisdiction_name]
468
+ return " ".join(str(p) for p in parts if p) or "Unknown"
469
+
470
+
471
+ class DisputeMethod(str, Enum):
472
+ """Controlled vocabulary for dispute_method values (ADR-0049 (2)). Unmapped values normalize to OTHER."""
473
+
474
+ ARBITRATION = "arbitration"
475
+ LITIGATION = "litigation"
476
+ MEDIATION = "mediation"
477
+ EXPERT_DETERMINATION = "expert_determination"
478
+ NEGOTIATION = "negotiation"
479
+ OTHER = "Other"
480
+
481
+
482
+ class CollateralType(str, Enum):
483
+ """Controlled vocabulary for collateral_type values (ADR-0049 (2), UCC Article 9 aligned). Unmapped -> OTHER."""
484
+
485
+ ACCOUNTS_RECEIVABLE = "accounts_receivable"
486
+ INVENTORY = "inventory"
487
+ EQUIPMENT = "equipment"
488
+ FIXTURES = "fixtures"
489
+ IP = "ip"
490
+ REAL_PROPERTY = "real_property"
491
+ DEPOSIT_ACCOUNTS = "deposit_accounts"
492
+ GENERAL_INTANGIBLES = "general_intangibles"
493
+ ALL_ASSETS = "all_assets"
494
+ OTHER = "Other"
495
+
496
+
497
+ class ForceMajeureEvent(str, Enum):
498
+ """Controlled vocabulary for force_majeure_event values (ADR-0049 (2)). Unmapped -> OTHER."""
499
+
500
+ ACT_OF_GOD = "act_of_god"
501
+ WAR = "war"
502
+ PANDEMIC = "pandemic"
503
+ GOVERNMENT_ACTION = "government_action"
504
+ LABOR_DISPUTE = "labor_dispute"
505
+ SUPPLY_FAILURE = "supply_failure"
506
+ NATURAL_DISASTER = "natural_disaster"
507
+ OTHER = "Other"
508
+
509
+
510
+ class RoyaltyBasis(str, Enum):
511
+ """Controlled vocabulary for royalty_basis values (ADR-0049 (2)). Unmapped -> OTHER."""
512
+
513
+ PCT_NET_SALES = "pct_net_sales"
514
+ PCT_GROSS_SALES = "pct_gross_sales"
515
+ PER_UNIT = "per_unit"
516
+ FIXED = "fixed"
517
+ TIERED = "tiered"
518
+ OTHER = "Other"
519
+
520
+
521
+ class ConfidentialityException(str, Enum):
522
+ """Controlled vocabulary for confidentiality_exception values (ADR-0049 (2), standard NDA carve-outs)."""
523
+
524
+ REQUIRED_BY_LAW = "required_by_law"
525
+ PUBLICLY_AVAILABLE = "publicly_available"
526
+ INDEPENDENTLY_DEVELOPED = "independently_developed"
527
+ PRIOR_POSSESSION = "prior_possession"
528
+ THIRD_PARTY_SOURCE = "third_party_source"
529
+ OTHER = "Other"
530
+
531
+
532
+ class ConditionType(str, Enum):
533
+ """Controlled vocabulary for condition_type values (ADR-0049 (2), broadened via corpus check). Unmapped -> OTHER."""
534
+
535
+ REGULATORY_APPROVAL = "regulatory_approval"
536
+ FINANCING = "financing"
537
+ THIRD_PARTY_CONSENT = "third_party_consent"
538
+ DUE_DILIGENCE = "due_diligence"
539
+ BOARD_APPROVAL = "board_approval"
540
+ NO_MATERIAL_ADVERSE_CHANGE = "no_material_adverse_change"
541
+ COURT_APPROVAL = "court_approval"
542
+ CLOSING_CONDITION = "closing_condition"
543
+ OTHER = "Other"
544
+
545
+
546
+ # -----------------------------------------------------------------------------
547
+ # Root document
548
+ # -----------------------------------------------------------------------------
549
+ class Clause(BaseModel):
550
+ """
551
+ One operative contract clause. Its function/type is known from the classifier; this template
552
+ extracts the clause's typed properties, each read verbatim from the clause text.
553
+ """
554
+
555
+ model_config = ConfigDict(graph_id_fields=["document_reference"])
556
+
557
+ # DEVIATION from the ontology (ADR-0037): the domain says a clause HAS a reference (required), but the LLM
558
+ # cannot reliably extract one and, when forced, dumps a verbatim clause quote here -- ballooning the JSON
559
+ # past max_tokens and truncating the whole record (INGEST-REFACTOR). Since we DISCARD this field (clause
560
+ # identity is our deterministic chunk_id, not this), we make it Optional/null-permitted to stop the runaway.
561
+ document_reference: Optional[str] = Field(
562
+ None,
563
+ max_length=80,
564
+ description=(
565
+ _d("Clause.document_reference")
566
+ ),
567
+ examples=_ex("Clause.document_reference"),
568
+ )
569
+
570
+ audit_frequency: Optional[str] = Field(
571
+ None,
572
+ max_length=60,
573
+ description=(
574
+ _d("Clause.audit_frequency")
575
+ ),
576
+ )
577
+
578
+ clause_type: Optional[str] = Field(
579
+ None,
580
+ max_length=80,
581
+ description=(
582
+ _d("Clause.clause_type")
583
+ ),
584
+ )
585
+
586
+ collateral_type: List[CollateralType] = Field(
587
+ default_factory=list,
588
+ description=(
589
+ _d("Clause.collateral_type")
590
+ ),
591
+ )
592
+
593
+ commitment_quantum: Optional[str] = Field(
594
+ None,
595
+ max_length=80,
596
+ description=(
597
+ _d("Clause.commitment_quantum")
598
+ ),
599
+ )
600
+
601
+ condition_type: ConditionType = Field(
602
+ ConditionType.OTHER,
603
+ description=(
604
+ _d("Clause.condition_type")
605
+ ),
606
+ )
607
+
608
+ confidentiality_exception: List[ConfidentialityException] = Field(
609
+ default_factory=list,
610
+ description=(
611
+ _d("Clause.confidentiality_exception")
612
+ ),
613
+ )
614
+
615
+ force_majeure_event: List[ForceMajeureEvent] = Field(
616
+ default_factory=list,
617
+ description=(
618
+ _d("Clause.force_majeure_event")
619
+ ),
620
+ )
621
+
622
+ royalty_basis: RoyaltyBasis = Field(
623
+ RoyaltyBasis.OTHER,
624
+ description=(
625
+ _d("Clause.royalty_basis")
626
+ ),
627
+ )
628
+
629
+ covers: List[Subject] = Field( # issue 0040: CLOSED conduct vocab (fraud/gross_negligence/...) -- NOT open;
630
+ default_factory=list, # an out-of-vocab value ('API', a party name) is noise, dropped (not retained)
631
+ description=_d("Clause.covers"),
632
+ )
633
+
634
+ covers_party_scope: PartyScope = Field(
635
+ PartyScope.OTHER,
636
+ description=_d("Clause.covers_party_scope"), # TODO(docling-graph): add a 1-3 sentence LOOK-FOR description
637
+ )
638
+
639
+ dispute_method: DisputeMethod = Field(
640
+ DisputeMethod.OTHER,
641
+ description=(
642
+ _d("Clause.dispute_method")
643
+ ),
644
+ )
645
+
646
+ excepts: List[str] = Field( # issue 0037: OPEN capture -- verbatim carve-outs; canonicalized at the KG boundary
647
+ default_factory=list,
648
+ description=_d("Clause.excepts"),
649
+ )
650
+
651
+ has_assignment_consent: AssignmentConsent = Field(
652
+ AssignmentConsent.OTHER,
653
+ description=_d("Clause.has_assignment_consent"),
654
+ )
655
+
656
+ has_asymmetry: Asymmetry = Field(
657
+ Asymmetry.OTHER,
658
+ description=_d("Clause.has_asymmetry"), # TODO(docling-graph): add a 1-3 sentence LOOK-FOR description
659
+ )
660
+
661
+ has_claim_scope: ClaimScope = Field(
662
+ ClaimScope.OTHER,
663
+ description=_d("Clause.has_claim_scope"), # TODO(docling-graph): add a 1-3 sentence LOOK-FOR description
664
+ )
665
+
666
+ has_coc_consent: CocConsent = Field(
667
+ CocConsent.OTHER,
668
+ description=(
669
+ _d("Clause.has_coc_consent")
670
+ ),
671
+ )
672
+
673
+ has_escrow_release_trigger: EscrowReleaseTrigger = Field(
674
+ EscrowReleaseTrigger.OTHER,
675
+ description=(
676
+ _d("Clause.has_escrow_release_trigger")
677
+ ),
678
+ )
679
+
680
+ has_exclusivity_type: ExclusivityType = Field(
681
+ ExclusivityType.OTHER,
682
+ description=(
683
+ _d("Clause.has_exclusivity_type")
684
+ ),
685
+ )
686
+
687
+ has_favorability: Favorability = Field(
688
+ Favorability.OTHER,
689
+ description=_d("Clause.has_favorability"), # TODO(docling-graph): add a 1-3 sentence LOOK-FOR description
690
+ )
691
+
692
+ has_ip_ownership: IpOwnership = Field(
693
+ IpOwnership.OTHER,
694
+ description=_d("Clause.has_ip_ownership"), # TODO(docling-graph): add a 1-3 sentence LOOK-FOR description
695
+ )
696
+
697
+ has_mfn_scope: MfnScope = Field(
698
+ MfnScope.OTHER,
699
+ description=_d("Clause.has_mfn_scope"),
700
+ )
701
+
702
+ has_mutuality: Mutuality = Field(
703
+ Mutuality.OTHER,
704
+ description=(
705
+ _d("Clause.has_mutuality")
706
+ ),
707
+ )
708
+
709
+ has_renewal: RenewalMechanism = Field(
710
+ RenewalMechanism.OTHER,
711
+ description=_d("Clause.has_renewal"), # TODO(docling-graph): add a 1-3 sentence LOOK-FOR description
712
+ )
713
+
714
+ has_restriction_scope: RestrictionScope = Field(
715
+ RestrictionScope.OTHER,
716
+ description=_d("Clause.has_restriction_scope"),
717
+ )
718
+
719
+ has_right_of_first_type: RightOfFirstType = Field(
720
+ RightOfFirstType.OTHER,
721
+ description=(
722
+ _d("Clause.has_right_of_first_type")
723
+ ),
724
+ )
725
+
726
+ has_termination_right: TerminationRight = Field(
727
+ TerminationRight.OTHER,
728
+ description=_d("Clause.has_termination_right"),
729
+ )
730
+
731
+ has_warranty_scope: WarrantyScope = Field(
732
+ WarrantyScope.OTHER,
733
+ description=_d("Clause.has_warranty_scope"), # TODO(docling-graph): add a 1-3 sentence LOOK-FOR description
734
+ )
735
+
736
+ ld_trigger: Optional[str] = Field(
737
+ None,
738
+ description=_d("Clause.ld_trigger"),
739
+ )
740
+
741
+ prohibits_damage: List[str] = Field( # issue 0037: OPEN capture -- verbatim; canonicalized at the KG boundary
742
+ default_factory=list,
743
+ description=_d("Clause.prohibits_damage"),
744
+ )
745
+
746
+ prohibits_solicit: NonsolicitTarget = Field(
747
+ NonsolicitTarget.OTHER,
748
+ description=_d("Clause.prohibits_solicit"),
749
+ )
750
+
751
+ requires_duty: ProceduralDuty = Field(
752
+ ProceduralDuty.OTHER,
753
+ description=_d("Clause.requires_duty"),
754
+ )
755
+
756
+ bounded_by: Optional[TemporalConstraint] = edge(
757
+ label="BOUNDED_BY",
758
+ description=_d("Clause.bounded_by"),
759
+ )
760
+
761
+ caps: Optional[CapConstraint] = edge(
762
+ label="HAS_CAPS",
763
+ description=_d("Clause.caps"),
764
+ )
765
+
766
+ governed_by: Optional[Jurisdiction] = edge(
767
+ label="GOVERNED_BY",
768
+ description=_d("Clause.governed_by"),
769
+ )
770
+
771
+ @field_validator("covers", mode="before")
772
+ @classmethod
773
+ def _normalize_covers(cls, v: Any) -> Any:
774
+ """issue 0040: `covered_subject` is a CLOSED conduct vocabulary (Subject: fraud / gross_negligence /
775
+ ip_infringement / ...), NOT an open descriptive dim like carve_out. Map each item onto a Subject member
776
+ (keyword fallback recovers a quoted conduct phrase); an out-of-vocab value -> OTHER, dropped downstream.
777
+ Reverts the 0037 verbatim-retention for THIS dim only: retaining 'API'/party-names as AMBIGUOUS filled it
778
+ with 99.7% noise (354:1) because 'what does this cover?' is asked of every provision. carve_out/damage_type
779
+ stay open (their tails are real)."""
780
+ if isinstance(v, list):
781
+ return [_normalize_enum(Subject, item, keyword_fallback=True) for item in v]
782
+ return _normalize_enum(Subject, v, keyword_fallback=True)
783
+
784
+ @field_validator("covers_party_scope", mode="before")
785
+ @classmethod
786
+ def _normalize_covers_party_scope(cls, v: Any) -> Any:
787
+ """Map free-text values onto PartyScope members (falls back to OTHER)."""
788
+ return _normalize_enum(PartyScope, v)
789
+
790
+ @field_validator("excepts", mode="before")
791
+ @classmethod
792
+ def _normalize_excepts(cls, v: Any) -> Any:
793
+ """issue 0037: capture carve-outs VERBATIM (open dim); canonicalized onto `ExceptionModel` at the KG
794
+ boundary, keeping the phrase when no vocab value matches (was: collapse to OTHER -> discarded)."""
795
+ return _clean_open_list(v)
796
+
797
+ @field_validator("has_assignment_consent", mode="before")
798
+ @classmethod
799
+ def _normalize_has_assignment_consent(cls, v: Any) -> Any:
800
+ """Map free-text values onto AssignmentConsent members (falls back to OTHER)."""
801
+ return _normalize_enum(AssignmentConsent, v)
802
+
803
+ @field_validator("has_asymmetry", mode="before")
804
+ @classmethod
805
+ def _normalize_has_asymmetry(cls, v: Any) -> Any:
806
+ """Map free-text values onto Asymmetry members (falls back to OTHER)."""
807
+ return _normalize_enum(Asymmetry, v)
808
+
809
+ @field_validator("has_claim_scope", mode="before")
810
+ @classmethod
811
+ def _normalize_has_claim_scope(cls, v: Any) -> Any:
812
+ """Map free-text values onto ClaimScope members (falls back to OTHER)."""
813
+ return _normalize_enum(ClaimScope, v)
814
+
815
+ @field_validator("has_coc_consent", mode="before")
816
+ @classmethod
817
+ def _normalize_has_coc_consent(cls, v: Any) -> Any:
818
+ """Map free-text values onto CocConsent members (falls back to OTHER)."""
819
+ return _normalize_enum(CocConsent, v)
820
+
821
+ @field_validator("has_escrow_release_trigger", mode="before")
822
+ @classmethod
823
+ def _normalize_has_escrow_release_trigger(cls, v: Any) -> Any:
824
+ """Map free-text values onto EscrowReleaseTrigger members (falls back to OTHER)."""
825
+ return _normalize_enum(EscrowReleaseTrigger, v)
826
+
827
+ @field_validator("has_exclusivity_type", mode="before")
828
+ @classmethod
829
+ def _normalize_has_exclusivity_type(cls, v: Any) -> Any:
830
+ """Map free-text values onto ExclusivityType members (falls back to OTHER)."""
831
+ return _normalize_enum(ExclusivityType, v)
832
+
833
+ @field_validator("has_favorability", mode="before")
834
+ @classmethod
835
+ def _normalize_has_favorability(cls, v: Any) -> Any:
836
+ """Map free-text values onto Favorability members (falls back to OTHER)."""
837
+ return _normalize_enum(Favorability, v)
838
+
839
+ @field_validator("has_ip_ownership", mode="before")
840
+ @classmethod
841
+ def _normalize_has_ip_ownership(cls, v: Any) -> Any:
842
+ """Map free-text values onto IpOwnership members (falls back to OTHER)."""
843
+ return _normalize_enum(IpOwnership, v)
844
+
845
+ @field_validator("has_mfn_scope", mode="before")
846
+ @classmethod
847
+ def _normalize_has_mfn_scope(cls, v: Any) -> Any:
848
+ """Map free-text values onto MfnScope members (falls back to OTHER)."""
849
+ return _normalize_enum(MfnScope, v)
850
+
851
+ @field_validator("has_mutuality", mode="before")
852
+ @classmethod
853
+ def _normalize_has_mutuality(cls, v: Any) -> Any:
854
+ """Map free-text values onto Mutuality members (falls back to OTHER)."""
855
+ return _normalize_enum(Mutuality, v)
856
+
857
+ @field_validator("has_renewal", mode="before")
858
+ @classmethod
859
+ def _normalize_has_renewal(cls, v: Any) -> Any:
860
+ """Map free-text values onto RenewalMechanism members (falls back to OTHER)."""
861
+ return _normalize_enum(RenewalMechanism, v)
862
+
863
+ @field_validator("has_restriction_scope", mode="before")
864
+ @classmethod
865
+ def _normalize_has_restriction_scope(cls, v: Any) -> Any:
866
+ """Map free-text values onto RestrictionScope members (falls back to OTHER)."""
867
+ return _normalize_enum(RestrictionScope, v)
868
+
869
+ @field_validator("has_right_of_first_type", mode="before")
870
+ @classmethod
871
+ def _normalize_has_right_of_first_type(cls, v: Any) -> Any:
872
+ """Map free-text values onto RightOfFirstType members (falls back to OTHER)."""
873
+ return _normalize_enum(RightOfFirstType, v)
874
+
875
+ @field_validator("has_termination_right", mode="before")
876
+ @classmethod
877
+ def _normalize_has_termination_right(cls, v: Any) -> Any:
878
+ """Map free-text values onto TerminationRight members (falls back to OTHER)."""
879
+ return _normalize_enum(TerminationRight, v)
880
+
881
+ @field_validator("has_warranty_scope", mode="before")
882
+ @classmethod
883
+ def _normalize_has_warranty_scope(cls, v: Any) -> Any:
884
+ """Map free-text values onto WarrantyScope members (falls back to OTHER)."""
885
+ return _normalize_enum(WarrantyScope, v)
886
+
887
+ @field_validator("prohibits_damage", mode="before")
888
+ @classmethod
889
+ def _normalize_prohibits_damage(cls, v: Any) -> Any:
890
+ """issue 0037: capture damage types VERBATIM (open dim); canonicalized onto `DamageType` (+ skos:broader
891
+ synonyms) at the KG boundary, keeping the phrase when nothing matches (was: collapse to OTHER -> discarded)."""
892
+ return _clean_open_list(v)
893
+
894
+ @field_validator("prohibits_solicit", mode="before")
895
+ @classmethod
896
+ def _normalize_prohibits_solicit(cls, v: Any) -> Any:
897
+ """Map free-text values onto NonsolicitTarget members (falls back to OTHER)."""
898
+ return _normalize_enum(NonsolicitTarget, v)
899
+
900
+ @field_validator("requires_duty", mode="before")
901
+ @classmethod
902
+ def _normalize_requires_duty(cls, v: Any) -> Any:
903
+ """Map free-text values onto ProceduralDuty members (falls back to OTHER)."""
904
+ return _normalize_enum(ProceduralDuty, v)
905
+
906
+ @model_validator(mode="after")
907
+ def _deduplicate_covers(self) -> Self:
908
+ """Keep first occurrence per key (removes duplicates from chunked extraction)."""
909
+ if not self.covers:
910
+ return self
911
+ seen: set[str] = set()
912
+ unique: list[Any] = []
913
+ for item in self.covers:
914
+ key = str(item).strip().lower()
915
+ if key not in seen:
916
+ seen.add(key)
917
+ unique.append(item)
918
+ object.__setattr__(self, "covers", unique)
919
+ return self
920
+
921
+ @model_validator(mode="after")
922
+ def _deduplicate_excepts(self) -> Self:
923
+ """Keep first occurrence per key (removes duplicates from chunked extraction)."""
924
+ if not self.excepts:
925
+ return self
926
+ seen: set[str] = set()
927
+ unique: list[Any] = []
928
+ for item in self.excepts:
929
+ key = str(item).strip().lower()
930
+ if key not in seen:
931
+ seen.add(key)
932
+ unique.append(item)
933
+ object.__setattr__(self, "excepts", unique)
934
+ return self
935
+
936
+ @model_validator(mode="after")
937
+ def _deduplicate_prohibits_damage(self) -> Self:
938
+ """Keep first occurrence per key (removes duplicates from chunked extraction)."""
939
+ if not self.prohibits_damage:
940
+ return self
941
+ seen: set[str] = set()
942
+ unique: list[Any] = []
943
+ for item in self.prohibits_damage:
944
+ key = str(item).strip().lower()
945
+ if key not in seen:
946
+ seen.add(key)
947
+ unique.append(item)
948
+ object.__setattr__(self, "prohibits_damage", unique)
949
+ return self
950
+
951
+ def __str__(self) -> str:
952
+ parts = [self.document_reference, self.audit_frequency]
953
+ return " ".join(str(p) for p in parts if p) or "Unknown"
954
+
955
+
956
+ # -----------------------------------------------------------------------------
957
+ # Verification
958
+ # -----------------------------------------------------------------------------
959
+ # This template passed generation gates V1-V6 (parse, import allowlist,
960
+ # load, structured-output schema, dense catalog walk, graph-shape smoke
961
+ # test). Comments never reach extraction prompts — only class docstrings
962
+ # and field descriptions do. After hand edits, re-verify with:
963
+ #
964
+ # docling-graph template lint <module.path.Clause>