vre 0.4.3__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.
- vre/__init__.py +440 -0
- vre/core/__init__.py +32 -0
- vre/core/errors.py +49 -0
- vre/core/graph.py +678 -0
- vre/core/grounding/__init__.py +13 -0
- vre/core/grounding/engine.py +377 -0
- vre/core/grounding/models.py +182 -0
- vre/core/grounding/resolver.py +116 -0
- vre/core/models.py +260 -0
- vre/core/policy/__init__.py +22 -0
- vre/core/policy/callback.py +74 -0
- vre/core/policy/gate.py +90 -0
- vre/core/policy/models.py +128 -0
- vre/core/policy/wizard.py +297 -0
- vre/guard.py +147 -0
- vre/identity/__init__.py +10 -0
- vre/identity/models.py +25 -0
- vre/identity/registry.py +124 -0
- vre/learning/__init__.py +28 -0
- vre/learning/callback.py +81 -0
- vre/learning/engine.py +404 -0
- vre/learning/models.py +141 -0
- vre/learning/templates.py +46 -0
- vre/tracing.py +93 -0
- vre-0.4.3.dist-info/METADATA +958 -0
- vre-0.4.3.dist-info/RECORD +28 -0
- vre-0.4.3.dist-info/WHEEL +4 -0
- vre-0.4.3.dist-info/licenses/LICENSE +202 -0
vre/__init__.py
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
# Copyright 2026 Andrew Greene
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Volute Reasoning Engine — decorator-based epistemic enforcement.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
from vre import VRE
|
|
10
|
+
from vre.core.graph import PrimitiveRepository
|
|
11
|
+
|
|
12
|
+
repo = PrimitiveRepository("neo4j://localhost:7687", "neo4j", "password")
|
|
13
|
+
vre = VRE(repo)
|
|
14
|
+
result = vre.check(["file", "write"])
|
|
15
|
+
print(result.grounded, result.resolved)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Callable
|
|
22
|
+
from uuid import UUID
|
|
23
|
+
|
|
24
|
+
from vre.core.graph import PrimitiveRepository
|
|
25
|
+
from vre.core.grounding import ConceptResolver, GroundingEngine, GroundingResult
|
|
26
|
+
from vre.core.errors import (
|
|
27
|
+
CandidateValidationError,
|
|
28
|
+
CyclicRelationshipError,
|
|
29
|
+
GraphError,
|
|
30
|
+
GraphIntegrityError,
|
|
31
|
+
HydrationError,
|
|
32
|
+
PersistenceError,
|
|
33
|
+
RegistryError,
|
|
34
|
+
ResolutionError,
|
|
35
|
+
VREError,
|
|
36
|
+
)
|
|
37
|
+
from vre.core.models import (
|
|
38
|
+
DepthGap,
|
|
39
|
+
DepthLevel,
|
|
40
|
+
ExistenceGap,
|
|
41
|
+
PrimitiveMetrics,
|
|
42
|
+
Provenance,
|
|
43
|
+
ProvenanceSource,
|
|
44
|
+
ReachabilityGap,
|
|
45
|
+
RelationalGap,
|
|
46
|
+
)
|
|
47
|
+
from vre.core.policy import Cardinality, PolicyAction, PolicyCallbackResult, PolicyResult, PolicyViolation
|
|
48
|
+
from vre.core.policy.callback import PolicyCallContext
|
|
49
|
+
from vre.core.policy.gate import PolicyGate
|
|
50
|
+
from vre.identity import AgentIdentity, AgentRegistry
|
|
51
|
+
from vre.learning import (
|
|
52
|
+
CandidateDecision,
|
|
53
|
+
LearningCallback,
|
|
54
|
+
LearningCandidate,
|
|
55
|
+
LearningEngine,
|
|
56
|
+
LearningResult,
|
|
57
|
+
)
|
|
58
|
+
from vre.tracing import TraceWriter, build_trace_entry
|
|
59
|
+
|
|
60
|
+
logging.getLogger("vre").addHandler(logging.NullHandler())
|
|
61
|
+
|
|
62
|
+
logger = logging.getLogger(__name__)
|
|
63
|
+
|
|
64
|
+
__all__ = [
|
|
65
|
+
"VRE",
|
|
66
|
+
"AgentIdentity",
|
|
67
|
+
"AgentRegistry",
|
|
68
|
+
"CandidateValidationError",
|
|
69
|
+
"CyclicRelationshipError",
|
|
70
|
+
"GraphError",
|
|
71
|
+
"GraphIntegrityError",
|
|
72
|
+
"HydrationError",
|
|
73
|
+
"PersistenceError",
|
|
74
|
+
"RegistryError",
|
|
75
|
+
"ResolutionError",
|
|
76
|
+
"VREError",
|
|
77
|
+
"PrimitiveRepository",
|
|
78
|
+
"ConceptResolver",
|
|
79
|
+
"GroundingEngine",
|
|
80
|
+
"GroundingResult",
|
|
81
|
+
"DepthLevel",
|
|
82
|
+
"PrimitiveMetrics",
|
|
83
|
+
"Provenance",
|
|
84
|
+
"ProvenanceSource",
|
|
85
|
+
"Cardinality",
|
|
86
|
+
"PolicyAction",
|
|
87
|
+
"PolicyCallbackResult",
|
|
88
|
+
"PolicyResult",
|
|
89
|
+
"PolicyViolation",
|
|
90
|
+
"PolicyCallContext",
|
|
91
|
+
"PolicyGate",
|
|
92
|
+
"CandidateDecision",
|
|
93
|
+
"LearningCallback",
|
|
94
|
+
"LearningCandidate",
|
|
95
|
+
"LearningEngine",
|
|
96
|
+
"LearningResult",
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class VRE:
|
|
101
|
+
"""
|
|
102
|
+
Volute Reasoning Engine — public interface.
|
|
103
|
+
|
|
104
|
+
Wraps ConceptResolver and GroundingEngine. Depth requirements are
|
|
105
|
+
derived from graph structure; an optional min_depth override lets
|
|
106
|
+
integrators enforce a stricter floor.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(
|
|
110
|
+
self,
|
|
111
|
+
repository: PrimitiveRepository,
|
|
112
|
+
agent_key: str | None = None,
|
|
113
|
+
agent_name: str | None = None,
|
|
114
|
+
registry_path: str | Path | None = None,
|
|
115
|
+
persist_traces: bool = True,
|
|
116
|
+
) -> None:
|
|
117
|
+
"""
|
|
118
|
+
Initialize VRE with the given primitive repository.
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
repository:
|
|
123
|
+
The Neo4j primitive repository for graph operations.
|
|
124
|
+
agent_key:
|
|
125
|
+
Optional registration key for agent identity. When provided,
|
|
126
|
+
the key is resolved via the persisted registry to a stable
|
|
127
|
+
AgentIdentity whose `agent_id` is stamped on every
|
|
128
|
+
GroundingResult produced by this instance.
|
|
129
|
+
agent_name:
|
|
130
|
+
Optional human-readable name for the agent. Only used on
|
|
131
|
+
first registration; ignored on subsequent calls with the
|
|
132
|
+
same `agent_key`.
|
|
133
|
+
registry_path:
|
|
134
|
+
Path to the agent registry JSON file. Defaults to
|
|
135
|
+
`AgentRegistry`'s built-in default when None.
|
|
136
|
+
persist_traces:
|
|
137
|
+
When True (default), grounding traces are persisted to daily
|
|
138
|
+
JSONL files under `~/.vre/traces/`.
|
|
139
|
+
"""
|
|
140
|
+
self._repo = repository
|
|
141
|
+
self._resolver = ConceptResolver(repository)
|
|
142
|
+
self._engine = GroundingEngine(repository)
|
|
143
|
+
self._learning_engine = LearningEngine(repository)
|
|
144
|
+
|
|
145
|
+
if agent_key is not None:
|
|
146
|
+
self._identity: AgentIdentity | None = AgentRegistry(registry_path).get_or_create(agent_key, name=agent_name)
|
|
147
|
+
else:
|
|
148
|
+
self._identity = None
|
|
149
|
+
|
|
150
|
+
self._suppress_trace = False
|
|
151
|
+
self._trace_writer: TraceWriter | None = TraceWriter() if persist_traces else None
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def identity(self) -> AgentIdentity | None:
|
|
155
|
+
"""
|
|
156
|
+
The agent identity associated with this VRE instance, if any.
|
|
157
|
+
"""
|
|
158
|
+
return self._identity
|
|
159
|
+
|
|
160
|
+
def _stamp_identity(self, result: GroundingResult) -> GroundingResult:
|
|
161
|
+
"""
|
|
162
|
+
Set `agent_id` on the result if this instance has an identity and the result doesn't already have one.
|
|
163
|
+
"""
|
|
164
|
+
if self._identity is not None and result.agent_id is None:
|
|
165
|
+
result.agent_id = self._identity.agent_id
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def _gap_primitive_ids(gaps: list) -> set[UUID]:
|
|
170
|
+
"""
|
|
171
|
+
Collect the set of primitive UUIDs that are epistemically deficient.
|
|
172
|
+
|
|
173
|
+
RelationalGaps contribute only the target ID — the relationship
|
|
174
|
+
is a pure structural declaration and the source is epistemically
|
|
175
|
+
sound if the edge is visible. The failure belongs to the target
|
|
176
|
+
that lacks the required depth.
|
|
177
|
+
"""
|
|
178
|
+
ids: set[UUID] = set()
|
|
179
|
+
for gap in gaps:
|
|
180
|
+
if gap.kind == "RELATIONAL":
|
|
181
|
+
ids.add(gap.target.id)
|
|
182
|
+
else:
|
|
183
|
+
ids.add(gap.primitive.id)
|
|
184
|
+
return ids
|
|
185
|
+
|
|
186
|
+
def _update_grounding_metrics(self, result: GroundingResult) -> None:
|
|
187
|
+
"""
|
|
188
|
+
Update per-primitive grounding metrics after a `check` call.
|
|
189
|
+
|
|
190
|
+
Batch-reads current metrics for all resolved root concepts, computes
|
|
191
|
+
increments in-process, and batch-writes the results. Updates are
|
|
192
|
+
best-effort — failures are logged but never block the caller.
|
|
193
|
+
"""
|
|
194
|
+
if not result.trace:
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
now = datetime.now(timezone.utc)
|
|
198
|
+
gap_ids = self._gap_primitive_ids(result.gaps)
|
|
199
|
+
resolved_lower = {r.lower() for r in result.resolved}
|
|
200
|
+
|
|
201
|
+
target_prims = [
|
|
202
|
+
prim for prim in result.trace.result.primitives
|
|
203
|
+
if prim.name.lower() in resolved_lower
|
|
204
|
+
]
|
|
205
|
+
if not target_prims:
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
target_ids = [p.id for p in target_prims]
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
current_metrics = self._repo.batch_read_metrics(target_ids)
|
|
212
|
+
except Exception:
|
|
213
|
+
logger.warning("Failed to batch-read metrics", exc_info=True)
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
updates: dict[UUID, PrimitiveMetrics] = {}
|
|
217
|
+
for prim in target_prims:
|
|
218
|
+
if prim.id not in current_metrics:
|
|
219
|
+
continue
|
|
220
|
+
metrics = current_metrics[prim.id] or PrimitiveMetrics()
|
|
221
|
+
|
|
222
|
+
if prim.id in gap_ids:
|
|
223
|
+
metrics.failure_count += 1
|
|
224
|
+
metrics.last_failed = now
|
|
225
|
+
else:
|
|
226
|
+
metrics.grounding_count += 1
|
|
227
|
+
metrics.last_grounded = now
|
|
228
|
+
|
|
229
|
+
updates[prim.id] = metrics
|
|
230
|
+
|
|
231
|
+
if updates:
|
|
232
|
+
try:
|
|
233
|
+
self._repo.batch_update_metrics(updates)
|
|
234
|
+
except Exception:
|
|
235
|
+
logger.warning("Failed to batch-update metrics for %d primitives", len(updates), exc_info=True)
|
|
236
|
+
|
|
237
|
+
def _update_learning_metric(
|
|
238
|
+
self,
|
|
239
|
+
gap: DepthGap | ExistenceGap | RelationalGap | ReachabilityGap,
|
|
240
|
+
decision: CandidateDecision,
|
|
241
|
+
) -> None:
|
|
242
|
+
"""
|
|
243
|
+
Update learning metrics on the primitive targeted by a gap.
|
|
244
|
+
|
|
245
|
+
Increments `learning_count` for accepted/modified decisions and
|
|
246
|
+
`rejection_count` for rejected decisions. Skipped decisions are
|
|
247
|
+
ignored. Looks up the primitive by ID first, falling back to name
|
|
248
|
+
for ExistenceGaps where the gap carries a transient ID.
|
|
249
|
+
"""
|
|
250
|
+
if decision == CandidateDecision.SKIPPED:
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
if isinstance(gap, RelationalGap):
|
|
254
|
+
prim_id, prim_name = gap.target.id, gap.target.name
|
|
255
|
+
elif isinstance(gap, (DepthGap, ExistenceGap, ReachabilityGap)):
|
|
256
|
+
prim_id, prim_name = gap.primitive.id, gap.primitive.name
|
|
257
|
+
else:
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
found = self._repo.find_by_id(prim_id)
|
|
261
|
+
if found is None:
|
|
262
|
+
found = self._repo.find_by_name(prim_name)
|
|
263
|
+
if found is None:
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
metrics = found.metrics or PrimitiveMetrics()
|
|
267
|
+
|
|
268
|
+
if decision in (CandidateDecision.ACCEPTED, CandidateDecision.MODIFIED):
|
|
269
|
+
metrics.learning_count += 1
|
|
270
|
+
elif decision == CandidateDecision.REJECTED:
|
|
271
|
+
metrics.rejection_count += 1
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
self._repo.update_metrics(found.id, metrics)
|
|
275
|
+
except Exception:
|
|
276
|
+
logger.warning("Failed to update learning metrics for %r", prim_name, exc_info=True)
|
|
277
|
+
|
|
278
|
+
def resolve(self, concepts: list[str]) -> list[str]:
|
|
279
|
+
"""
|
|
280
|
+
Resolve free-form concept names to canonical primitive names.
|
|
281
|
+
"""
|
|
282
|
+
return self._resolver.resolve(concepts)
|
|
283
|
+
|
|
284
|
+
def check(
|
|
285
|
+
self,
|
|
286
|
+
concepts: list[str],
|
|
287
|
+
min_depth: DepthLevel | None = None,
|
|
288
|
+
) -> GroundingResult:
|
|
289
|
+
"""
|
|
290
|
+
Ground concepts with graph-derived depth gating.
|
|
291
|
+
|
|
292
|
+
Returns a GroundingResult with grounded=True only when all resolved
|
|
293
|
+
concepts are fully grounded with no gaps.
|
|
294
|
+
|
|
295
|
+
Parameters
|
|
296
|
+
----------
|
|
297
|
+
concepts:
|
|
298
|
+
List of free-form concept names to ground.
|
|
299
|
+
min_depth:
|
|
300
|
+
Optional integrator override — enforces a minimum depth floor
|
|
301
|
+
on all root primitives. Can only raise the floor, never lower it.
|
|
302
|
+
"""
|
|
303
|
+
result = self._stamp_identity(self._engine.ground(concepts, self._resolver, min_depth=min_depth))
|
|
304
|
+
self._update_grounding_metrics(result)
|
|
305
|
+
if self._trace_writer is not None and not self._suppress_trace:
|
|
306
|
+
try:
|
|
307
|
+
self._trace_writer.write(build_trace_entry("check", concepts, result))
|
|
308
|
+
except Exception:
|
|
309
|
+
logger.warning("Failed to persist trace for check()", exc_info=True)
|
|
310
|
+
return result
|
|
311
|
+
|
|
312
|
+
def learn_all(
|
|
313
|
+
self,
|
|
314
|
+
grounding: GroundingResult,
|
|
315
|
+
callback: LearningCallback,
|
|
316
|
+
concepts: list[str],
|
|
317
|
+
min_depth: DepthLevel | None = None,
|
|
318
|
+
) -> GroundingResult:
|
|
319
|
+
"""
|
|
320
|
+
Iteratively resolve all gaps via the learning loop.
|
|
321
|
+
|
|
322
|
+
Processes one gap at a time, re-grounding after each accepted/modified
|
|
323
|
+
candidate. Skipped gaps are excluded from subsequent rounds (the user
|
|
324
|
+
has acknowledged them). Rejected gaps stop the loop entirely.
|
|
325
|
+
Returns the final GroundingResult.
|
|
326
|
+
"""
|
|
327
|
+
skipped: set[int] = set()
|
|
328
|
+
learning_outcomes: list[LearningResult] = []
|
|
329
|
+
self._suppress_trace = True
|
|
330
|
+
try:
|
|
331
|
+
with callback:
|
|
332
|
+
while not grounding.grounded and grounding.gaps:
|
|
333
|
+
gap_index = next(
|
|
334
|
+
(i for i, g in enumerate(grounding.gaps) if i not in skipped),
|
|
335
|
+
None,
|
|
336
|
+
)
|
|
337
|
+
if gap_index is None:
|
|
338
|
+
break
|
|
339
|
+
result = self._learning_engine.learn_at(grounding, gap_index, callback)
|
|
340
|
+
learning_outcomes.append(result)
|
|
341
|
+
self._update_learning_metric(grounding.gaps[gap_index], result.decision)
|
|
342
|
+
if result.decision == CandidateDecision.REJECTED:
|
|
343
|
+
break
|
|
344
|
+
if result.decision == CandidateDecision.SKIPPED:
|
|
345
|
+
skipped.add(gap_index)
|
|
346
|
+
continue
|
|
347
|
+
self._resolver.invalidate()
|
|
348
|
+
grounding = self.check(concepts, min_depth=min_depth)
|
|
349
|
+
skipped.clear()
|
|
350
|
+
finally:
|
|
351
|
+
self._suppress_trace = False
|
|
352
|
+
|
|
353
|
+
if self._trace_writer is not None and learning_outcomes:
|
|
354
|
+
try:
|
|
355
|
+
self._trace_writer.write(
|
|
356
|
+
build_trace_entry("learn", concepts, grounding, learning_outcomes)
|
|
357
|
+
)
|
|
358
|
+
except Exception:
|
|
359
|
+
logger.warning("Failed to persist trace for learn_all()", exc_info=True)
|
|
360
|
+
|
|
361
|
+
return grounding
|
|
362
|
+
|
|
363
|
+
def check_policy(
|
|
364
|
+
self,
|
|
365
|
+
concepts: list[str] | GroundingResult,
|
|
366
|
+
cardinality: str | None = None,
|
|
367
|
+
call_context: PolicyCallContext | None = None,
|
|
368
|
+
on_policy: Callable[[list[PolicyViolation]], bool] | None = None,
|
|
369
|
+
) -> PolicyResult:
|
|
370
|
+
"""
|
|
371
|
+
Evaluate policies for the given concepts.
|
|
372
|
+
|
|
373
|
+
`concepts` may be a list of concept names (grounding is run) or a
|
|
374
|
+
pre-computed `GroundingResult` (grounding is skipped).
|
|
375
|
+
|
|
376
|
+
`call_context` carries the tool name, grounding result, and the args/
|
|
377
|
+
kwargs of the decorated function so that policy callbacks can make
|
|
378
|
+
domain-specific decisions. Omit when calling outside a guarded context.
|
|
379
|
+
|
|
380
|
+
`on_policy` is an optional handler consulted when any violation has
|
|
381
|
+
`requires_confirmation=True`. It receives all violations and returns
|
|
382
|
+
a single bool — True to proceed, False to block. When absent, the
|
|
383
|
+
fail-safe is BLOCK.
|
|
384
|
+
|
|
385
|
+
Returns PolicyResult with action PASS or BLOCK (never PENDING).
|
|
386
|
+
"""
|
|
387
|
+
if isinstance(concepts, GroundingResult):
|
|
388
|
+
grounding = concepts
|
|
389
|
+
else:
|
|
390
|
+
grounding = self._stamp_identity(self._engine.ground(concepts, self._resolver))
|
|
391
|
+
|
|
392
|
+
if grounding.trace is None:
|
|
393
|
+
return PolicyResult(action=PolicyAction.PASS)
|
|
394
|
+
|
|
395
|
+
card_enum: Cardinality | None = None
|
|
396
|
+
if cardinality is not None:
|
|
397
|
+
try:
|
|
398
|
+
card_enum = Cardinality(cardinality)
|
|
399
|
+
except ValueError:
|
|
400
|
+
card_enum = None # unknown → fire all policies
|
|
401
|
+
|
|
402
|
+
gate = PolicyGate()
|
|
403
|
+
violations = gate.evaluate(grounding.trace, card_enum, call_context)
|
|
404
|
+
|
|
405
|
+
if not violations:
|
|
406
|
+
policy_result = PolicyResult(action=PolicyAction.PASS)
|
|
407
|
+
else:
|
|
408
|
+
hard_blocks = [v for v in violations if not v.requires_confirmation]
|
|
409
|
+
pending = [v for v in violations if v.requires_confirmation]
|
|
410
|
+
|
|
411
|
+
# Hard blocks do not consult on_policy — they are immediate BLOCKs with their own messages
|
|
412
|
+
if hard_blocks:
|
|
413
|
+
messages = "; ".join(v.message for v in hard_blocks)
|
|
414
|
+
policy_result = PolicyResult(
|
|
415
|
+
action=PolicyAction.BLOCK,
|
|
416
|
+
reason=messages,
|
|
417
|
+
violations=violations,
|
|
418
|
+
)
|
|
419
|
+
else:
|
|
420
|
+
# Only confirmation-required violations remain — consult on_policy
|
|
421
|
+
if on_policy is not None:
|
|
422
|
+
if on_policy(pending):
|
|
423
|
+
policy_result = PolicyResult(
|
|
424
|
+
action=PolicyAction.PASS,
|
|
425
|
+
violations=pending,
|
|
426
|
+
)
|
|
427
|
+
else:
|
|
428
|
+
policy_result = PolicyResult(
|
|
429
|
+
action=PolicyAction.BLOCK,
|
|
430
|
+
reason="User declined",
|
|
431
|
+
violations=pending,
|
|
432
|
+
)
|
|
433
|
+
else:
|
|
434
|
+
policy_result = PolicyResult(
|
|
435
|
+
action=PolicyAction.BLOCK,
|
|
436
|
+
reason="Confirmation required, no handler",
|
|
437
|
+
violations=pending,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
return policy_result
|
vre/core/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Copyright 2026 Andrew Greene
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
from vre.core.errors import (
|
|
5
|
+
CandidateValidationError,
|
|
6
|
+
CyclicRelationshipError,
|
|
7
|
+
GraphError,
|
|
8
|
+
GraphIntegrityError,
|
|
9
|
+
HydrationError,
|
|
10
|
+
PersistenceError,
|
|
11
|
+
ResolutionError,
|
|
12
|
+
VREError,
|
|
13
|
+
)
|
|
14
|
+
from vre.core.models import (
|
|
15
|
+
Provenance,
|
|
16
|
+
ProvenanceSource,
|
|
17
|
+
TRANSITIVE_RELATION_TYPES,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"CandidateValidationError",
|
|
22
|
+
"CyclicRelationshipError",
|
|
23
|
+
"GraphError",
|
|
24
|
+
"GraphIntegrityError",
|
|
25
|
+
"HydrationError",
|
|
26
|
+
"PersistenceError",
|
|
27
|
+
"Provenance",
|
|
28
|
+
"ProvenanceSource",
|
|
29
|
+
"ResolutionError",
|
|
30
|
+
"TRANSITIVE_RELATION_TYPES",
|
|
31
|
+
"VREError",
|
|
32
|
+
]
|
vre/core/errors.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Copyright 2026 Andrew Greene
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
VRE exception hierarchy.
|
|
6
|
+
|
|
7
|
+
All VRE-specific exceptions derive from VREError so integrators can catch
|
|
8
|
+
at the desired granularity — from a single error type up to the entire
|
|
9
|
+
framework.
|
|
10
|
+
|
|
11
|
+
VRE's responsibility is to roll back any in-memory mutations and re-raise
|
|
12
|
+
errors with clear, typed exceptions. Integrators decide recovery strategy.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class VREError(Exception):
|
|
17
|
+
"""Base exception for all VRE errors."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GraphError(VREError):
|
|
21
|
+
"""A graph backend operation failed (read or write)."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PersistenceError(GraphError):
|
|
25
|
+
"""A write operation against the graph backend failed."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class GraphIntegrityError(VREError):
|
|
29
|
+
"""A graph operation would violate structural integrity constraints."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CyclicRelationshipError(GraphIntegrityError):
|
|
33
|
+
"""An edge would create a cycle on transitive relationship types."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class HydrationError(VREError):
|
|
37
|
+
"""Failed to reconstruct a domain object from stored data."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ResolutionError(VREError):
|
|
41
|
+
"""Failed to resolve a concept name or identifier."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CandidateValidationError(VREError):
|
|
45
|
+
"""A learning candidate is missing required fields or references invalid data."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RegistryError(VREError):
|
|
49
|
+
"""A file-based registry operation failed (read, write, or corruption)."""
|