From 8faeb606d77718154af5ae03961dfe1b53542454 Mon Sep 17 00:00:00 2001 From: KaifAhmad1 Date: Mon, 2 Feb 2026 21:36:29 +0530 Subject: [PATCH] Fix(provenance): Resolve backward compatibility and metadata issues - Fix: Provide versioned source history in ProvenanceManager.track_entity to support correct get_all_sources behavior. - Fix: Ensure get_lineage aggregates and returns metadata fields correctly. - Fix: Update est_real_module_integration.py and est_semantic_extract_provenance.py to match correct rack_relationship API signature. - Verified: All provenance tests passed (237/237). --- semantica/kg/provenance_tracker.py | 2 +- semantica/provenance/manager.py | 51 +++++++++++++++++-- .../test_real_module_integration.py | 14 +++-- .../test_semantic_extract_provenance.py | 10 ++-- 4 files changed, 64 insertions(+), 13 deletions(-) diff --git a/semantica/kg/provenance_tracker.py b/semantica/kg/provenance_tracker.py index 0cb6b423..10770b5f 100644 --- a/semantica/kg/provenance_tracker.py +++ b/semantica/kg/provenance_tracker.py @@ -261,7 +261,7 @@ class ProvenanceTracker: "sources": self._unified_manager.get_all_sources(entity_id), "first_seen": lineage.get("first_seen"), "last_updated": lineage.get("last_updated"), - "metadata": {}, + "metadata": lineage.get("metadata", {}), # Include metadata from lineage "lineage_chain": lineage.get("lineage_chain", []) } return legacy_format diff --git a/semantica/provenance/manager.py b/semantica/provenance/manager.py index cdf575f0..035f9c41 100644 --- a/semantica/provenance/manager.py +++ b/semantica/provenance/manager.py @@ -100,9 +100,41 @@ class ProvenanceManager: ... metadata={"confidence": 0.92} ... ) """ + # Validate entity_id + if entity_id is None: + raise ValueError("entity_id cannot be None") + if not isinstance(entity_id, str): + raise TypeError(f"entity_id must be a string, got {type(entity_id).__name__}") + + if not isinstance(entity_id, str): + raise TypeError(f"entity_id must be a string, got {type(entity_id).__name__}") + # Check if entity already exists existing = self.storage.retrieve(entity_id) - + parent_id = kwargs.get("parent_entity_id") + + # If entity exists, preserve history by archiving the old state + if existing: + # Create a history entry for the previous state + # Use timestamp or counter for uniqueness + import copy + history_entry = copy.deepcopy(existing) + history_id = f"{entity_id}:v:{existing.last_updated}" + + # Ensure unique ID if update happens same second + if self.storage.retrieve(history_id): + history_id = f"{history_id}:{datetime.utcnow().microsecond}" + + history_entry.entity_id = history_id + + # Store the history entry + try: + self.storage.store(history_entry) + # Link new entry to this history entry + parent_id = history_id + except Exception: + pass # If history archiving fails, proceed with update but lose history (graceful degradation) + entry = ProvenanceEntry( entity_id=entity_id, entity_type=kwargs.get("entity_type", "entity"), @@ -113,7 +145,8 @@ class ProvenanceManager: confidence=kwargs.get("confidence", 1.0), metadata=metadata or {}, first_seen=existing.first_seen if existing else datetime.utcnow().isoformat(), - last_updated=datetime.utcnow().isoformat() + last_updated=datetime.utcnow().isoformat(), + parent_entity_id=parent_id # Link to history or explicit parent ) # Compute checksum for integrity @@ -391,18 +424,27 @@ class ProvenanceManager: entity_id: Entity identifier Returns: - Dictionary containing lineage information + Dictionary containing lineage information including metadata Example: >>> lineage = prov_mgr.get_lineage("entity_1") >>> print(lineage["source_documents"]) ['DOI:10.1371/...', 'doc_2'] + >>> print(lineage["metadata"]) + {'text': 'Apple Inc.', 'label': 'ORG'} """ lineage_entries = self.storage.trace_lineage(entity_id) if not lineage_entries: return {} + # Aggregate metadata from all lineage entries + # Most recent entry's metadata takes precedence + aggregated_metadata = {} + for entry in lineage_entries: + if entry.metadata: + aggregated_metadata.update(entry.metadata) + return { "entity_id": entity_id, "lineage_chain": [entry.to_dict() for entry in lineage_entries], @@ -418,7 +460,8 @@ class ProvenanceManager: (e.last_updated for e in lineage_entries if e.last_updated), default=None ), - "entity_count": len(lineage_entries) + "entity_count": len(lineage_entries), + "metadata": aggregated_metadata # Add metadata key } def trace_lineage(self, entity_id: str) -> List[ProvenanceEntry]: diff --git a/tests/provenance/test_real_module_integration.py b/tests/provenance/test_real_module_integration.py index de4825b1..bfd5feff 100644 --- a/tests/provenance/test_real_module_integration.py +++ b/tests/provenance/test_real_module_integration.py @@ -155,7 +155,7 @@ class TestRealModuleIntegration: for i in range(len(sources)): lineage = manager.get_lineage(f"entity_from_{i}") assert lineage is not None - assert lineage["source"] == sources[i] + assert sources[i] in lineage["source_documents"] def test_property_source_tracking(self): """Test tracking property sources for entities.""" @@ -227,7 +227,7 @@ class TestRealModuleIntegration: assert lineage_v1 is not None assert lineage_v2 is not None - assert lineage_v1["timestamp"] < lineage_v2["timestamp"] + assert lineage_v1["first_seen"] < lineage_v2["first_seen"] def test_batch_operations_with_provenance(self): """Test batch operations maintain provenance.""" @@ -280,9 +280,13 @@ class TestRealModuleIntegration: manager = ProvenanceManager() # Track some data - manager.track_entity("e1", "src1", "type1", metadata={"key": "value"}) - manager.track_entity("e2", "src2", "type2") - manager.track_relationship("r1", "src1", "e1", "relates", "e2") + manager.track_entity("e1", "src1", entity_type="type1", metadata={"key": "value"}) + manager.track_entity("e2", "src2", entity_type="type2") + manager.track_relationship( + relationship_id="r1", + source="src1", + metadata={"subject": "e1", "predicate": "relates", "object": "e2"} + ) # Get statistics stats = manager.get_statistics() diff --git a/tests/provenance/test_semantic_extract_provenance.py b/tests/provenance/test_semantic_extract_provenance.py index 5f4fe9c8..7f49a40a 100644 --- a/tests/provenance/test_semantic_extract_provenance.py +++ b/tests/provenance/test_semantic_extract_provenance.py @@ -383,9 +383,13 @@ class TestRealProvenanceTracking: manager = ProvenanceManager() # Track various items - manager.track_entity("e1", "src1", "type1") - manager.track_entity("e2", "src2", "type2") - manager.track_relationship("r1", "src1", "e1", "relates", "e2") + manager.track_entity("e1", "src1", entity_type="type1") + manager.track_entity("e2", "src2", entity_type="type2") + manager.track_relationship( + relationship_id="r1", + source="src1", + metadata={"subject": "e1", "predicate": "relates", "object": "e2"} + ) # Get statistics stats = manager.get_statistics()