Conversation
8b2345c to
000bc8d
Compare
Establish a typed exception hierarchy and consistent error contract so integrators never see raw ValueError/Neo4j exceptions. VRE rolls back in-memory mutations and re-raises — integrators decide recovery strategy. Key changes: - New core/errors.py with VREError hierarchy (GraphError, PersistenceError, GraphIntegrityError, CyclicRelationshipError, HydrationError, ResolutionError, CandidateValidationError) - Fix silent override: CyclicRelationshipError in _persist_reachability now does rollback + re-raise instead of silently returning SKIPPED - Wrap all PrimitiveRepository methods with GraphError/PersistenceError - Wrap hydration with HydrationError, resolver with ResolutionError - Unknown cardinality defaults to None, firing all policies (safe default) - CyclicRelationshipError re-parented from ValueError to GraphIntegrityError Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
000bc8d to
6ea6544
Compare
There was a problem hiding this comment.
Pull request overview
Defines a typed, integrator-facing error contract across the VRE stack, ensuring engine/repository/grounding layers re-raise consistent VREError subclasses (instead of raw/untyped exceptions) and avoid silently overriding integrator decisions.
Changes:
- Introduces
vre.core.errorsexception hierarchy and re-exports error types fromvre.coreand top-levelvre. - Updates learning engine validation + cycle handling to raise typed exceptions and propagate
CyclicRelationshipError(with in-memory rollback). - Wraps Neo4j repository operations and hydration to prevent raw driver/JSON exceptions from leaking to integrators; updates policy cardinality handling to “safe default” behavior.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/vre/core/errors.py |
Adds new VRE exception hierarchy. |
src/vre/core/__init__.py |
Re-exports new error types from vre.core. |
src/vre/__init__.py |
Re-exports error types from top-level package; adjusts cardinality parsing to safe-default behavior. |
src/vre/core/graph.py |
Wraps Neo4jError at repository boundary and wraps hydration failures with HydrationError. |
src/vre/core/grounding/resolver.py |
Uses ResolutionError when spaCy model is missing. |
src/vre/learning/engine.py |
Replaces ValueError with CandidateValidationError; propagates CyclicRelationshipError after rollback. |
src/vre/core/policy/models.py |
Wraps policy callback dotted-path resolution failures as VREError. |
src/vre/core/policy/gate.py |
Allows cardinality=None to mean “unknown → do not skip any policies.” |
src/vre/core/models.py |
Removes CyclicRelationshipError from models (moved to errors module). |
tests/vre/test_learning.py |
Updates tests for typed learning validation errors and cycle propagation behavior. |
tests/vre/test_vre.py |
Updates tests for unknown-cardinality safe-default policy behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| try: | ||
| module = importlib.import_module(module_path) | ||
| return getattr(module, func_name) | ||
| except (ImportError, AttributeError) as exc: |
There was a problem hiding this comment.
resolve_callback() only wraps ImportError/AttributeError, but importlib.import_module(module_path) can raise ValueError when self.callback isn’t a valid dotted path (e.g. missing module part → empty module name). This would leak a raw ValueError to integrators. Validate that module_path/func_name are non-empty and/or include ValueError in the wrapped exceptions so all resolution failures surface as VREError.
| try: | |
| module = importlib.import_module(module_path) | |
| return getattr(module, func_name) | |
| except (ImportError, AttributeError) as exc: | |
| if not module_path or not func_name: | |
| raise VREError( | |
| f"Invalid policy callback path '{self.callback}': expected 'module.attr'" | |
| ) | |
| try: | |
| module = importlib.import_module(module_path) | |
| return getattr(module, func_name) | |
| except (ImportError, AttributeError, ValueError) as exc: |
| if (cardinality is not None | ||
| and policy.trigger_cardinality is not None | ||
| and policy.trigger_cardinality != cardinality): |
There was a problem hiding this comment.
The multi-line condition indentation here is inconsistent and may trigger Ruff/pycodestyle continuation-indentation violations (and it’s harder to read). Consider either keeping the condition on one line (it appears to fit within the 120 char limit) or re-indenting the continuation lines to align consistently under the opening parenthesis.
| if (cardinality is not None | |
| and policy.trigger_cardinality is not None | |
| and policy.trigger_cardinality != cardinality): | |
| if (cardinality is not None and | |
| policy.trigger_cardinality is not None and | |
| policy.trigger_cardinality != cardinality): |
| try: | ||
| import spacy | ||
| return spacy.load("en_core_web_sm") | ||
| except OSError: | ||
| raise RuntimeError( | ||
| raise ResolutionError( | ||
| "spaCy model not found. Run: python -m spacy download en_core_web_sm" | ||
| ) |
There was a problem hiding this comment.
This re-raises as ResolutionError but drops the original OSError context. Using exception chaining (raise ... from exc) would preserve the underlying error details for debugging while still enforcing the typed error contract.
| class PersistenceError(GraphError): | ||
| """A write operation against the graph backend failed.""" | ||
|
|
||
|
|
||
| class CyclicRelationshipError(GraphError): |
There was a problem hiding this comment.
CyclicRelationshipError is currently a GraphError, but it represents a graph integrity/constraint violation (cycle detection) rather than a backend/IO failure. This also diverges from the PR description’s hierarchy (mentions GraphIntegrityError with CyclicRelationshipError under it). Consider introducing GraphIntegrityError(VREError) and subclassing CyclicRelationshipError from that, so integrators can distinguish integrity errors from backend failures when catching exceptions.
| class PersistenceError(GraphError): | |
| """A write operation against the graph backend failed.""" | |
| class CyclicRelationshipError(GraphError): | |
| class GraphIntegrityError(VREError): | |
| """The graph violates an integrity or constraint rule.""" | |
| class PersistenceError(GraphError): | |
| """A write operation against the graph backend failed.""" | |
| class CyclicRelationshipError(GraphIntegrityError): |
Summary
Closes #42.
core/errors.py— typed exception hierarchy rooted atVREError. Integrators can catch at desired granularity (individual error type →VREErrorcatch-all).CyclicRelationshipErrorin_persist_reachabilitynow does rollback + re-raise instead of silently converting toSKIPPED. The engine cleans up its in-memory mutation; the integrator decides recovery strategy.PrimitiveRepositorymethods catchNeo4jErrorand raiseGraphError(reads) orPersistenceError(writes). Integrators never see raw Neo4j driver exceptions.HydrationErrorinstead of rawjson.JSONDecodeError/KeyError.None, which fires all policies. If we can't determine the cardinality, we can't justify skipping any policy.CyclicRelationshipErrorre-parented fromValueErrortoGraphIntegrityError(VREError). Breaking change — clean break, no backward-compat shim.Error hierarchy
Test plan
CyclicRelationshipErrorto propagate (notSKIPPED)BLOCKCandidateValidationErrorreplacesValueErrorin all learning engine validationImplementation plan
Issue #42: Engine-to-Integrator Error Contract
Context
The LearningEngine silently overrides integrator decisions when post-decision failures occur (
engine.py:279-283). More broadly, VRE lacks a consistent error contract — many errors propagate as rawValueErroror untyped exceptions, making it hard for integrators to handle them programmatically.Principle: VRE's responsibility is to roll back any in-memory mutations and re-raise errors with clear, typed exceptions. Integrators decide recovery strategy. VRE does not make recovery decisions on behalf of integrators.
Guard decorator: Unhandled exceptions propagating out of
vre_guardis acceptable — the integrator wraps the decorated function call in their own error handling.Part 1: Error Hierarchy — new
src/vre/core/errors.pyDedicated module for the exception hierarchy. Keeps
models.pyfocused on domain models.CyclicRelationshipErrormoves fromcore/models.pytocore/errors.py. Clean break — no re-export frommodels.py.ValueErrortoGraphIntegrityError(VREError). Breaking change — acceptable.from vre.core.errors import CyclicRelationshipError(orfrom vre import CyclicRelationshipError).Part 2: Fix the Silent Override
File:
src/vre/learning/engine.py:279-283Exception propagates through
learn_at→learn_all→ integrator. Thewith callback:context manager ensures__exit__fires for cleanup.Part 3: Wrap Critical Sections
3a. Repository boundary — Neo4j (
src/vre/core/graph.py)Wrap all public methods so integrators never see raw Neo4j exceptions. Use
GraphErrorfor reads,PersistenceErrorfor writes:save_primitive()PersistenceError(letCyclicRelationshipError+ provenanceValueErrorpass through)delete_primitive()PersistenceErrorfind_by_id()GraphErrorfind_by_name()GraphErrorlist_names()GraphErrorresolve_subgraph()GraphErrorensure_constraints()GraphError3b. Hydration (
src/vre/core/graph.py)Wrap
_hydrate_primitivewithHydrationErrorfor JSON/data corruption.3c. Concept resolution (
src/vre/core/grounding/resolver.py)Replace
RuntimeErrorwithResolutionErrorfor missing spaCy model.3d. Learning engine validation (
src/vre/learning/engine.py)Replace raw
ValueErrorwithCandidateValidationErrorin all_persist_*methods,_resolve_name_to_id, andlearn_at.3e. Policy callback resolution (
src/vre/core/policy/models.py)Wrap
resolve_callback()withVREErrorforImportError/AttributeError.3f. Cardinality — safe default on bad input
Default to
Noneon unknown cardinality. Update the gate to fire all policies when cardinality is unknown — if we can't determine cardinality, we can't justify skipping any policy.🤖 Generated with Claude Code