From 12d61b92dfc28f4f809929dff2b6f56fbc956984 Mon Sep 17 00:00:00 2001
From: Mohd Kaif <98801504+KaifAhmad1@users.noreply.github.com>
Date: Thu, 18 Jun 2026 13:37:24 +0530
Subject: [PATCH] docs: add Temporal & Distance Intelligence reference pages
with accurate API (#650)
- Add docs/reference/temporal.md: full Temporal Intelligence reference covering
bi-temporal model (TemporalBound.OPEN sentinel, BiTemporalFact.from_relationship()
factory), TemporalGraphQuery (query_at_time, reconstruct_at_time, query_time_range,
find_temporal_paths, analyze_evolution, validate_temporal_consistency),
TemporalPatternDetector, TemporalReasoningEngine with all 13 Allen interval
relations over TemporalInterval objects, TemporalNormalizer (returns
Optional[Tuple[datetime, datetime]]), TemporalQueryRewriter.rewrite() returning
TemporalQueryResult, and TemporalVersionManager with SQLite storage and correct
method names (list_versions, compare_versions, get_version, apply_revision,
validate_snapshot, verify_checksum)
- Add docs/reference/distance.md: Distance Intelligence reference with corrected
SimilarityCalculator API (pairwise_similarity, batch_similarity, find_most_similar)
and semantic neighborhood / proximity-blended retrieval patterns
- Update docs/reference/kg.md: expand Exported Classes table to include all
TemporalPatternDetector, TemporalInterval, IntervalRelation, TemporalQueryResult,
AlgorithmTrackerWithProvenance, AlgorithmRegistry, ProvenanceTracker, SeedManager,
KGConfig; fix all temporal code examples to use correct constructors and method names
- Update docs/reference/context.md: add Distance Intelligence section
- Update docs/index.md: add v0.3.0 release accordion with feature highlights
- Update docs/docs.json: wire temporal and distance pages into Modules navigation
---
docs/docs.json | 2 +
docs/index.md | 40 +-
docs/reference/context.md | 76 +++-
docs/reference/distance.md | 615 ++++++++++++++++++++++++++
docs/reference/kg.md | 219 +++++++--
docs/reference/temporal.md | 883 +++++++++++++++++++++++++++++++++++++
6 files changed, 1798 insertions(+), 37 deletions(-)
create mode 100644 docs/reference/distance.md
create mode 100644 docs/reference/temporal.md
diff --git a/docs/docs.json b/docs/docs.json
index 8cd1f3f4..4c47a653 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -120,6 +120,8 @@
"pages": [
"reference/context",
"reference/kg",
+ "reference/temporal",
+ "reference/distance",
"reference/semantic_extract",
"reference/reasoning",
"reference/ontology"
diff --git a/docs/index.md b/docs/index.md
index 4de8e1a3..738c1ce7 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -15,10 +15,12 @@ If your stack can't answer those questions with a traceable record, you have a g
**Semantica closes that gap.** It's the context and accountability layer that sits beneath your existing agent framework: not a replacement for LangChain or LlamaIndex, but the infrastructure that makes their outputs trustworthy.
-
+
@@ -300,11 +302,37 @@ pip install semantica==0.5.0
-- **Temporal Intelligence** — 6-PR system: temporal data model, point-in-time queries, Allen interval algebra (all 13 relations), OWL-Time export
-- **Knowledge Explorer API** — Full FastAPI backend: 99 tests, 12 export formats, WebSocket progress, thread-safe sessions, audit trail
-- **Ontology Foundations** — SHACL generation/validation, SKOS vocabulary, ontology alignment API, diff & migration tooling
+Released **April 8, 2026**
+
+- **Temporal Intelligence** — Bi-temporal data model (`BiTemporalFact`, `TemporalBound`), point-in-time query engine, Allen interval algebra (all 13 relations), `TemporalNormalizer` (zero LLM calls), `TemporalQueryRewriter`, OWL-Time export
+- **Knowledge Explorer API** — Full FastAPI backend: 99 integration tests, 12 export formats, WebSocket progress, thread-safe `RLock` sessions, audit trail, rollback protection
+- **Ontology Foundations** — SHACL generation/validation (basic/standard/strict tiers), SKOS vocabulary, ontology alignment API, diff & migration with change impact analysis
- **Datalog Reasoning** — Pure-Python bottom-up semi-naive fixpoint, recursive Horn clause rules, guaranteed termination
- **Agno Integration** — 5 components: graph-backed memory, multi-hop GraphRAG, decision toolkit, KG toolkit, shared team context; 110 tests
+- **Novita AI provider** — OpenAI-compatible provider for 200+ open-weight models
+
+```bash
+pip install semantica==0.4.0
+```
+
+
+
+
+
+Released **March 10, 2026**
+
+- **Context Graph completeness** — `valid_from`/`valid_until` on every node, `find_active_nodes()` temporal filtering, `get_neighbors(min_weight)` confidence-filtered traversal
+- **Cross-graph navigation** — `link_graph()`, `navigate_to()`, `resolve_links()`, `cross_graph_path()` for multi-graph agent architectures
+- **Decision tracking system** — Full lifecycle: `record_decision()` → `add_causal_relationship()` → `find_similar_decisions()` → `trace_decision_chain()` → `analyze_decision_impact()` → `check_decision_rules()` → `get_decision_insights()`
+- **Advanced KG algorithms** — Node2Vec embeddings, betweenness / closeness / eigenvector centrality, Louvain community detection
+- **Policy management** — Versioned policy storage with policy exception tracking and approver audit trail
+- **Vector store** — Hybrid search (dense + sparse), PgVector HNSW/IVFFlat indexing with JSONB metadata filtering
+- **Apache AGE backend** — Full graph store support for PostgreSQL-native AGE graphs
+- **ResourceScheduler fix** — `RLock` deadlock fix for concurrent agent pipelines
+
+```bash
+pip install semantica==0.3.0
+```
diff --git a/docs/reference/context.md b/docs/reference/context.md
index e013667e..4768edaf 100644
--- a/docs/reference/context.md
+++ b/docs/reference/context.md
@@ -443,7 +443,81 @@ print("Nodes: {}, Edges: {}".format(stats["node_count"], stats["edge_count"]))
| `cross_graph_path(source_node_id, target_graph, target_node_id, max_hops)` | `Dict` | Shortest path across linked graphs |
| `clear()` | `None` | Reset graph state and all indexes |
-### Cross-Graph Navigation
+### Distance Intelligence (v0.5.0)
+
+`ContextGraph` exposes a full Distance Intelligence API for exploring semantic neighborhoods and blending proximity into retrieval.
+
+
+ Full Distance Intelligence reference — distance matrices, API endpoints, embedding cache, Explorer UI — is covered in the dedicated [Distance Intelligence](distance) page. This section documents the context-layer API.
+
+
+### Neighbors with Distance Metadata
+
+Pass `include_distance_metadata=True` to `get_neighbors()` to receive distance band, confidence decay, and path information alongside every neighbor:
+
+```python
+graph = ContextGraph(advanced_analytics=True)
+
+# ... populate graph ...
+
+neighbors = graph.get_neighbors(
+ "python",
+ hops=3,
+ include_distance_metadata=True,
+ min_weight=0.3, # exclude low-confidence edges
+)
+
+for n in neighbors:
+ print(
+ f"{n['node_id']:15s} "
+ f"band={n['distance_band']:10s} "
+ f"decay={n['confidence_decay']:.3f} "
+ f"hops={n['hop_count']}"
+ )
+```
+
+| Added field | Type | Description |
+| :---------- | :---- | :----------- |
+| `distance_band` | `str` | `"direct"` (1 hop) / `"near"` (2) / `"mid-range"` (3–4) / `"distant"` (5+) |
+| `confidence_decay` | `float` | `edge_weight ^ hop_count` — decays with each hop |
+| `path_to_anchor` | `List[str]` | Shortest path from anchor node to this neighbor |
+| `hop_count` | `int` | BFS depth from anchor |
+
+### Proximity-Blended Retrieval
+
+Set `proximity_weight` on `AgentContext` to blend graph proximity into every `retrieve()` and `find_precedents()` call:
+
+```python
+context = AgentContext(
+ vector_store=VectorStore(backend="faiss", dimension=768),
+ knowledge_graph=ContextGraph(advanced_analytics=True),
+ proximity_weight=0.3, # 0.7×semantic + 0.3×proximity
+)
+
+# combined_score is returned alongside semantic_score and proximity_score
+results = context.retrieve("web API frameworks", max_results=10)
+for r in results:
+ print(
+ f"[{r['combined_score']:.3f}] "
+ f"semantic={r['semantic_score']:.3f} "
+ f"proximity={r['proximity_score']:.3f} "
+ f"{r['content'][:60]}"
+ )
+
+# Override weight per-call
+precedents = context.find_precedents(
+ "infrastructure scaling decisions",
+ proximity_weight=0.5,
+ limit=5,
+)
+```
+
+
+ `proximity_weight=0.0` disables proximity blending entirely (pure semantic). `proximity_weight=1.0` returns results ranked purely by graph proximity to the query anchor. Values between `0.2`–`0.4` work well for most production use cases.
+
+
+
+## Cross-Graph Navigation
Link multiple independent `ContextGraph` instances so agents can traverse across problem spaces:
diff --git a/docs/reference/distance.md b/docs/reference/distance.md
new file mode 100644
index 00000000..376bda3e
--- /dev/null
+++ b/docs/reference/distance.md
@@ -0,0 +1,615 @@
+---
+title: "Distance Intelligence"
+description: "Semantic neighborhoods, N×N distance matrices, ego-mode exploration, proximity-blended retrieval, and embedding cache optimization."
+icon: "radar"
+---
+
+Distance Intelligence gives every node in your knowledge graph a **semantic neighborhood** — making it possible to answer not just "is A connected to B?" but "how semantically close is A to B, and what lies in between?"
+
+Introduced in **v0.5.0**, Distance Intelligence operates across three layers:
+
+
+
+
Distance Matrices
+
N×N upper-triangle semantic distance between any node set
+
+
+
Semantic Neighborhoods
+
BFS ego-graphs with confidence decay and distance band classification
+
+
+
Proximity Blending
+
Combine semantic similarity with graph proximity in retrieval
+
+
+
10× Cache
+
Graph revision–based embedding cache avoids redundant re-computation
+
+
+
+
+## Distance Bands
+
+Every neighbor result is classified into one of four distance bands based on hop count and semantic similarity:
+
+| Band | Hop count | Meaning | Explorer color |
+| :---- | :-------- | :------- | :------------- |
+| `direct` | 1 | Immediate neighbor — strong semantic overlap | Green |
+| `near` | 2 | One-hop away — closely related concept | Teal |
+| `mid-range` | 3–4 | Conceptually related but some separation | Yellow |
+| `distant` | 5+ | Weak structural connection | Red |
+
+Distance bands flow through the entire system: retrieval results, path responses, API endpoints, and the Explorer Ego Mode visualization all use the same four-tier classification.
+
+
+## Quick Start
+
+
+
+ The simplest entry point: call `get_neighbors()` with `include_distance_metadata=True`:
+
+ ```python
+ from semantica.context import ContextGraph
+
+ graph = ContextGraph(advanced_analytics=True)
+
+ graph.add_node("python", "language", properties={"paradigm": "multi"})
+ graph.add_node("fastapi", "framework", properties={"language": "Python"})
+ graph.add_node("django", "framework", properties={"language": "Python"})
+ graph.add_node("sqlmodel", "library", properties={"orm": True})
+
+ graph.add_edge("python", "fastapi", "enables")
+ graph.add_edge("python", "django", "enables")
+ graph.add_edge("fastapi", "sqlmodel", "uses")
+
+ neighbors = graph.get_neighbors(
+ "python",
+ hops=3,
+ include_distance_metadata=True,
+ )
+
+ for n in neighbors:
+ print(f"{n['node_id']:12s} band={n['distance_band']:10s} "
+ f"decay={n['confidence_decay']:.3f} "
+ f"path={n['path_to_anchor']}")
+ ```
+ ```
+ fastapi band=direct decay=1.000 path=['python', 'fastapi']
+ django band=direct decay=1.000 path=['python', 'django']
+ sqlmodel band=near decay=0.750 path=['python', 'fastapi', 'sqlmodel']
+ ```
+
+
+ ```python
+ from semantica.kg import SimilarityCalculator, NodeEmbedder
+
+ # Generate structural embeddings first
+ embedder = NodeEmbedder(method="node2vec", embedding_dimension=128)
+ embeddings = embedder.compute_embeddings(kg, ["language", "framework", "library"], ["enables", "uses"])
+
+ # N×N upper-triangle distance matrix
+ calc = SimilarityCalculator()
+ matrix = calc.compute_distance_matrix(embeddings)
+
+ # matrix["distances"] is an upper-triangle dict: {(node_a, node_b): distance}
+ for (a, b), dist in sorted(matrix["distances"].items(), key=lambda x: x[1]):
+ print(f"{a:15s} ↔ {b:15s} distance={dist:.4f}")
+ ```
+
+
+ Set `proximity_weight` on `AgentContext` to blend graph proximity into every semantic retrieval call:
+
+ ```python
+ from semantica.context import AgentContext, ContextGraph
+ from semantica.vector_store import VectorStore
+
+ context = AgentContext(
+ vector_store=VectorStore(backend="faiss", dimension=768),
+ knowledge_graph=ContextGraph(advanced_analytics=True),
+ decision_tracking=True,
+ proximity_weight=0.3, # combined = 0.7×semantic + 0.3×proximity
+ )
+
+ # retrieve() and find_precedents() both use the blended score
+ results = context.retrieve("web API frameworks", max_results=10)
+ for r in results:
+ print(f"[{r['combined_score']:.3f}] semantic={r['semantic_score']:.3f} "
+ f"proximity={r['proximity_score']:.3f} {r['content'][:60]}")
+ ```
+
+
+
+
+## ContextGraph Distance API
+
+### `get_neighbors()`
+
+Returns BFS neighbors enriched with distance metadata when `include_distance_metadata=True`:
+
+```python
+neighbors = graph.get_neighbors(
+ node_id="python",
+ hops=4,
+ include_distance_metadata=True,
+ min_weight=0.3, # exclude low-confidence edges
+)
+```
+
+| Field | Type | Description |
+| :---- | :---- | :----------- |
+| `node_id` | `str` | Node identifier |
+| `node_type` | `str` | Node type label |
+| `properties` | `Dict` | Node property dict |
+| `hop_count` | `int` | BFS hops from anchor |
+| `distance_band` | `str` | `"direct"` / `"near"` / `"mid-range"` / `"distant"` |
+| `confidence_decay` | `float` | Confidence score after hop-based decay: `weight^hop_count` |
+| `path_to_anchor` | `List[str]` | Shortest path from anchor to this node |
+| `edge_weight` | `float` | Weight of the direct edge (if hop=1) |
+
+### `get_neighbor_distances()`
+
+Returns a sorted list of neighbors ranked by combined confidence-decay distance score:
+
+```python
+distances = graph.get_neighbor_distances("fastapi", hops=3)
+
+for d in distances:
+ print(f"{d['node_id']:15s} score={d['combined_distance_score']:.4f} "
+ f"band={d['distance_band']}")
+```
+
+
+## SimilarityCalculator — Pairwise Similarity
+
+`SimilarityCalculator` computes similarity between node embeddings using four metrics.
+
+```python
+from semantica.kg import SimilarityCalculator
+
+calc = SimilarityCalculator(method="cosine", normalize=True)
+# method: "cosine" | "euclidean" | "manhattan" | "correlation"
+```
+
+### Constructor
+
+| Parameter | Type | Default | Description |
+| :--------- | :---- | :------- | :----------- |
+| `method` | `str` | `"cosine"` | Default metric: `"cosine"`, `"euclidean"`, `"manhattan"`, `"correlation"` |
+| `normalize` | `bool` | `True` | Normalize vectors before calculation |
+
+### Methods
+
+| Method | Returns | Description |
+| :------ | :------- | :----------- |
+| `cosine_similarity(vector1, vector2)` | `float` | Cosine similarity `[-1, 1]` between two vectors |
+| `euclidean_distance(embedding1, embedding2)` | `float` | L2 distance (non-negative) between two vectors |
+| `manhattan_distance(embedding1, embedding2)` | `float` | L1 distance (non-negative) between two vectors |
+| `correlation_similarity(embedding1, embedding2)` | `float` | Pearson correlation `[-1, 1]` between two vectors |
+| `batch_similarity(embeddings, query_embedding, method=None, top_k=None, chunk_size=1000)` | `Dict[str, float]` | Similarity of all nodes against a query vector. Returns `{node_id: score}` |
+| `pairwise_similarity(embeddings, method=None)` | `Dict[Tuple[str,str], float]` | Upper-triangle N×N pairwise similarity matrix for all node pairs |
+| `find_most_similar(embeddings, query_embedding, top_k=10, method=None)` | `List[Tuple[str, float]]` | Top-k `(node_id, score)` pairs sorted by similarity |
+
+### Pairwise Similarity Matrix
+
+`pairwise_similarity()` returns the upper triangle of the N×N matrix — each key is a `(node_id_a, node_id_b)` tuple:
+
+```python
+from semantica.kg import NodeEmbedder, SimilarityCalculator
+
+embedder = NodeEmbedder(method="node2vec", embedding_dimension=128)
+embeddings = embedder.compute_embeddings(kg, ["language", "framework"], ["enables", "uses"])
+
+calc = SimilarityCalculator(method="cosine")
+
+# N×N upper-triangle: Dict[(node_a, node_b), similarity_score]
+matrix = calc.pairwise_similarity(embeddings)
+
+# Sort by similarity (most similar first)
+for (a, b), score in sorted(matrix.items(), key=lambda x: x[1], reverse=True)[:5]:
+ print(f"{a:15s} ↔ {b:15s} similarity={score:.4f}")
+
+# Find most similar pair
+best_pair = max(matrix.items(), key=lambda x: x[1])
+print(f"Most similar: {best_pair[0]} score={best_pair[1]:.4f}")
+
+# Find most dissimilar pair
+worst_pair = min(matrix.items(), key=lambda x: x[1])
+print(f"Most distant: {worst_pair[0]} score={worst_pair[1]:.4f}")
+```
+
+
+ The matrix is upper-triangle only — `(a, b)` is stored but `(b, a)` is not. To look up either direction: `matrix.get((a, b)) or matrix.get((b, a))`.
+
+
+### Batch Similarity
+
+Efficiently compare a query vector against all nodes using chunked vectorized ops:
+
+```python
+# Query vector against all nodes
+scores = calc.batch_similarity(
+ embeddings,
+ query_embedding=my_query_vec,
+ method="cosine", # override default
+ top_k=10, # return only top 10 (None = all)
+ chunk_size=1000, # chunk size for memory efficiency
+)
+
+for node_id, score in sorted(scores.items(), key=lambda x: x[1], reverse=True):
+ print(f"{node_id:15s} {score:.4f}")
+```
+
+### Find Most Similar
+
+```python
+# Top-k (node_id, score) tuples sorted descending
+similar = calc.find_most_similar(
+ embeddings,
+ query_embedding=embeddings["python"],
+ top_k=5,
+ method="cosine",
+)
+
+for node_id, score in similar:
+ print(f"{node_id:15s} similarity={score:.4f}")
+```
+
+### Individual Metrics
+
+```python
+vec_a = embeddings["fastapi"]
+vec_b = embeddings["django"]
+
+cosine = calc.cosine_similarity(vec_a, vec_b)
+l2 = calc.euclidean_distance(vec_a, vec_b)
+l1 = calc.manhattan_distance(vec_a, vec_b)
+pearson = calc.correlation_similarity(vec_a, vec_b)
+
+print(f"Cosine: {cosine:.4f}")
+print(f"Euclidean: {l2:.4f}")
+print(f"Manhattan: {l1:.4f}")
+print(f"Correlation: {pearson:.4f}")
+```
+
+
+## Proximity-Blended Retrieval
+
+`AgentContext.retrieve()` and `find_precedents()` both support a `proximity_weight` parameter that blends graph proximity into the semantic similarity score:
+
+```
+combined_score = (1 − proximity_weight) × semantic_score
+ + proximity_weight × proximity_score
+```
+
+Where `proximity_score` is derived from hop count and edge weights from the query anchor node.
+
+```python
+from semantica.context import AgentContext, ContextGraph
+from semantica.vector_store import VectorStore
+
+context = AgentContext(
+ vector_store=VectorStore(backend="faiss", dimension=768),
+ knowledge_graph=ContextGraph(advanced_analytics=True),
+ proximity_weight=0.3,
+)
+
+# Standard retrieval — proximity blended automatically
+results = context.retrieve("model deployment strategies", max_results=10)
+
+# Override weight per-call
+results = context.retrieve(
+ "model deployment strategies",
+ max_results=10,
+ proximity_weight=0.5, # stronger proximity weight for this query
+)
+
+# find_precedents also blends proximity
+precedents = context.find_precedents(
+ "infrastructure scaling decisions",
+ proximity_weight=0.4,
+ limit=5,
+)
+
+for p in precedents:
+ print(f"[{p.combined_score:.3f}] {p.outcome} (confidence: {p.confidence:.2f})")
+```
+
+
+## Embedding Cache
+
+The embedding cache avoids re-computing embeddings for nodes that haven't changed since the last call — delivering up to **10× throughput improvement** on large graphs.
+
+### How It Works
+
+Each `GraphSession` tracks a **graph revision hash** derived from the current node and edge state. When a distance matrix or neighborhood request arrives:
+
+1. The revision hash is compared to the cached hash
+2. If unchanged: the cached embeddings are returned directly
+3. If changed (nodes/edges added or modified): the cache is invalidated and embeddings are recomputed
+
+```python
+from semantica.explorer import GraphSession
+
+session = GraphSession(graph=kg)
+
+# First call: computes embeddings, stores in cache
+embeddings = session.get_cached_embeddings()
+
+# Second call (graph unchanged): returns cache instantly
+embeddings = session.get_cached_embeddings()
+
+# After graph modification: cache is automatically invalidated
+session.graph.add_node("new_node", "concept", properties={})
+embeddings = session.get_cached_embeddings() # recomputes
+```
+
+| Parameter | Type | Default | Description |
+| :--------- | :---- | :------- | :----------- |
+| `force_refresh` | `bool` | `False` | Force cache invalidation even if the graph is unchanged |
+| Cache invalidation | Automatic | — | Triggered by `add_nodes()`, `add_edges()`, or any mutation |
+| Cache scope | Per-session | — | Each `GraphSession` maintains its own independent cache |
+
+
+ The cache is most effective in Explorer deployments where the same graph is queried repeatedly for distance matrices and ego-mode neighborhoods. In batch pipeline contexts, set `force_refresh=True` to ensure the latest graph state is always used.
+
+
+
+## REST API Endpoints
+
+Five new endpoints were added in v0.5.0 for programmatic distance intelligence access:
+
+### `POST /api/graph/distance-matrix`
+
+Compute N×N semantic distance matrix for a set of node IDs:
+
+```bash
+curl -X POST http://localhost:8000/api/graph/distance-matrix \
+ -H "Content-Type: application/json" \
+ -d '{
+ "node_ids": ["alice", "bob", "acme_corp", "beta_ltd"],
+ "embedding_model": "all-MiniLM-L6-v2",
+ "include_band_classification": true
+ }'
+```
+
+```json
+{
+ "matrix": {
+ "alice,bob": 0.312,
+ "alice,acme_corp": 0.087,
+ "alice,beta_ltd": 0.154,
+ "bob,acme_corp": 0.401,
+ "bob,beta_ltd": 0.233,
+ "acme_corp,beta_ltd": 0.198
+ },
+ "most_similar": ["alice", "acme_corp"],
+ "most_distant": ["bob", "acme_corp"],
+ "mean_distance": 0.231
+}
+```
+
+### `GET /api/graph/node/{id}/semantic-neighborhood`
+
+Retrieve the ego-graph (BFS neighborhood) of a node with distance metadata:
+
+```bash
+curl "http://localhost:8000/api/graph/node/alice/semantic-neighborhood?depth=3&include_distance_metadata=true"
+```
+
+```json
+{
+ "anchor_node": "alice",
+ "neighbors": [
+ {"node_id": "acme_corp", "distance_band": "direct", "confidence_decay": 1.0, "hop_count": 1},
+ {"node_id": "ceo_role", "distance_band": "direct", "confidence_decay": 1.0, "hop_count": 1},
+ {"node_id": "beta_ltd", "distance_band": "near", "confidence_decay": 0.75, "hop_count": 2},
+ {"node_id": "london_hq", "distance_band": "mid-range","confidence_decay": 0.56, "hop_count": 3}
+ ],
+ "total_neighbors": 4,
+ "depth": 3
+}
+```
+
+### `GET /api/decisions/causal-distance`
+
+Return causal distance (hop count through causal edges) between two decision nodes:
+
+```bash
+curl "http://localhost:8000/api/decisions/causal-distance?source=dec_001&target=dec_005"
+```
+
+```json
+{
+ "source": "dec_001",
+ "target": "dec_005",
+ "causal_hops": 3,
+ "causal_path": ["dec_001", "dec_002", "dec_004", "dec_005"],
+ "distance_band": "near"
+}
+```
+
+### `GET /api/temporal/distance-history`
+
+Track how the semantic distance between two nodes has evolved over time:
+
+```bash
+curl "http://localhost:8000/api/temporal/distance-history?node_a=alice&node_b=acme_corp&snapshots=2021-01-01,2022-01-01,2023-01-01"
+```
+
+```json
+{
+ "node_a": "alice",
+ "node_b": "acme_corp",
+ "history": [
+ {"timestamp": "2021-01-01", "distance": 0.08, "band": "direct"},
+ {"timestamp": "2022-01-01", "distance": 0.09, "band": "direct"},
+ {"timestamp": "2023-01-01", "distance": 0.54, "band": "mid-range"}
+ ]
+}
+```
+
+### `POST /api/export/distance-enriched`
+
+Export graph data enriched with distance metadata (CSV or JSONL, capped at 200 nodes):
+
+```bash
+curl -X POST http://localhost:8000/api/export/distance-enriched \
+ -H "Content-Type: application/json" \
+ -d '{"anchor_node": "alice", "depth": 4, "format": "csv"}'
+```
+
+
+## Explorer Distance Intelligence UI
+
+The Knowledge Explorer embeds Distance Intelligence directly in the browser dashboard:
+
+
+
+
+ Ego Mode centers the visualization on a selected node and renders its semantic neighborhood with **BFS depth-of-field fading** — nodes further from the anchor become progressively dimmer, revealing the "shape" of conceptual proximity.
+
+ - **Depth slider (1–8)**: controls the BFS radius of the neighborhood
+ - **Confidence decay visualization**: edge opacity maps to `confidence_decay` score
+ - **Distance band color coding**: green (direct) → teal (near) → yellow (mid-range) → red (distant)
+ - **Bottleneck highlighting**: bridge nodes that connect otherwise separate clusters are highlighted in the path inspector
+
+ Activate via the Explorer toolbar: **View → Ego Mode**, then click any node to set it as anchor.
+
+
+
+ The heatmap renders an N×N distance matrix as a color-coded grid — instantly revealing which clusters of nodes are semantically cohesive and which are isolated.
+
+ - **Color scale**: green (near, distance → 0) through yellow to red (distant, distance → 1)
+ - **Hover**: shows exact distance value and distance band for each cell
+ - **Sort options**: sort rows/columns by node type, community membership, or alphabetical
+
+ Access via **View → Distance Heatmap** in the Explorer sidebar.
+
+
+
+ Overlay semantic similarity on the standard force-directed graph layout without switching modes:
+
+ - **Semantic overlay**: edge thickness scaled by semantic similarity score
+ - **Structural overlay**: edge thickness scaled by graph centrality
+ - Both overlays can be toggled independently
+
+ Access via the **Overlay** toggle in the Explorer toolbar.
+
+
+
+ Click any two nodes to inspect the shortest path between them. The Path Inspector shows:
+
+ - **Distance band chip**: classifies the overall path as direct / near / mid-range / distant
+ - **Metric cards**: hop count, mean edge weight, path confidence decay
+ - **Bottleneck node highlight**: the single node whose removal would disconnect the path
+ - **Distance history**: timeline of how the distance between the two nodes has changed across graph snapshots
+
+ Access via **right-click → Inspect Path** on any two selected nodes.
+
+
+
+
+
+## Real-World Patterns
+
+
+
+ Find semantically cohesive topic clusters in a large knowledge graph without running community detection:
+
+ ```python
+ from semantica.kg import NodeEmbedder, SimilarityCalculator
+
+ embedder = NodeEmbedder(method="node2vec", embedding_dimension=128)
+ embeddings = embedder.compute_embeddings(kg, node_types=["Concept", "Topic"])
+
+ calc = SimilarityCalculator()
+
+ # Cluster nodes where pairwise distance < 0.2
+ clusters = calc.cluster_by_distance(embeddings, threshold=0.2)
+
+ for i, cluster in enumerate(clusters):
+ print(f"Cluster {i+1} ({len(cluster)} nodes): {cluster[:5]}")
+ ```
+
+
+ Flag nodes that are unexpectedly distant from their structural neighbors — potential data quality issues or genuine anomalies:
+
+ ```python
+ from semantica.context import ContextGraph
+ from semantica.kg import NodeEmbedder, SimilarityCalculator
+
+ graph = ContextGraph(advanced_analytics=True)
+ # ... build graph ...
+
+ embedder = NodeEmbedder(method="node2vec", embedding_dimension=128)
+ embeddings = embedder.compute_embeddings(graph._graph, ["entity"], ["RELATED_TO"])
+
+ calc = SimilarityCalculator()
+
+ for node_id in graph._graph.nodes():
+ neighbors = graph.get_neighbors(node_id, hops=1, include_distance_metadata=True)
+ for n in neighbors:
+ # Node connected by edge but semantically very distant → anomaly candidate
+ structural_dist = 1.0 - n["edge_weight"]
+ semantic_dist = calc.euclidean_distance(
+ embeddings[node_id], embeddings[n["node_id"]]
+ )
+ if semantic_dist > 0.7 and structural_dist < 0.3:
+ print(f"Anomaly: {node_id} → {n['node_id']} "
+ f"(structural={structural_dist:.2f}, semantic={semantic_dist:.2f})")
+ ```
+
+
+ Verify that similar decisions (low semantic distance) reached similar outcomes — flag inconsistencies for review:
+
+ ```python
+ from semantica.context import AgentContext, ContextGraph
+ from semantica.vector_store import VectorStore
+
+ context = AgentContext(
+ vector_store=VectorStore(backend="faiss", dimension=768),
+ knowledge_graph=ContextGraph(advanced_analytics=True),
+ decision_tracking=True,
+ proximity_weight=0.4,
+ )
+
+ # ... populate with historical decisions ...
+
+ # Find pairs of semantically close decisions with different outcomes
+ all_decisions = context.query_decisions("", max_hops=0)
+ for i, d1 in enumerate(all_decisions):
+ for d2 in all_decisions[i+1:]:
+ precedents = context.find_precedents(
+ d1.scenario, limit=5, proximity_weight=0.4
+ )
+ for p in precedents:
+ if p.source_decision_id == d2.decision_id:
+ if p.similarity_score > 0.85 and d1.outcome != d2.outcome:
+ print(f"INCONSISTENCY: {d1.scenario}")
+ print(f" Decision A: {d1.outcome} (confidence {d1.confidence:.2f})")
+ print(f" Decision B: {d2.outcome} (confidence {d2.confidence:.2f})")
+ print(f" Similarity: {p.similarity_score:.3f}")
+ ```
+
+
+
+
+## Performance
+
+| Operation | Without cache | With cache | Improvement |
+| :--------- | :------------ | :--------- | :---------- |
+| Distance matrix (118k nodes) | ~48s | ~4.8s | **10×** |
+| Semantic neighborhood (depth 4) | ~2.1s | ~0.21s | **10×** |
+| Node search (indexed) | 24 ms | 0.004 ms | **6,000×** |
+| Semantic deduplication | baseline | — | **6.98×** (v2 algorithms) |
+
+
+ The 10× cache improvement applies when the graph is unchanged between requests. In write-heavy pipelines where nodes are added continuously, cache hit rates will be lower. Use `force_refresh=False` (default) for read-heavy Explorer usage and `force_refresh=True` for batch pipeline contexts.
+
+
+- [Context Module](context) — `ContextGraph.get_neighbors()` and proximity-blended retrieval.
+- [Knowledge Graph Module](kg) — `NodeEmbedder`, `SimilarityCalculator`, and graph analytics.
+- [Visualization](visualization) — Programmatic distance heatmaps and ego-mode graph renders.
+- [Explorer](explorer) — Knowledge Explorer with built-in Distance Intelligence dashboard.
+
+- [Distance Intelligence](https://github.com/semantica-agi/semantica/blob/main/cookbook/advanced/12_Distance_Intelligence.ipynb) — Semantic neighborhoods and distance matrices · Advanced
diff --git a/docs/reference/kg.md b/docs/reference/kg.md
index 2fb7e66c..59bf42cb 100644
--- a/docs/reference/kg.md
+++ b/docs/reference/kg.md
@@ -23,7 +23,17 @@ icon: "diagram-project"
| `EntityResolver` | Entity deduplication and merging during graph construction |
| `GraphAnalyzer` | Unified analytics wrapper: runs centrality, community detection, and connectivity in one call |
| `ConnectivityAnalyzer` | Connected component detection, bridge identification, density, and degree statistics |
-| `TemporalGraphQuery` | Point-in-time snapshots, temporal diffs, and all 13 Allen interval queries |
+| `TemporalGraphQuery` | Point-in-time snapshots, range queries, evolution analysis, temporal path finding |
+| `TemporalPatternDetector` | Sequence and cycle pattern detection over temporal edges |
+| `TemporalReasoningEngine` | All 13 Allen interval algebra relations over `TemporalInterval` objects |
+| `TemporalInterval` | Frozen dataclass `(start: datetime, end: datetime \| TemporalBound, label?)` |
+| `IntervalRelation` | Enum of all 13 Allen relation labels (`BEFORE`, `AFTER`, `MEETS`, …) |
+| `BiTemporalFact` | Dataclass wrapping `valid_from`, `valid_until`, `recorded_at`, `superseded_at`. Factory: `BiTemporalFact.from_relationship(rel_dict)` |
+| `TemporalBound` | Sentinel enum for open-ended intervals — single value: `TemporalBound.OPEN` |
+| `TemporalNormalizer` | Parse NL temporal expressions to `(datetime, datetime)` tuples — zero LLM calls |
+| `TemporalQueryRewriter` | Extract temporal intent from free-text queries; returns `TemporalQueryResult` |
+| `TemporalQueryResult` | Dataclass output of `TemporalQueryRewriter.rewrite()` |
+| `TemporalVersionManager` | Versioned snapshots with SHA-256 integrity, SQLite-backed persistent storage |
| `CentralityCalculator` | PageRank, degree, betweenness, closeness, eigenvector centrality |
| `CommunityDetector` | Louvain, Leiden, Label Propagation, and K-Clique community detection |
| `PathFinder` | Dijkstra, A*, BFS, and K-Shortest path algorithms |
@@ -31,6 +41,11 @@ icon: "diagram-project"
| `NodeEmbedder` | Node2Vec structural embeddings for downstream ML |
| `SimilarityCalculator` | Cosine, Euclidean, Manhattan, and correlation similarity scoring |
| `GraphValidator` | Schema and constraint validation before persistence |
+| `AlgorithmTrackerWithProvenance` | Algorithm execution tracking with provenance metadata |
+| `AlgorithmRegistry` / `algorithm_registry` | Registry for registered algorithms; `algorithm_registry` is the shared singleton |
+| `ProvenanceTracker` | W3C PROV-O provenance tracking for graph operations |
+| `SeedManager` | Reproducible random seed management across algorithms |
+| `KGConfig` / `kg_config` | Module-level configuration; `kg_config` is the shared singleton |
@@ -57,56 +72,200 @@ kg = builder.build({"entities": entities, "relationships": relationships})
| `build_single_source(data)` | `dict` | Build graph from a single data source dict |
-## Temporal Knowledge Graphs (v0.4.0)
+## Temporal Knowledge Graphs (v0.4.0+)
-Use **`TemporalGraphQuery`** to attach `valid_from`/`valid_until` windows and query **point-in-time snapshots** of any graph:
+
+ Full temporal reference including `BiTemporalFact`, `TemporalReasoningEngine`, Allen interval algebra, and `TemporalNormalizer` is covered in the dedicated [Temporal Intelligence](temporal) page. This section documents the KG-layer temporal API.
+
+
+The temporal stack — see the [Temporal Intelligence](temporal) page for the full reference.
+
+### Building a Temporal Graph
```python
from semantica.kg import GraphBuilder, TemporalGraphQuery, TemporalVersionManager
-from datetime import datetime
-# Build a time-aware graph
builder = GraphBuilder()
kg = builder.build(sources=[
{
"entities": [
{"id": "alice", "type": "Person"},
{"id": "acme_corp", "type": "Organization"},
+ {"id": "beta_ltd", "type": "Organization"},
],
"relationships": [
{
"source": "alice", "target": "acme_corp", "type": "ceo_of",
- "valid_from": "2020-01-01",
- "valid_until": "2023-06-01",
- }
- ]
+ "valid_from": "2018-01-01",
+ "valid_until": "2022-06-01",
+ },
+ {
+ "source": "alice", "target": "beta_ltd", "type": "ceo_of",
+ "valid_from": "2022-06-01",
+ # No valid_until → open-ended (TemporalBound.OPEN)
+ },
+ ],
}
])
-
-# Point-in-time snapshot: TemporalGraphQuery takes no positional graph arg;
-# pass the graph into each query method instead.
-query = TemporalGraphQuery()
-snapshot_2021 = query.reconstruct_at_time(kg, "2021-06-15")
-snapshot_2023 = query.reconstruct_at_time(kg, "2023-01-01")
-
-# Relationships active within a date range
-range_result = query.query_time_range(kg, "", "2020-01-01", "2023-01-01")
-print(f"Relationships in range: {range_result['num_relationships']}")
-
-# Versioned snapshots: author and description are required
-versioner = TemporalVersionManager()
-versioner.create_snapshot(kg, version_label="2024-Q1",
- author="user@example.com",
- description="Q1 2024 snapshot")
```
-Supports all 13 Allen interval algebra relations:
+### Point-in-Time Queries
-- before, after, meets, met_by
-- overlaps, overlapped_by
-- during, contains, starts, started_by, finishes, finished_by, equals
+`TemporalGraphQuery` accepts optional constructor args; pass the graph into each query call:
-OWL-Time export available.
+```python
+from semantica.kg import TemporalGraphQuery
+
+query = TemporalGraphQuery(
+ temporal_granularity="day", # second|minute|hour|day|week|month|year
+ enable_temporal_reasoning=True,
+)
+
+# Primary API: query_at_time returns counts + filtered data
+result_2020 = query.query_at_time(kg, "", at_time="2020-06-15")
+result_2023 = query.query_at_time(kg, "", at_time="2023-01-01")
+print(f"Rels in 2020: {result_2020['num_relationships']}")
+
+# Low-level: reconstruct_at_time returns a deep-copied subgraph dict
+snapshot = query.reconstruct_at_time(kg, "2020-06-15")
+
+# Range query: all relationships active during any part of 2021
+range_result = query.query_time_range(kg, "", "2021-01-01", "2021-12-31")
+
+# Compare two snapshots: use TemporalVersionManager.compare_versions()
+# (temporal_diff() does not exist — see TemporalVersionManager below)
+```
+
+### Bi-Temporal Facts
+
+`BiTemporalFact` is a **dataclass** — use the `from_relationship()` factory, not a positional constructor:
+
+```python
+from semantica.kg import BiTemporalFact, TemporalBound
+
+rel = {
+ "source": "alice", "target": "acme_corp", "type": "ceo_of",
+ "valid_from": "2018-01-01",
+ "valid_until": "2022-06-01",
+ "recorded_at": "2018-01-05T09:32:00Z",
+ "superseded_at": None, # None → TemporalBound.OPEN (still current)
+}
+fact = BiTemporalFact.from_relationship(rel)
+
+print(fact.valid_from) # datetime(2018, 1, 1, tzinfo=utc)
+print(fact.valid_until) # datetime(2022, 6, 1, tzinfo=utc)
+print(fact.superseded_at) # TemporalBound.OPEN
+
+# Open-ended fact (no valid_until → TemporalBound.OPEN)
+open_rel = {"source": "alice", "target": "beta_ltd", "type": "ceo_of",
+ "valid_from": "2022-06-01"}
+open_fact = BiTemporalFact.from_relationship(open_rel)
+print(open_fact.valid_until) # TemporalBound.OPEN
+
+# Serialize back to dict fields for storage
+fields = fact.to_relationship_fields()
+```
+
+### Allen Interval Algebra
+
+`TemporalReasoningEngine` implements **all 13 Allen relations** deterministically — no LLM, no probability. It operates on `TemporalInterval` objects (not plain dicts):
+
+```python
+from semantica.kg import (
+ TemporalReasoningEngine, TemporalInterval, IntervalRelation
+)
+from datetime import datetime, timezone
+
+def dt(y, m, d): return datetime(y, m, d, tzinfo=timezone.utc)
+
+engine = TemporalReasoningEngine()
+
+h1_2020 = TemporalInterval(start=dt(2020, 1, 1), end=dt(2020, 6, 30))
+q2_q4 = TemporalInterval(start=dt(2020, 4, 1), end=dt(2020, 12, 31))
+
+relation = engine.relation(h1_2020, q2_q4) # primary method
+print(relation) # IntervalRelation.OVERLAPS
+print(relation.value) # "overlaps"
+
+print(engine.overlaps(h1_2020, q2_q4)) # True
+print(engine.contains(q2_q4, h1_2020)) # False
+print(engine.active_at(h1_2020, dt(2020, 3, 15))) # True
+```
+
+| `IntervalRelation` | `.value` | Description |
+| :--- | :--- | :--- |
+| `BEFORE` | `"before"` | A ends strictly before B starts |
+| `MEETS` | `"meets"` | A ends exactly when B starts |
+| `OVERLAPS` | `"overlaps"` | A and B share a period; A starts and ends first |
+| `STARTS` | `"starts"` | Same start; A ends before B |
+| `DURING` | `"during"` | A is entirely within B |
+| `FINISHES` | `"finishes"` | Same end; B started earlier |
+| `EQUALS` | `"equals"` | Identical interval |
+| `AFTER`, `MET_BY`, `OVERLAPPED_BY`, `STARTED_BY`, `CONTAINS`, `FINISHED_BY` | *(inverses)* | Mirror relations |
+
+### Natural Language Temporal Parsing
+
+```python
+from semantica.kg import TemporalNormalizer, TemporalQueryRewriter
+from datetime import datetime, timezone
+
+# reference_date set at construction time (required for relative phrases)
+norm = TemporalNormalizer(reference_date=datetime(2024, 6, 15, tzinfo=timezone.utc))
+
+# Returns Optional[Tuple[datetime, datetime]] — not a dict
+result = norm.normalize("last quarter")
+start, end = result
+print(start) # datetime(2024, 1, 1, tzinfo=utc)
+print(end) # datetime(2024, 3, 31, tzinfo=utc)
+
+result = norm.normalize("2022")
+# (datetime(2022, 1, 1, tzinfo=utc), datetime(2022, 12, 31, tzinfo=utc))
+
+result = norm.normalize("unparseable phrase")
+print(result) # None
+
+# TemporalQueryRewriter: primary method is rewrite(), returns TemporalQueryResult
+rewriter = TemporalQueryRewriter()
+result = rewriter.rewrite("Who was CEO before the 2022 restructuring?")
+print(result.temporal_intent) # "before"
+print(result.at_time.year) # 2022
+print(result.rewritten_query) # "Who was CEO"
+print(result.confidence) # 0.85
+print(result.has_temporal_context()) # True
+```
+
+### Versioned Snapshots
+
+```python
+from semantica.kg import TemporalVersionManager
+
+# In-memory (default); pass storage_path="versions.db" for SQLite persistence
+versioner = TemporalVersionManager()
+
+# author and description are required for create_snapshot
+versioner.create_snapshot(kg, version_label="2024-Q1",
+ author="user@example.com",
+ description="Q1 2024 baseline")
+
+# List versions (not list_snapshots)
+for v in versioner.list_versions():
+ print(f"{v['label']:12s} {v['author']}")
+
+# Compare two versions (not diff_versions)
+diff = versioner.compare_versions("2023-Q4", "2024-Q1")
+print(f"Entities added: {diff['summary']['entities_added']}")
+print(f"Relationships added: {diff['summary']['relationships_added']}")
+
+# Retrieve a version (not restore_snapshot)
+past_kg = versioner.get_version("2023-Q4")
+
+# SHA-256 integrity check
+versioner.verify_checksum(past_kg)
+```
+
+
+ See the [Temporal Intelligence](temporal) reference for the full class API, domain examples (personnel changes, policy evolution, financial timelines), and configuration options.
+
## Similarity Scoring
diff --git a/docs/reference/temporal.md b/docs/reference/temporal.md
new file mode 100644
index 00000000..d4bd4fdd
--- /dev/null
+++ b/docs/reference/temporal.md
@@ -0,0 +1,883 @@
+---
+title: "Temporal Intelligence"
+description: "Bi-temporal facts, point-in-time snapshots, Allen interval algebra, temporal pattern detection, and natural-language temporal parsing for time-aware knowledge graphs."
+icon: "clock"
+---
+
+Temporal Intelligence gives your knowledge graph a complete understanding of *when* — not just what is true, but when it was true in the real world, when it was recorded, and how facts have evolved over time.
+
+Shipped across **v0.3.0** (context temporal validity) and **v0.4.0** (full temporal stack), the system covers five layers:
+
+
+
+
Bi-temporal model
+
Valid time + transaction time on every fact
+
+
+
Point-in-time queries
+
Reconstruct any historical graph state in one call
+
+
+
Allen interval algebra
+
All 13 temporal relations, deterministic reasoning
+
+
+
NL temporal parsing
+
Zero LLM calls — pure regex + dateutil
+
+
+
+
+## Exported Classes
+
+| Class | Role |
+| :---- | :---- |
+| `BiTemporalFact` | Dataclass wrapping `valid_from`, `valid_until`, `recorded_at`, `superseded_at`. Factory: `BiTemporalFact.from_relationship(rel_dict)` |
+| `TemporalBound` | Enum sentinel for open-ended intervals. Single value: `TemporalBound.OPEN` |
+| `TemporalInterval` | Frozen dataclass `(start: datetime, end: datetime \| TemporalBound, label?)` used by `TemporalReasoningEngine` |
+| `IntervalRelation` | Enum of all 13 Allen relation labels (`BEFORE`, `AFTER`, `MEETS`, etc.) |
+| `TemporalGraphQuery` | Point-in-time snapshots, range queries, pattern detection, evolution analysis, temporal path finding |
+| `TemporalPatternDetector` | Sequence and cycle pattern detection over temporal edges |
+| `TemporalReasoningEngine` | Allen interval algebra over `TemporalInterval` objects — pure Python, deterministic |
+| `TemporalNormalizer` | Parse NL temporal expressions to `(datetime, datetime)` tuples — zero LLM calls |
+| `TemporalQueryRewriter` | Extract temporal intent from free-text queries; returns `TemporalQueryResult` |
+| `TemporalQueryResult` | Dataclass output of `TemporalQueryRewriter.rewrite()` |
+| `TemporalVersionManager` | Create, list, compare, and apply revisions to versioned graph snapshots |
+
+
+## Quick Start
+
+
+
+ Attach `valid_from` / `valid_until` to any relationship at construction time:
+
+ ```python
+ from semantica.kg import GraphBuilder
+
+ builder = GraphBuilder()
+ kg = builder.build(sources=[{
+ "entities": [
+ {"id": "alice", "type": "Person"},
+ {"id": "acme_corp", "type": "Organization"},
+ {"id": "beta_ltd", "type": "Organization"},
+ ],
+ "relationships": [
+ {
+ "source": "alice", "target": "acme_corp", "type": "ceo_of",
+ "valid_from": "2018-01-01",
+ "valid_until": "2022-06-01",
+ },
+ {
+ "source": "alice", "target": "beta_ltd", "type": "ceo_of",
+ "valid_from": "2022-06-01",
+ # No valid_until → open-ended (TemporalBound.OPEN)
+ },
+ ],
+ }])
+ ```
+
+
+ `TemporalGraphQuery` takes constructor args; pass the graph into each query call:
+
+ ```python
+ from semantica.kg import TemporalGraphQuery
+
+ query = TemporalGraphQuery(temporal_granularity="day")
+
+ # query_at_time is the primary public API
+ result_2020 = query.query_at_time(kg, query="", at_time="2020-06-15")
+ result_2023 = query.query_at_time(kg, query="", at_time="2023-01-01")
+
+ print(f"Rels active in 2020: {result_2020['num_relationships']}")
+ print(f"Rels active in 2023: {result_2023['num_relationships']}")
+ ```
+
+
+ `reconstruct_at_time()` is the low-level primitive — returns a full graph dict
+ with only nodes and edges that were valid at the given moment:
+
+ ```python
+ snapshot = query.reconstruct_at_time(kg, "2021-06-15")
+ # snapshot has "entities" and "relationships" keys
+ # usable with all GraphAnalyzer, PathFinder, CommunityDetector calls
+ ```
+
+
+ ```python
+ from semantica.kg import TemporalVersionManager
+
+ versioner = TemporalVersionManager() # in-memory storage
+ # versioner = TemporalVersionManager(storage_path="versions.db") # SQLite
+
+ versioner.create_snapshot(
+ kg,
+ version_label="2024-Q1",
+ author="user@example.com",
+ description="Q1 2024 snapshot after board restructure",
+ )
+
+ for v in versioner.list_versions():
+ print(f"{v['label']:12s} {v['author']} {v['timestamp']}")
+ ```
+
+
+
+
+## The Bi-Temporal Model
+
+Most systems track only one timeline: when something is currently true. Bi-temporal graphs track **two independent timelines** simultaneously:
+
+
+
+ *When was the fact true in the real world?*
+
+ - `valid_from` — date the fact became true
+ - `valid_until` — date the fact ceased to be true. Omit (or use `TemporalBound.OPEN`) for currently-active facts
+
+ ```python
+ from semantica.kg import BiTemporalFact, TemporalBound
+
+ # Create from an existing relationship dict
+ rel = {
+ "source": "alice", "target": "acme_corp", "type": "ceo_of",
+ "valid_from": "2018-01-01",
+ "valid_until": "2022-06-01",
+ }
+ fact = BiTemporalFact.from_relationship(rel)
+
+ print(fact.valid_from) # datetime(2018, 1, 1, tzinfo=utc)
+ print(fact.valid_until) # datetime(2022, 6, 1, tzinfo=utc)
+
+ # Serialize back to dict fields
+ fields = fact.to_relationship_fields()
+ print(fields["valid_from"]) # "2018-01-01T00:00:00Z"
+ print(fields["valid_until"]) # "2022-06-01T00:00:00Z"
+ ```
+
+
+ *When did we record this fact in the system?*
+
+ - `recorded_at` — auto-stamped at ingestion time (defaults to `datetime.now(utc)`)
+ - `superseded_at` — set when a later version replaces this record. `TemporalBound.OPEN` means still current
+
+ ```python
+ rel = {
+ "source": "alice", "target": "acme_corp", "type": "ceo_of",
+ "valid_from": "2018-01-01",
+ "valid_until": "2022-06-01",
+ "recorded_at": "2018-01-05T09:32:00Z",
+ "superseded_at": None, # still the current record
+ }
+ fact = BiTemporalFact.from_relationship(rel)
+
+ print(fact.recorded_at) # datetime(2018, 1, 5, 9, 32, tzinfo=utc)
+ print(fact.superseded_at) # TemporalBound.OPEN
+ ```
+
+
+ `TemporalBound.OPEN` is the single sentinel that represents an open-ended interval — a fact with no defined end date:
+
+ ```python
+ from semantica.kg import TemporalBound
+
+ print(TemporalBound.OPEN) # TemporalBound.OPEN
+ print(TemporalBound.OPEN.value) # "OPEN"
+
+ # A relationship with no valid_until gets TemporalBound.OPEN automatically
+ rel = {"source": "alice", "target": "beta_ltd", "type": "ceo_of",
+ "valid_from": "2022-06-01"}
+ fact = BiTemporalFact.from_relationship(rel)
+ print(fact.valid_until) # TemporalBound.OPEN
+ ```
+
+
+ `TemporalBound.OPEN` replaces both the start and end sentinels — there is only one value. The reasoning engine treats `OPEN` as `datetime.max` (far future) when comparing end bounds, and as `datetime.min` (far past) when used for `superseded_at`.
+
+
+
+
+
+## TemporalGraphQuery — Reference
+
+Constructed once; the graph is passed into each method call:
+
+```python
+from semantica.kg import TemporalGraphQuery
+
+query = TemporalGraphQuery(
+ enable_temporal_reasoning=True, # default
+ temporal_granularity="day", # second|minute|hour|day|week|month|year
+ max_temporal_depth=None, # optional max depth
+)
+```
+
+### Core Methods
+
+| Method | Returns | Description |
+| :------ | :------- | :----------- |
+| `query_at_time(graph, query, at_time, include_history=False, time_axis="valid")` | `Dict` | Primary API — filter graph to facts valid at `at_time`. Returns `entities`, `relationships`, `num_entities`, `num_relationships` |
+| `reconstruct_at_time(graph, at_time, *, time_axis="valid")` | `Dict` | Low-level — returns a deep-copied subgraph valid at `at_time`. Usable with all analytics tools |
+| `query_time_range(graph, query, start_time, end_time, temporal_aggregation="union", include_intervals=True, time_axis="valid")` | `Dict` | All relationships active during `[start, end]`. `temporal_aggregation`: `"union"` / `"intersection"` / `"evolution"` |
+| `validate_temporal_consistency(graph)` | `TemporalConsistencyReport` | Detect inverted intervals, overlapping same-edge facts, and entity lifetime violations |
+| `query_temporal_pattern(graph, pattern, time_window=None, min_support=1)` | `Dict` | Detect `"sequence"` or `"cycle"` patterns. Delegates to `TemporalPatternDetector` |
+| `analyze_evolution(graph, entity=None, relationship=None, start_time=None, end_time=None, metrics=None)` | `Dict` | Track evolution metrics (`"count"`, `"diversity"`, `"stability"`) over time |
+| `find_temporal_paths(graph, source, target, start_time=None, end_time=None, max_path_length=None, enforce_causal_ordering=True, ordering_strategy="strict")` | `Dict` | BFS paths respecting temporal validity. `ordering_strategy`: `"strict"` / `"overlap"` / `"loose"` |
+
+### `time_axis` Parameter
+
+All query methods accept a `time_axis` parameter controlling which timestamps are used for filtering:
+
+| Value | Effect |
+| :---- | :----- |
+| `"valid"` (default) | Filter by `valid_from` / `valid_until` — when the fact was true |
+| `"transaction"` | Filter by `recorded_at` / `superseded_at` — when we recorded it |
+| `"both"` | Fact must be active on both axes simultaneously |
+
+### Range Query Example
+
+```python
+# All relationships active at any point in 2021
+result = query.query_time_range(kg, "", "2021-01-01", "2021-12-31")
+for rel in result["relationships"]:
+ print(f" {rel['source']} --[{rel['type']}]--> {rel['target']}")
+
+# Only relationships valid throughout the entire range (stricter)
+result = query.query_time_range(
+ kg, "", "2021-01-01", "2021-12-31",
+ temporal_aggregation="intersection",
+)
+
+# Grouped by calendar period
+result = query.query_time_range(
+ kg, "", "2021-01-01", "2021-12-31",
+ temporal_aggregation="evolution",
+)
+for period, rels in result["relationship_buckets"].items():
+ print(f" {period}: {len(rels)} relationships active")
+```
+
+### Evolution Analysis
+
+```python
+evolution = query.analyze_evolution(
+ kg,
+ entity="alice", # track a specific entity (None = whole graph)
+ relationship="ceo_of", # track a specific edge type (None = all)
+ start_time="2018-01-01",
+ end_time="2024-12-31",
+ metrics=["count", "diversity", "stability"],
+)
+print(f"Relationship count: {evolution['count']}")
+print(f"Relationship types: {evolution['diversity']}")
+```
+
+### Temporal Path Finding
+
+```python
+paths = query.find_temporal_paths(
+ kg,
+ source="alice",
+ target="beta_ltd",
+ start_time="2022-01-01",
+ end_time="2024-12-31",
+ max_path_length=5,
+ enforce_causal_ordering=True,
+ ordering_strategy="strict", # strict|overlap|loose
+)
+for p in paths["paths"]:
+ print(f" {' → '.join(p['path'])} (length={p['length']})")
+```
+
+### Consistency Validation
+
+```python
+from semantica.kg import TemporalGraphQuery
+
+report = TemporalGraphQuery().validate_temporal_consistency(kg)
+
+print(f"Errors: {len(report.errors)}")
+print(f"Warnings: {len(report.warnings)}")
+
+for err in report.errors:
+ print(f" [{err['issue_type']}] fact_id={err['fact_id']}: {err['message']}")
+```
+
+Error types reported: `inverted_interval`, `invalid_temporal_fields`, `missing_source_entity`, `missing_target_entity`, `source_lifetime_mismatch`, `target_lifetime_mismatch`.
+Warning types: `overlapping_same_edge`, `gap_after_restart`.
+
+
+## TemporalPatternDetector
+
+Detect recurring temporal patterns across graph edges. Accessed directly or via `TemporalGraphQuery.query_temporal_pattern()`:
+
+```python
+from semantica.kg import TemporalPatternDetector
+
+detector = TemporalPatternDetector()
+
+# Find sequential edge patterns (A→B→C where edges are back-to-back)
+sequences = detector.detect_temporal_patterns(
+ kg,
+ pattern_type="sequence",
+ min_frequency=2,
+ time_window=None,
+)
+
+for seq in sequences:
+ print(f"Sequence: {seq['signature']} (occurs {seq['frequency']} times)")
+ for occ in seq["occurrences"]:
+ print(f" nodes={occ['nodes']} {occ['start_time']} → {occ['end_time']}")
+
+# Find cyclic patterns (A→B→C→A)
+cycles = detector.detect_temporal_patterns(
+ kg,
+ pattern_type="cycle",
+ min_frequency=1,
+)
+```
+
+| Parameter | Type | Default | Description |
+| :--------- | :---- | :------- | :----------- |
+| `pattern_type` | `str` | `"sequence"` | `"sequence"` or `"cycle"` |
+| `min_frequency` | `int` | `2` | Minimum occurrences for a pattern to be returned |
+| `time_window` | `Any` | `None` | Optional time constraint on pattern window |
+
+Each pattern dict has: `pattern_type`, `signature` (tuple of node IDs), `frequency`, `occurrences` (list with `nodes`, `edges`, `start_time`, `end_time`).
+
+
+## Allen Interval Algebra
+
+`TemporalReasoningEngine` operates on `TemporalInterval` objects — a frozen dataclass with `start: datetime` and `end: datetime | TemporalBound`:
+
+```python
+from semantica.kg import (
+ TemporalReasoningEngine, TemporalInterval, IntervalRelation, TemporalBound
+)
+from datetime import datetime, timezone
+
+def dt(year, month, day):
+ return datetime(year, month, day, tzinfo=timezone.utc)
+
+engine = TemporalReasoningEngine()
+
+h1_2020 = TemporalInterval(start=dt(2020, 1, 1), end=dt(2020, 6, 30))
+q2_q4 = TemporalInterval(start=dt(2020, 4, 1), end=dt(2020, 12, 31))
+
+relation = engine.relation(h1_2020, q2_q4)
+print(relation) # IntervalRelation.OVERLAPS
+print(relation.value) # "overlaps"
+
+print(engine.overlaps(h1_2020, q2_q4)) # True
+print(engine.contains(q2_q4, h1_2020)) # False
+```
+
+### All 13 Relations
+
+| `IntervalRelation` | `.value` | Inverse | Description |
+| :--- | :--- | :--- | :--- |
+| `BEFORE` | `"before"` | `AFTER` | A ends strictly before B starts |
+| `AFTER` | `"after"` | `BEFORE` | A starts strictly after B ends |
+| `MEETS` | `"meets"` | `MET_BY` | A ends exactly when B starts |
+| `MET_BY` | `"met_by"` | `MEETS` | A starts exactly when B ends |
+| `OVERLAPS` | `"overlaps"` | `OVERLAPPED_BY` | A and B share a period; A starts and ends first |
+| `OVERLAPPED_BY` | `"overlapped_by"` | `OVERLAPS` | B starts and ends before A, they share a period |
+| `STARTS` | `"starts"` | `STARTED_BY` | Same start time; A ends before B |
+| `STARTED_BY` | `"started_by"` | `STARTS` | Same start time; B ends before A |
+| `DURING` | `"during"` | `CONTAINS` | A is entirely inside B |
+| `CONTAINS` | `"contains"` | `DURING` | B is entirely inside A |
+| `FINISHES` | `"finishes"` | `FINISHED_BY` | Same end time; A started after B |
+| `FINISHED_BY` | `"finished_by"` | `FINISHES` | Same end time; B started after A |
+| `EQUALS` | `"equals"` | *(self-inverse)* | Identical interval |
+
+### Additional Engine Methods
+
+| Method | Returns | Description |
+| :------ | :------- | :----------- |
+| `active_at(interval, timestamp, granularity=None)` | `bool` | Is `timestamp` within `interval`? |
+| `merge_intervals(intervals)` | `List[TemporalInterval]` | Merge overlapping/touching intervals |
+| `gap_analysis(intervals, domain_start, domain_end)` | `List[TemporalInterval]` | Find uncovered gaps within a domain |
+| `coverage_percentage(intervals, domain_start, domain_end)` | `float` | Fraction of domain covered by intervals |
+| `timeline_of(entity_id, graph)` | `List[Dict]` | Sorted event timeline for an entity |
+| `retroactive_coverage(revision, original_facts)` | `Dict` | Classify facts as `affected`, `partial`, or `unaffected` by a revision |
+| `normalize_timestamp(timestamp, granularity)` | `datetime` | Truncate timestamp to granularity |
+| `normalize_interval(start, end, granularity)` | `TemporalInterval` | Parse and expand interval to granularity boundaries |
+
+### Advanced: Interval Operations
+
+```python
+from datetime import datetime, timezone
+
+def dt(y, m, d): return datetime(y, m, d, tzinfo=timezone.utc)
+
+intervals = [
+ TemporalInterval(start=dt(2020, 1, 1), end=dt(2020, 6, 30)),
+ TemporalInterval(start=dt(2020, 4, 1), end=dt(2020, 12, 31)),
+ TemporalInterval(start=dt(2021, 3, 1), end=TemporalBound.OPEN),
+]
+
+# Merge overlapping intervals
+merged = engine.merge_intervals(intervals)
+print(f"Merged into {len(merged)} intervals")
+
+# Find gaps in coverage across 2020
+gaps = engine.gap_analysis(intervals, dt(2020, 1, 1), dt(2020, 12, 31))
+print(f"Uncovered gaps: {len(gaps)}")
+
+# Coverage fraction
+pct = engine.coverage_percentage(intervals, dt(2020, 1, 1), dt(2021, 12, 31))
+print(f"Coverage: {pct:.1%}")
+
+# Entity timeline (all add/modify/remove events sorted by time)
+timeline = engine.timeline_of("alice", kg)
+for event in timeline:
+ print(f" {event['timestamp'].date()} {event['change_type']}")
+```
+
+
+## TemporalNormalizer — NL Temporal Parsing
+
+Converts natural-language temporal phrases into `(valid_from, valid_until)` datetime tuples. **Zero LLM calls.** Pure regex + `dateutil.relativedelta`.
+
+```python
+from semantica.kg import TemporalNormalizer
+from datetime import datetime, timezone
+
+norm = TemporalNormalizer(
+ reference_date=datetime(2024, 6, 15, tzinfo=timezone.utc)
+)
+```
+
+### `normalize(value)` → `Optional[Tuple[datetime, datetime]]`
+
+```python
+# ISO 8601 → point interval
+result = norm.normalize("2022-03-15")
+print(result)
+# (datetime(2022, 3, 15, tzinfo=utc), datetime(2022, 3, 15, tzinfo=utc))
+
+# Year → full year span
+result = norm.normalize("2022")
+print(result)
+# (datetime(2022, 1, 1, tzinfo=utc), datetime(2022, 12, 31, tzinfo=utc))
+
+# Quarter → quarter span
+result = norm.normalize("Q2 2021")
+print(result)
+# (datetime(2021, 4, 1, tzinfo=utc), datetime(2021, 6, 30, tzinfo=utc))
+
+# Month + year
+result = norm.normalize("January 2022")
+print(result)
+# (datetime(2022, 1, 1, tzinfo=utc), datetime(2022, 1, 31, tzinfo=utc))
+
+# YYYY-MM (ISO partial)
+result = norm.normalize("2022-03")
+print(result)
+# (datetime(2022, 3, 1, tzinfo=utc), datetime(2022, 3, 31, tzinfo=utc))
+
+# Relative phrases (requires reference_date)
+result = norm.normalize("last quarter")
+print(result)
+# (datetime(2024, 1, 1, tzinfo=utc), datetime(2024, 3, 31, tzinfo=utc))
+
+result = norm.normalize("last year")
+# (datetime(2023, 1, 1, tzinfo=utc), datetime(2023, 12, 31, tzinfo=utc))
+
+# Unparseable → None (never raises, logs debug)
+result = norm.normalize("recently")
+print(result) # None
+```
+
+
+ `normalize()` returns `None` for unparseable input — it **never raises** an exception. For relative phrases (`"last quarter"`, `"this year"`, etc.), `reference_date` **must** be set at construction time, otherwise `ValueError` is raised at call time.
+
+
+### `normalize_phrase(phrase)` → `Optional[Dict]`
+
+Look up a domain-specific temporal phrase in the phrase map:
+
+```python
+meta = norm.normalize_phrase("expiry date")
+print(meta)
+# {"maps_to": "valid_until", "type": "end", "domain": ["Healthcare", "Supply Chain"]}
+
+meta = norm.normalize_phrase("retroactive to")
+print(meta)
+# {"maps_to": "valid_from", "type": "start", "retroactive": True, "domain": ["Regulatory", "Finance"]}
+
+meta = norm.normalize_phrase("unknown phrase")
+print(meta) # None
+```
+
+Built-in domain phrases cover: General/Policy, Healthcare, Cybersecurity, Supply Chain, Finance, and Energy.
+
+### Custom Phrase Map
+
+```python
+from datetime import datetime, timezone
+
+def my_grant_window(ref: datetime):
+ return (
+ datetime(ref.year, 10, 1, tzinfo=timezone.utc),
+ datetime(ref.year, 10, 31, tzinfo=timezone.utc),
+ )
+
+norm = TemporalNormalizer(
+ reference_date=datetime(2024, 1, 1, tzinfo=timezone.utc),
+ phrase_map={"grant application window": my_grant_window},
+)
+start, end = norm.normalize("grant application window")
+```
+
+### Supported Expressions
+
+| Pattern | Example | Return type |
+| :------- | :------- | :---------- |
+| ISO 8601 full date/datetime | `"2022-03-15"`, `"2022-03-15T10:00:00Z"` | Point interval |
+| Year only | `"2022"` | Full year span |
+| Month + year (word) | `"January 2022"`, `"Jan 2022"` | Full month span |
+| YYYY-MM (ISO partial) | `"2022-03"` | Full month span |
+| Quarter + year | `"Q2 2021"` | Quarter span |
+| Relative (built-in) | `"last year"`, `"last quarter"`, `"this month"`, `"three months ago"`, `"six months ago"`, `"two years ago"` | Computed span |
+| Ambiguous slash date | `"03/04/2022"` | `None` + `TemporalAmbiguityWarning` |
+| Domain phrase | `"expiry date"`, `"retroactive to"` | Only via `normalize_phrase()` |
+
+
+## TemporalQueryRewriter
+
+Extract temporal intent from a natural-language query so downstream retrieval can apply deterministic temporal filtering.
+
+**Two modes:** regex-only (no LLM) or LLM-assisted for free-form phrasing.
+
+```python
+from semantica.kg import TemporalQueryRewriter
+
+# Regex-only (default — no dependencies beyond standard library)
+rewriter = TemporalQueryRewriter()
+
+# LLM-assisted for more complex phrasings
+from semantica.llms import Groq
+rewriter = TemporalQueryRewriter(
+ llm_provider=Groq(model="llama-3.1-8b-instant"),
+ reference_date=datetime.now(timezone.utc),
+)
+```
+
+### `rewrite(query, context=None)` → `TemporalQueryResult`
+
+```python
+# "before" intent
+r = rewriter.rewrite("which suppliers were certified before 2021?")
+print(r.temporal_intent) # "before"
+print(r.at_time.year) # 2021
+print(r.rewritten_query) # "which suppliers were certified?"
+print(r.confidence) # 0.85
+
+# "between" intent
+r = rewriter.rewrite("revenue between Q1 2022 and Q3 2022")
+print(r.temporal_intent) # "between"
+print(r.start_time) # datetime(2022, 1, 1, tzinfo=utc)
+print(r.end_time) # datetime(2022, 9, 30, tzinfo=utc)
+
+# "during" intent
+r = rewriter.rewrite("what decisions were made during Q2 2023?")
+print(r.temporal_intent) # "during"
+print(r.at_time) # datetime(2023, 4, 1, tzinfo=utc)
+
+# No temporal phrase
+r = rewriter.rewrite("list all active suppliers")
+print(r.temporal_intent) # None
+print(r.rewritten_query) # "list all active suppliers"
+print(r.has_temporal_context()) # False
+```
+
+### `TemporalQueryResult` Fields
+
+| Field | Type | Description |
+| :---- | :---- | :----------- |
+| `rewritten_query` | `str` | Original query with the temporal phrase stripped and whitespace normalised |
+| `at_time` | `Optional[datetime]` | Point-in-time bound for `before`, `after`, `at`, `during` intents |
+| `start_time` | `Optional[datetime]` | Lower bound for `between` queries |
+| `end_time` | `Optional[datetime]` | Upper bound for `between` queries |
+| `temporal_intent` | `Optional[str]` | One of `"before"`, `"after"`, `"at"`, `"during"`, `"between"`, or `None` |
+| `confidence` | `float` | `0.85` for regex extraction; LLM-propagated confidence or `0.75` fallback |
+
+| Method | Returns | Description |
+| :------ | :------- | :----------- |
+| `has_temporal_context()` | `bool` | `True` if any temporal parameter was extracted |
+
+Supported intent keywords: `before` / `prior to` / `until` / `up to`, `after` / `since` / `following`, `during` / `in` / `within`, `as of` / `at` / `on`, `between … and …`.
+
+
+## TemporalVersionManager
+
+Create and manage versioned graph snapshots with SHA-256 integrity checking. Supports both **in-memory** (default) and **SQLite persistent** storage.
+
+```python
+from semantica.kg import TemporalVersionManager
+
+# In-memory (default)
+versioner = TemporalVersionManager()
+
+# SQLite-backed (persists across process restarts)
+versioner = TemporalVersionManager(
+ storage_path="graph_versions.db",
+ version_strategy="timestamp", # timestamp | incremental | semantic
+)
+```
+
+### Methods
+
+| Method | Returns | Description |
+| :------ | :------- | :----------- |
+| `create_snapshot(graph, version_label, author, description)` | `Dict` | Create snapshot with SHA-256 checksum. `author` and `description` are required |
+| `create_version(graph, version_label=None, timestamp=None, metadata=None)` | `Dict` | Lightweight version without checksum or mandatory author |
+| `list_versions()` | `List[Dict]` | List all stored snapshots |
+| `get_version(label)` | `Optional[Dict]` | Retrieve snapshot by label |
+| `compare_versions(v1, v2, comparison_metrics=None)` | `Dict` | Detailed entity + relationship diff between two versions or labels |
+| `apply_revision(snapshot, revision)` | `Dict` | Temporal revision: supersede matching facts without deleting originals |
+| `validate_snapshot(snapshot)` | `bool` | Validate against v1.0 schema (required fields + types) |
+| `migrate_snapshot(snapshot)` | `Dict` | Upgrade old-format snapshot to v1.0 |
+| `verify_checksum(snapshot)` | `bool` | Integrity check via SHA-256 |
+
+### Snapshot & Diff Example
+
+```python
+# Create a snapshot (author and description are required)
+snap = versioner.create_snapshot(
+ kg,
+ version_label="v1.0",
+ author="analyst@example.com",
+ description="Initial baseline",
+)
+print(snap["checksum"]) # SHA-256 hex string
+
+# List versions
+for v in versioner.list_versions():
+ print(f"{v['label']:12s} {v['author']} {v['timestamp']}")
+
+# Get a specific version
+past = versioner.get_version("v1.0")
+
+# Diff: compare two versions (pass labels or snapshot dicts)
+diff = versioner.compare_versions("v1.0", "v2.0")
+print(f"Entities added: {diff['summary']['entities_added']}")
+print(f"Entities removed: {diff['summary']['entities_removed']}")
+print(f"Relationships added: {diff['summary']['relationships_added']}")
+print(f"Relationships removed: {diff['summary']['relationships_removed']}")
+
+# Field-level changes on each modified entity
+for change in diff["entities_modified"]:
+ print(f" {change['id']}: {change['changes']}")
+```
+
+### Temporal Revision
+
+Apply a revision to specific fact IDs — the originals are **superseded** (not deleted), preserving full audit history:
+
+```python
+revision = {
+ "fact_ids": ["alice|ceo_of|acme_corp"], # relationship key: src|type|target
+ "new_valid_from": "2018-03-01",
+ "new_valid_until": None, # None = TemporalBound.OPEN
+ "revision_type": "correction", # correction | retroactive
+ "author": "analyst@example.com",
+ "reason": "Original start date was incorrect",
+}
+
+revised_snapshot = versioner.apply_revision(snap, revision)
+# original fact is preserved with superseded_at set
+# replacement fact has new_valid_from, superseded_at = OPEN
+```
+
+### Integrity & Migration
+
+```python
+# Validate snapshot schema
+is_valid = versioner.validate_snapshot(snap)
+
+# Verify checksum integrity
+is_intact = versioner.verify_checksum(snap)
+
+# Upgrade old-format snapshot (no format_version field)
+upgraded = versioner.migrate_snapshot(old_snap)
+```
+
+
+## Context Graph Temporal Features (v0.3.0)
+
+The `ContextGraph` exposes temporal awareness directly on graph nodes and decisions, available since v0.3.0:
+
+```python
+from semantica.context import ContextGraph
+from datetime import datetime, timezone
+
+graph = ContextGraph(advanced_analytics=True)
+
+# Add time-bounded nodes
+graph.add_node("policy_v1", "policy",
+ properties={"text": "All transactions require dual approval"},
+ valid_from="2021-01-01",
+ valid_until="2023-06-30")
+
+graph.add_node("policy_v2", "policy",
+ properties={"text": "Transactions > $50k require dual approval"},
+ valid_from="2023-07-01")
+
+# Find nodes active at a specific timestamp
+current_policies = graph.find_active_nodes(
+ node_type="policy",
+ at_time=datetime.now(timezone.utc),
+)
+for p in current_policies:
+ print(p["properties"]["text"])
+# → "Transactions > $50k require dual approval"
+
+# Historical query
+past_policies = graph.find_active_nodes(
+ node_type="policy",
+ at_time=datetime(2022, 6, 1, tzinfo=timezone.utc),
+)
+for p in past_policies:
+ print(p["properties"]["text"])
+# → "All transactions require dual approval"
+```
+
+### Temporal Decision Windows
+
+```python
+from semantica.context import AgentContext, ContextGraph
+from semantica.vector_store import VectorStore
+
+context = AgentContext(
+ vector_store=VectorStore(backend="faiss", dimension=768),
+ knowledge_graph=ContextGraph(),
+ decision_tracking=True,
+)
+
+# Decision superseded after policy change
+old_id = context.record_decision(
+ category="data_retention", scenario="Set retention window for user PII",
+ reasoning="GDPR Article 5(1)(e) limits storage",
+ outcome="retain_90_days", confidence=0.98,
+ valid_from="2023-01-01", valid_until="2023-06-30",
+)
+
+new_id = context.record_decision(
+ category="data_retention", scenario="Set retention window for user PII",
+ reasoning="Legal confirmed 60-day window after new DPA amendment",
+ outcome="retain_60_days", confidence=0.99,
+ valid_from="2023-07-01",
+)
+
+# Temporal precedent search
+old_prec = context.find_precedents("data retention PII", as_of="2023-03-01", limit=3)
+new_prec = context.find_precedents("data retention PII", as_of="2024-01-01", limit=3)
+```
+
+
+## Real-World Patterns
+
+
+
+ ```python
+ from semantica.kg import GraphBuilder, TemporalGraphQuery
+
+ builder = GraphBuilder()
+ kg = builder.build(sources=[{
+ "entities": [
+ {"id": "alice", "type": "Person"},
+ {"id": "finteam", "type": "Team"},
+ ],
+ "relationships": [
+ {"source": "alice", "target": "finteam", "type": "leads",
+ "valid_from": "2020-01-01", "valid_until": "2022-12-31"},
+ ],
+ }])
+
+ query = TemporalGraphQuery()
+
+ # Incident in Nov 2022 → who was responsible?
+ result = query.query_at_time(kg, "", "2022-11-15")
+ leads = [r for r in result["relationships"] if r["type"] == "leads"]
+ print(f"Team lead at incident: {leads[0]['source']}")
+ ```
+
+
+ ```python
+ from semantica.kg import TemporalVersionManager, TemporalGraphQuery
+
+ versioner = TemporalVersionManager(storage_path="policy_history.db")
+ versioner.create_snapshot(kg_before, version_label="2023-H1",
+ author="compliance@org.com",
+ description="Pre-July policy baseline")
+ versioner.create_snapshot(kg_after, version_label="2023-H2",
+ author="compliance@org.com",
+ description="Post-July amendment")
+
+ diff = versioner.compare_versions("2023-H1", "2023-H2")
+ print(f"Policy changes: {diff['summary']['relationships_modified']}")
+ ```
+
+
+ ```python
+ from semantica.kg import TemporalGraphQuery
+
+ report = TemporalGraphQuery().validate_temporal_consistency(kg)
+
+ if report.errors:
+ print("ERRORS (must fix):")
+ for e in report.errors:
+ print(f" [{e['issue_type']}] {e['message']} (fact: {e['fact_id']})")
+
+ if report.warnings:
+ print("WARNINGS (review):")
+ for w in report.warnings:
+ print(f" [{w['issue_type']}] {w['message']} (fact: {w['fact_id']})")
+ ```
+
+
+ ```python
+ from semantica.kg import TemporalQueryRewriter, TemporalGraphQuery
+
+ rewriter = TemporalQueryRewriter()
+ query = TemporalGraphQuery()
+
+ user_query = "Who was responsible for compliance before the 2022 audit?"
+ result = rewriter.rewrite(user_query)
+
+ if result.has_temporal_context():
+ # Use point-in-time filtering
+ snapshot = query.reconstruct_at_time(kg, result.at_time)
+ else:
+ snapshot = kg
+
+ # Now run your retrieval over snapshot with result.rewritten_query
+ print(f"Intent: {result.temporal_intent}")
+ print(f"Query: {result.rewritten_query}")
+ ```
+
+
+
+
+## Configuration
+
+```yaml
+kg:
+ temporal:
+ enabled: true
+ default_validity: infinite # OPEN when valid_until is omitted
+ recorded_at_auto_stamp: true # auto-fill recorded_at on every ingested fact
+ reasoning:
+ enabled: true
+ granularity: day # second|minute|hour|day|week|month|year
+ engine: allen # allen | point_in_time_only
+```
+
+- [Knowledge Graph Module](kg) — Core graph construction, `GraphBuilder`, analytics.
+- [Context Module](context) — Decision temporal windows and `find_active_nodes()`.
+- [Provenance](provenance) — W3C PROV-O lineage stamped alongside temporal metadata.
+- [Export](export) — OWL, Turtle, JSON-LD, and Parquet export with temporal annotations.
+
+- [Temporal Knowledge Graphs](https://github.com/semantica-agi/semantica/blob/main/cookbook/advanced/10_Temporal_Knowledge_Graphs.ipynb) — Temporal reasoning and Allen algebra · Advanced
+- [Context Module](https://github.com/semantica-agi/semantica/blob/main/cookbook/introduction/19_Context_Module.ipynb) — Including temporal decision windows · Intermediate