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,87 @@
1
+ """ADR-0117 DD-1b: the compliance domain's store extension.
2
+
3
+ Composes a generic `Store` (`kg_write`/`kg_read`) to persist + read the Requirement KG, so the engine store imports
4
+ no compliance contract. A new domain writes its own typed records the same way -- the pattern, not the schema, is
5
+ what the engine provides. The `Requirement` vertex + its property storage types are schema-declared (ADR-0067 P5b),
6
+ so `kg_write` encodes each field (incl. `pages` as a native array, `bbox`/`applicability_json` as JSON strings)."""
7
+ from __future__ import annotations
8
+
9
+ from typing import Iterable, Optional
10
+
11
+ from rag_wright.capabilities.highlight_serve import _decode_bbox
12
+ from rag_wright.contracts.compliance import Requirement, RequirementLocation
13
+ from rag_wright.store.arcadedb import REQUIREMENT_TYPE
14
+ from rag_wright.store.seam import KgNode
15
+
16
+
17
+ class ComplianceStore:
18
+ """Requirement-KG writes + the compliance READ facade over a generic `Store` (EP-REF-1b). The reads shape
19
+ the stored `Requirement` rows into the domain answers a product serves (what EP-SEAM-3 lifts), so the engine
20
+ store stays generic and a new domain supplies its own facade the same way."""
21
+
22
+ def __init__(self, store) -> None:
23
+ self._store = store
24
+
25
+ def write_requirements(self, requirements: Iterable[Requirement]) -> int:
26
+ """Upsert `Requirement` nodes by `requirement_id` (idempotent -- a re-ingest is a no-op on unchanged rules),
27
+ in one transaction. Domain-native values in; the store encodes per the declared storage type. Returns count."""
28
+ nodes = [
29
+ KgNode(REQUIREMENT_TYPE, "requirement_id", {
30
+ "requirement_id": req.requirement_id,
31
+ "source": req.source,
32
+ "citation": req.citation,
33
+ "deontic_type": req.deontic_type.value,
34
+ "actor": req.actor,
35
+ "requirement_text": req.requirement_text,
36
+ "evidence_standard": req.evidence_standard or "",
37
+ "severity": req.severity.value if req.severity else "",
38
+ "applicability_json": [[c.dimension, c.value] for c in req.applicability_scope],
39
+ "confidence": req.confidence.value,
40
+ "pages": [int(p) for p in req.pages],
41
+ "bbox": list(req.bbox) if req.bbox is not None else None,
42
+ })
43
+ for req in requirements
44
+ ]
45
+ self._store.kg_write(nodes)
46
+ return len(nodes)
47
+
48
+ # --- EP-REF-1b: the compliance READ facade (the reference versions the product seam lifts) ---------
49
+
50
+ def requirements_for(self, source: str) -> list[dict]:
51
+ """The curated requirement rows filed under one policy `source` -- the proof an ingest landed. DB-scoped
52
+ (`all_requirements(sources=[source])`), so a multi-policy store never fetches the rows outside scope."""
53
+ return self._store.all_requirements(sources=[source])
54
+
55
+ def requirement_locations(self, source: str) -> list[RequirementLocation]:
56
+ """Where each requirement of one policy sits in its document (pages + optional bbox), for a citation
57
+ preview. Reads defensively: a requirement curated before provenance landed (ADR-0107) has no pages/bbox,
58
+ so `pages` -> [] and a malformed/absent `bbox` -> None (a page is still openable without a rectangle)."""
59
+ return [
60
+ RequirementLocation(
61
+ requirement_id=str(r.get("requirement_id") or ""),
62
+ citation=str(r.get("citation") or ""),
63
+ pages=[int(p) for p in (r.get("pages") or [])],
64
+ bbox=_decode_bbox(r.get("bbox")),
65
+ text=str(r.get("requirement_text") or ""),
66
+ )
67
+ for r in self._store.all_requirements(sources=[source])
68
+ ]
69
+
70
+ def curated_requirement_count(self, sources: Optional[list[str]] = None) -> int:
71
+ """How many requirements are IN SCOPE -- the denominator of an honest coverage statement (consulted N of
72
+ curated M). `sources=None` counts store-wide; a list scopes it (DB-side), `[]` is zero."""
73
+ return len(self._store.all_requirements(sources=sources))
74
+
75
+ @staticmethod
76
+ def policy_of_requirement(requirement_id: str) -> str:
77
+ """The policy a requirement belongs to, from its id `<source>:<section>:<hash>`. Split on the FIRST colon
78
+ -- a source name may legitimately contain dots/hyphens ("ISO.27001-2022"), which a last-colon split eats.
79
+ Empty id -> ''. This prefix is the only link a `ComplianceFinding` carries back to its policy."""
80
+ return requirement_id.split(":", 1)[0] if requirement_id else ""
81
+
82
+ @staticmethod
83
+ def gated_pairs(report) -> list[dict]:
84
+ """The (assertion|document, rule) pairs the symbolic ACTOR gate skipped before any judge call -- honest
85
+ coverage (ADR-0068): a shallow copy of `ComplianceReport.gated_pairs`. The ACTOR gate ONLY; top-k semantic
86
+ narrowing also drops pairs and is NOT reported here, so `[]` does not mean every pair was evaluated."""
87
+ return [dict(entry) for entry in (getattr(report, "gated_pairs", None) or [])]
@@ -0,0 +1,156 @@
1
+ """KG-4 (FR-Q, ADR-0033): Leg A -- intra-contract scoped-query serving over the typed contract KG.
2
+
3
+ This is the structured/relational/cited backend the classification-only front door (`highlight_serve`)
4
+ couldn't give. Scoped to one `contract_id` (a clause_id is `<contract_id>:<index>:<hash>`, so a contract's
5
+ clauses are one key range), it answers questions over the typed KG that a clause-type filter can't:
6
+
7
+ - **disambiguate** same-type clauses by a property ("the *mutual* cap clause"; "the indemnity that covers
8
+ *fraud*") -- `disambiguate`;
9
+ - **aggregate** across the contract ("every clause that waives *consequential* damages"; "all *governed_by*
10
+ facts") -- `aggregate_by_property`, `clauses_of_function`;
11
+ - serve the whole **per-contract KG** (each clause + its typed properties) -- `contract_clause_index`.
12
+
13
+ Every answer is CITED: each clause carries its `clause_id` and each property its edge provenance
14
+ (`confidence` EXTRACTED/INFERRED/AMBIGUOUS + `span_id`, whose doc offsets live on the span) -- no claim
15
+ without a citation (FR-Q.6). The store is duck-typed (the three `*_contract*` / `clauses_with_property`
16
+ readbacks) so this layer is unit-tested with no live ArcadeDB.
17
+
18
+ Out of scope here (need layers not in the clause KG): party<->clause ROLE questions (a per-clause party-role layer, Leg C -- not built; the PartyTo edge was retired,
19
+ issue 0028) and cross-clause REFERENCES (not extracted). Noted for a later pass.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from typing import Protocol, runtime_checkable
25
+
26
+ from pydantic import BaseModel
27
+
28
+
29
+ class CitedProperty(BaseModel):
30
+ """One typed property of a clause, with its edge provenance (FR-Q.6)."""
31
+
32
+ dimension: str
33
+ value: str
34
+ edge_type: str
35
+ predicate_iri: str = ""
36
+ confidence: str = ""
37
+ span_id: str = ""
38
+
39
+
40
+ class CitedClause(BaseModel):
41
+ """A clause in a contract with its typed properties, each cited."""
42
+
43
+ contract_id: str
44
+ clause_id: str
45
+ function: str
46
+ folio_iri: str = ""
47
+ span_id: str = "" # the clause's operative span (1:1); rehydrate a property-less clause from THIS, reliably
48
+ exception_of: str = "" # ADR-0044: if set, this clause is an INFERRED carve-out/exception to that cap clause_id
49
+ properties: list[CitedProperty] = []
50
+
51
+
52
+ @runtime_checkable
53
+ class _KGStore(Protocol):
54
+ def clauses_in_contract(self, contract_id: str) -> list[dict]: ...
55
+ def contract_clause_kg(self, contract_id: str) -> list[dict]: ...
56
+ def clauses_with_property(self, contract_id: str, dimension: str, value: str) -> list[dict]: ...
57
+ def all_spans_by_contract(self, contract_id: str) -> list[dict]: ... # 0006-D: untyped-span fallback
58
+
59
+
60
+ def _contract_of(clause_id: str) -> str:
61
+ return clause_id.split(":", 1)[0]
62
+
63
+
64
+ def _prop(row: dict) -> CitedProperty:
65
+ return CitedProperty(
66
+ dimension=row.get("dimension", ""), value=row.get("value", ""),
67
+ edge_type=row.get("edge_type", ""), predicate_iri=row.get("predicate_iri") or "",
68
+ confidence=row.get("confidence") or "", span_id=row.get("span_id") or "",
69
+ )
70
+
71
+
72
+ def contract_clause_index(
73
+ store: _KGStore, contract_id: str, *, include_untyped: bool = False
74
+ ) -> list[CitedClause]:
75
+ """The full per-contract KG: every clause (incl. property-less ones) with its typed properties, cited.
76
+
77
+ `include_untyped` (issue 0006-D): also serve the spans the classifier left UNTYPED (`function == NONE`),
78
+ which produce no Clause-KG node. Without this, a clause the classifier misses (e.g. a renewal clause it can't
79
+ map to the taxonomy) is ABSENT from what Leg-A serves -> a silent recall hole ("not_found" on a clause that
80
+ plainly exists). With it, an untyped span is served as a bare `CitedClause` (no properties), rehydrated from
81
+ its own `span_id` -- so recall is decoupled from classification (the ADR-0047 whole-index posture)."""
82
+ by_clause: dict[str, list[CitedProperty]] = {}
83
+ for row in store.contract_clause_kg(contract_id):
84
+ if not row.get("dimension"): # a non-property clause edge (e.g. IsExceptionTo, ADR-0044) -> not a fact
85
+ continue
86
+ by_clause.setdefault(row["clause_id"], []).append(_prop(row))
87
+ index = []
88
+ for c in store.clauses_in_contract(contract_id):
89
+ cid = c["clause_id"]
90
+ index.append(CitedClause(
91
+ contract_id=_contract_of(cid), clause_id=cid, function=c.get("function", ""),
92
+ folio_iri=c.get("folio_iri") or "", span_id=c.get("span_id") or "",
93
+ properties=by_clause.get(cid, []),
94
+ ))
95
+ if include_untyped:
96
+ covered = {c.span_id for c in index if c.span_id} # spans a typed clause already carries
97
+ for row in store.all_spans_by_contract(contract_id):
98
+ sid = row.get("span_id")
99
+ if sid and sid not in covered and (row.get("text") or "").strip():
100
+ index.append(CitedClause(
101
+ contract_id=contract_id, clause_id=sid, function=row.get("function") or "NONE",
102
+ span_id=sid, properties=[])) # bare; rehydrate_clause_texts fetches its text by function+span
103
+ return index
104
+
105
+
106
+ def clauses_of_function(store: _KGStore, contract_id: str, function: str) -> list[CitedClause]:
107
+ """Aggregation by clause type: all clauses of `function` in the contract, cited with their properties."""
108
+ return [c for c in contract_clause_index(store, contract_id) if c.function == function]
109
+
110
+
111
+ def disambiguate(
112
+ store: _KGStore, contract_id: str, function: str, dimension: str, value: str
113
+ ) -> list[CitedClause]:
114
+ """The same-type clauses in the contract that assert (dimension, value) -- e.g. among several
115
+ Cap-on-Liability clauses, the *mutual* one. The scoped-query answer classification can't give."""
116
+ matched = {r["clause_id"] for r in store.clauses_with_property(contract_id, dimension, value)}
117
+ return [c for c in clauses_of_function(store, contract_id, function) if c.clause_id in matched]
118
+
119
+
120
+ def aggregate_by_property(
121
+ store: _KGStore, contract_id: str, dimension: str, value: str
122
+ ) -> list[CitedClause]:
123
+ """Every clause in the contract asserting (dimension, value), regardless of type -- e.g. all clauses
124
+ that waive consequential damages, or every mutual obligation."""
125
+ matched = {r["clause_id"] for r in store.clauses_with_property(contract_id, dimension, value)}
126
+ by_id = {c.clause_id: c for c in contract_clause_index(store, contract_id)}
127
+ return [by_id[cid] for cid in matched if cid in by_id]
128
+
129
+
130
+ def grounded_only(clauses: list[CitedClause]) -> list[CitedClause]:
131
+ """Drop the AMBIGUOUS (grounding-judge-flagged) properties -- the high-precision view for a citation UI.
132
+ Returns copies keeping only EXTRACTED/INFERRED properties."""
133
+ return [
134
+ c.model_copy(update={"properties": [p for p in c.properties if p.confidence != "AMBIGUOUS"]})
135
+ for c in clauses
136
+ ]
137
+
138
+
139
+ def register_intra_document_scoped_query(registry) -> None:
140
+ """CAP-REG-2: register `intra_document_scoped_query` (function; intra-contract scoped KG serving)."""
141
+ registry.register(
142
+ "intra_document_scoped_query",
143
+ contract=CitedClause,
144
+ kind="function",
145
+ display_name="Intra-document scoped query",
146
+ )
147
+
148
+
149
+ def register_clause_disambiguation(registry) -> None:
150
+ """CAP-REG-2: register `clause_disambiguation` (function; disambiguation by typed property)."""
151
+ registry.register(
152
+ "clause_disambiguation",
153
+ contract=CitedClause,
154
+ kind="function",
155
+ display_name="Clause disambiguation",
156
+ )
@@ -0,0 +1,251 @@
1
+ """ADR-0117 DD-1b: the contract domain's store extension.
2
+
3
+ Composes a generic `Store` (`kg_write`/`kg_read`) to write the typed clause KG (KG-3), the legacy flat property
4
+ graph, and contract metadata -- so the engine store imports no contract contract. The clause/value/contract vertex
5
+ types + the dimension->edge map + predicate IRIs are pack-authoritative (`contract_bridge.ttl`, ADR-0067 P5a/P5b);
6
+ `kg_write` encodes each field per its declared storage type (e.g. `functions` as a JSON string). Domain-native
7
+ values in; the store owns all wire encoding."""
8
+ from __future__ import annotations
9
+
10
+ from typing import Optional
11
+
12
+ from rag_wright.capabilities.highlight_serve import _decode_bbox
13
+ from rag_wright.contracts.contract_meta import ContractRecord
14
+ from rag_wright.contracts.highlight import SpanLocation
15
+ from rag_wright.contracts.property import FOLIO_SUBJECT_IRI, ClausePropertyRecord
16
+ from rag_wright.store.arcadedb import (
17
+ CLAUSE_TYPE,
18
+ CONTRACT_TYPE,
19
+ IS_EXCEPTION_TO_EDGE_TYPE,
20
+ PROPERTY_EDGE_TYPE,
21
+ PROPVALUE_TYPE,
22
+ TYPED_PROPERTY_EDGE_TYPES,
23
+ _DIM_EDGE_STR,
24
+ _edge_predicate_iri,
25
+ _property_value_key,
26
+ )
27
+ from rag_wright.ontology.contract_taxonomy import AFFILIATE_OF, CONTRACTS_WITH # DD-5 contract edge names
28
+ from rag_wright.store.seam import NOT_NULL, KgEdge, KgNode
29
+
30
+
31
+ class ContractKGStore:
32
+ """Typed clause-KG + contract-metadata writes over a generic `Store`."""
33
+
34
+ def __init__(self, store) -> None:
35
+ self._store = store
36
+
37
+ def _already_written(self, clause_id: str) -> bool:
38
+ """Content-hash gate (FR-S.2): a committed `clause_id` embeds the content hash, so identical content ->
39
+ identical assertions -- skip (and the plain edge creates therefore run at most once per clause)."""
40
+ return bool(self._store.kg_read(CLAUSE_TYPE, fields=["clause_id"], where={"clause_id": clause_id}, limit=1))
41
+
42
+ def write_clause_kg(self, record: ClausePropertyRecord) -> None:
43
+ """KG-3 (ADR-0033): the Clause node + one shared PropertyValue node per (dimension,value) + one TYPED edge
44
+ per assertion (grounded with a predicate IRI + provenance). Idempotent by the content-hash gate."""
45
+ if self._already_written(record.clause_id):
46
+ return
47
+ functions = [{"function": f.function, "confidence": f.confidence.value} for f in record.functions]
48
+ nodes = [KgNode(CLAUSE_TYPE, "clause_id", {
49
+ "clause_id": record.clause_id, "function": record.function, "folio_iri": record.folio_iri,
50
+ "span_id": record.span_id, "functions": functions})]
51
+ edges = []
52
+ for a in record.assertions:
53
+ edge_type = _DIM_EDGE_STR[a.dimension.value]
54
+ key = _property_value_key(a.dimension.value, a.value)
55
+ nodes.append(KgNode(PROPVALUE_TYPE, "value_key", {
56
+ "value_key": key, "dimension": a.dimension.value, "value": a.value,
57
+ "folio_iri": FOLIO_SUBJECT_IRI.get(a.value, "")}))
58
+ edges.append(KgEdge(edge_type, CLAUSE_TYPE, "clause_id", record.clause_id, PROPVALUE_TYPE, "value_key",
59
+ key, {
60
+ "dimension": a.dimension.value, "predicate_iri": _edge_predicate_iri(edge_type),
61
+ "confidence": a.confidence.value, "span_id": a.span_id,
62
+ "chunk_id": str(a.provenance.chunk_id),
63
+ "source_doc_id": a.provenance.source_doc_id}))
64
+ self._store.kg_write(nodes, edges)
65
+
66
+ def write_property_graph(self, record: ClausePropertyRecord) -> None:
67
+ """Legacy flat property graph (ADR-0025/0026; superseded by `write_clause_kg`): the Clause node + shared
68
+ value nodes + one generic `HasProperty` edge per assertion. Same content-hash gate."""
69
+ if self._already_written(record.clause_id):
70
+ return
71
+ nodes = [KgNode(CLAUSE_TYPE, "clause_id", {
72
+ "clause_id": record.clause_id, "function": record.function, "folio_iri": record.folio_iri})]
73
+ edges = []
74
+ for a in record.assertions:
75
+ key = _property_value_key(a.dimension.value, a.value)
76
+ nodes.append(KgNode(PROPVALUE_TYPE, "value_key", {
77
+ "value_key": key, "dimension": a.dimension.value, "value": a.value,
78
+ "folio_iri": FOLIO_SUBJECT_IRI.get(a.value, "")}))
79
+ edges.append(KgEdge(PROPERTY_EDGE_TYPE, CLAUSE_TYPE, "clause_id", record.clause_id, PROPVALUE_TYPE,
80
+ "value_key", key, {
81
+ "confidence": a.confidence.value, "span_id": a.span_id,
82
+ "chunk_id": str(a.provenance.chunk_id),
83
+ "source_doc_id": a.provenance.source_doc_id}))
84
+ self._store.kg_write(nodes, edges)
85
+
86
+ def patch_canonical_jurisdictions(self) -> dict[str, int]:
87
+ """KG-5a (DD-1c): additively canonicalize the `jurisdiction` value nodes -- write a `canonical_value` on
88
+ each (surface `value` untouched, kept for citation), deterministically (no LLM, no re-extraction). Fixes the
89
+ retrieval-match loss from surface variants (England / England and Wales / English law). Idempotent; the
90
+ `canonical_value` property is pack-declared. Returns {seen, canonicalized}."""
91
+ from rag_wright.contracts.jurisdiction import canonicalize_jurisdiction
92
+
93
+ rows = self._store.kg_read(PROPVALUE_TYPE, fields=["value_key", "value"],
94
+ where={"dimension": "jurisdiction"})
95
+ nodes = []
96
+ for r in rows:
97
+ canon = canonicalize_jurisdiction(r["value"])
98
+ if canon:
99
+ nodes.append(KgNode(PROPVALUE_TYPE, "value_key",
100
+ {"value_key": r["value_key"], "canonical_value": canon}))
101
+ if nodes:
102
+ self._store.kg_write(nodes)
103
+ return {"seen": len(rows), "canonicalized": len(nodes)}
104
+
105
+ def upsert_contract(self, record: ContractRecord) -> None:
106
+ """CU-B3: upsert a contract's metadata by `contract_id`. `parties` -> native list (the store json-encodes
107
+ the `parties_json` STRING column); `page_count` may be None (-> null)."""
108
+ self._store.kg_write([KgNode(CONTRACT_TYPE, "contract_id", {
109
+ "contract_id": record.contract_id, "name": record.name, "agreement_type": record.agreement_type,
110
+ "parties_json": list(record.parties), "agreement_date": record.agreement_date,
111
+ "effective_date": record.effective_date, "source_doc_id": record.source_doc_id,
112
+ "content_hash": record.content_hash, "page_count": record.page_count})])
113
+
114
+ # --- EP-REF-1a-ii: the typed clause-KG edge-traversal READS, over the generic `kg_edges` primitive. These
115
+ # moved OFF the engine store (which must hold no domain traversal, ADR-0117); the store keeps only the
116
+ # generic kg_edges/kg_read. Shapes are byte-for-byte the old store methods' (acceptance: identical reads).
117
+
118
+ @staticmethod
119
+ def _bounds(contract_id: str) -> tuple[str, str]:
120
+ return contract_id + ":", contract_id + ";" # clause_ids are `<contract>:<idx>:<hash>`
121
+
122
+ def clause_typed_edges(self, clause_id: str) -> list[dict]:
123
+ """The clause's typed property edges: edge type, dimension, value, predicate IRI, and provenance."""
124
+ return self._store.kg_edges(
125
+ CLAUSE_TYPE, where={"clause_id": clause_id}, direction="out",
126
+ select={"edge_type": "e.@type", "predicate_iri": "e.predicate_iri", "dimension": "v.dimension",
127
+ "value": "v.value", "folio_iri": "v.folio_iri", "confidence": "e.confidence",
128
+ "span_id": "e.span_id"})
129
+
130
+ def contract_clause_kg(self, contract_id: str) -> list[dict]:
131
+ """The per-contract typed subgraph: one row per typed (clause -> value) edge, with the clause function,
132
+ edge type/dimension/value/predicate IRI, and provenance. Only PROPERTY edges (which carry a `dimension`);
133
+ the `dimension IS NOT NULL` guard excludes clause->clause edges (IsExceptionTo, ADR-0044)."""
134
+ lo, hi = self._bounds(contract_id)
135
+ return self._store.kg_edges(
136
+ CLAUSE_TYPE, key_range=("clause_id", lo, hi), direction="out", edge_where={"dimension": NOT_NULL},
137
+ select={"clause_id": "c.clause_id", "function": "c.function", "edge_type": "e.@type",
138
+ "dimension": "e.dimension", "value": "v.value", "folio_iri": "v.folio_iri",
139
+ "predicate_iri": "e.predicate_iri", "confidence": "e.confidence", "span_id": "e.span_id"})
140
+
141
+ def clauses_with_property(self, contract_id: str, dimension: str, value: str) -> list[dict]:
142
+ """The clauses in one contract that assert (dimension, value) -- clause_id + function + edge provenance."""
143
+ lo, hi = self._bounds(contract_id)
144
+ return self._store.kg_edges(
145
+ CLAUSE_TYPE, key_range=("clause_id", lo, hi), direction="out",
146
+ edge_where={"dimension": dimension}, target_where={"value": value},
147
+ select={"clause_id": "c.clause_id", "function": "c.function", "edge_type": "e.@type",
148
+ "confidence": "e.confidence", "span_id": "e.span_id"})
149
+
150
+ def exceptions_of_clause(self, cap_clause_id: str) -> list[dict]:
151
+ """The exception/carve-out clauses linked to a Cap clause (`IsExceptionTo` in-edges, ADR-0044): the SOURCE
152
+ clauses reached across the incoming edge. Rows: {clause_id, function, span_id}."""
153
+ return self._store.kg_edges(
154
+ CLAUSE_TYPE, where={"clause_id": cap_clause_id}, direction="in",
155
+ edge_type=IS_EXCEPTION_TO_EDGE_TYPE,
156
+ select={"clause_id": "v.clause_id", "function": "v.function", "span_id": "v.span_id"})
157
+
158
+ def span_properties(self, span_ids: list[str]) -> dict[str, set[tuple[str, str]]]:
159
+ """The typed (dimension, value) assertions on each span, joined via the ADR-0025 `edge.span_id`. Scans each
160
+ typed edge type once (IN-filtered), so round-trips are bounded by the edge-type count, not the pool size.
161
+ Returns {span_id: {(dimension, value)}} (every input span present, empty if none)."""
162
+ out: dict[str, set[tuple[str, str]]] = {s: set() for s in span_ids}
163
+ if not span_ids:
164
+ return out
165
+ for edge_type in TYPED_PROPERTY_EDGE_TYPES:
166
+ for r in self._store.kg_edges(
167
+ edge_type=edge_type, edge_where={"span_id": span_ids},
168
+ select={"span_id": "span_id", "dimension": "dimension", "value": "inV().value"}):
169
+ sid, dim, val = r.get("span_id"), r.get("dimension"), r.get("value")
170
+ if sid in out and dim and val is not None:
171
+ out[sid].add((str(dim), str(val)))
172
+ return out
173
+
174
+ def clauses_in_contract(self, contract_id: str) -> list[dict]:
175
+ """Every clause in one contract (a NODE read, delegated to the generic store -- it is a `kg_read`-
176
+ relocatable follow-up, not an edge traversal). Kept here so this extension is the complete `_KGStore`
177
+ reader the Leg-A serving (`contract_kg_serve`) needs."""
178
+ return self._store.clauses_in_contract(contract_id)
179
+
180
+ def all_spans_by_contract(self, contract_id: str) -> list[dict]:
181
+ """Every span in one contract (delegated to the generic store). The Leg-A serving
182
+ (`contract_clause_index(..., include_untyped=True)`) reads this so a span the classifier left untyped is
183
+ still a candidate -- without it, serve raised AttributeError and intra_document_qa abstained on EVERY doc."""
184
+ return self._store.all_spans_by_contract(contract_id)
185
+
186
+ # --- EP-REF-1b-ii: contract traversal + vocab (the reference reads EP-SEAM-3 lifts), over the entity-graph
187
+ # traversal primitive + the clause index. The CONTRACTS_WITH / AFFILIATE_OF naming is the DD-5 contract
188
+ # vocabulary over the generic (domain-free) graph_query.
189
+
190
+ def party_counterparties(self, entity_id: str, *, max_hops: int = 1,
191
+ documents: Optional[list[str]] = None) -> list:
192
+ """The parties this one has a CONTRACTS_WITH edge to, each citing the contract it came from. ONE hop by
193
+ default -- two hops would return the counterparties OF the counterparties (parties this one has no
194
+ agreement with), overstating exposure. `documents` scopes the traversal (every edge on a path must belong)."""
195
+ from rag_wright.capabilities.graph_query import graph_query
196
+ return list(graph_query(entity_id, store=self._store, relationship_type=CONTRACTS_WITH,
197
+ max_hops=max_hops, documents=documents).evidence)
198
+
199
+ def party_affiliates(self, entity_id: str, *, documents: Optional[list[str]] = None) -> list:
200
+ """The parties this one has an AFFILIATE_OF edge to (same corporate group). A SEPARATE traversal from
201
+ counterparties on purpose: a parent and its subsidiary are two legal persons whose obligations must not be
202
+ pooled, and the engine does not merge the entities -- the affiliate is its own node."""
203
+ from rag_wright.capabilities.graph_query import graph_query
204
+ return list(graph_query(entity_id, store=self._store, relationship_type=AFFILIATE_OF,
205
+ documents=documents).evidence)
206
+
207
+ def contract_terms(self, contract_id: str) -> list:
208
+ """The typed clauses of one contract -- the terms that create the exposure. The FULL view, NOT
209
+ grounded-only: the high-precision view drops AMBIGUOUS properties, including the verbatim out-of-vocab
210
+ values ADR-0102 kept, so an exposure/citation surface must read what the graph holds, not less (a
211
+ precision filter is a per-caller UI choice, never baked into the facade)."""
212
+ from rag_wright.capabilities.contract_kg_serve import contract_clause_index
213
+ return list(contract_clause_index(self, contract_id))
214
+
215
+ # --- EP-REF-1b-iii: clause-type taxonomy (pure, from contracts/function.py) + span location assembly ---
216
+
217
+ @staticmethod
218
+ def canonical_clause_type(label: str) -> Optional[str]:
219
+ """Map a user's clause label onto the engine's clause-type taxonomy, or `None` if it maps to nothing.
220
+ Case-insensitive + alias-resolving ("Limitation of Liability" -> "Cap On Liability"), so a caller's label
221
+ (e.g. a correction) is VALIDATED rather than taken on trust -- an unmappable label returns None, not a
222
+ silent empty scope."""
223
+ from rag_wright.contracts.function import canonical_function
224
+ return canonical_function(label)
225
+
226
+ @staticmethod
227
+ def clause_type_vocabulary() -> tuple[str, ...]:
228
+ """Every clause type a sweep/correction UI can be scoped to (the engine's function-label vocabulary)."""
229
+ from rag_wright.contracts.function import FUNCTION_LABELS
230
+ return tuple(FUNCTION_LABELS)
231
+
232
+ def span_locations(self, contract_id: str) -> list[SpanLocation]:
233
+ """Every span of one document with where it sits in the original (pages/bbox/offsets) + the clause ids
234
+ extracted from it -- for a citation PREVIEW (a span-id-in-hand lookup, no query/model call). Each clause
235
+ carries the `span_id` it came from (the only bridge between the clause-id and span-id spaces); a clause
236
+ whose span is not in this document is dropped rather than inventing a location."""
237
+ spans = self._store.all_spans_by_contract(contract_id)
238
+ present = {r["span_id"] for r in spans}
239
+ clauses: dict[str, list[str]] = {}
240
+ for c in self.clauses_in_contract(contract_id):
241
+ sid = c.get("span_id") or ""
242
+ if sid in present:
243
+ clauses.setdefault(sid, []).append(str(c.get("clause_id") or ""))
244
+ return [
245
+ SpanLocation(
246
+ span_id=r["span_id"], clause_ids=clauses.get(r["span_id"], []),
247
+ pages=[int(p) for p in (r.get("pages") or [])], bbox=_decode_bbox(r.get("bbox")),
248
+ doc_start=r.get("doc_start"), doc_end=r.get("doc_end"), text=r.get("text") or "",
249
+ )
250
+ for r in spans
251
+ ]