Grand Diomande Research · Full HTML Reader

Stage 3: Expand + Master Plan -- AI-Generated Graph Topology

-- Step 1: Add updated_at column ALTER TABLE knowledge_graph ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW();

Agents That Account for Themselves research note experiment writeup candidate score 18 .md

Full Public Reader

Stage 3: Expand + Master Plan -- AI-Generated Graph Topology

---

3a. Risk Audit

CRITICAL RISKS

#### Risk 1: Confidence Decay Destroys Valuable Low-Frequency Triples
- Failure scenario: The 3 `evolved_from` triples (accessed rarely but semantically
critical) decay below threshold and disappear from default queries within 90 days.
- Probability: HIGH (guaranteed by the decay formula if not addressed)
- Impact: HIGH (evolutionary lineage lost)
- Mitigation: Tier 1 predicates (including evolved_from) are EXEMPT from decay. Only
Tier 2 and Tier 3 predicates decay. This is enforced in the decay SQL:
`WHERE predicate NOT IN (SELECT name FROM predicate_registry WHERE tier = 1)`
- Validation: After first decay run, verify all Tier 1 triples retain original confidence.
Query: `SELECT count(*) FROM knowledge_graph WHERE predicate = 'evolved_from'` must = 3.

#### Risk 2: Pruning Breaks RAG++ Retrieval Quality
- Failure scenario: RAG++ searches that previously returned relevant results now return
empty because the connecting triples were pruned.
- Probability: MEDIUM (pruning targets low-confidence noise, but RAG++ may use noise
paths as fallback when high-signal paths are sparse)
- Impact: HIGH (RAG++ is the primary GK consumer)
- Mitigation: Three-layer defense:
1. Soft delete (pruned_at, not DELETE) -- reversible in seconds
2. Before pruning: run a test suite of 20 known-good RAG++ queries, record results
3. After pruning: re-run the same 20 queries, compare result overlap. If <80
rollback (NULL all pruned_at values)
- Validation: RAG++ test suite passes with >= 80

#### Risk 3: Entity Embedding Pipeline Produces Garbage Embeddings
- Failure scenario: Entities with no GK predicates (e.g., bare prompt hashes like
"prompt:1069f93fdd8f24c3") are embedded as meaningless vectors, creating noise clusters
that contaminate hierarchy discovery.
- Probability: MEDIUM (8,303 entities are prompt hashes with only contains_prompt edges)
- Impact: MEDIUM (garbage clusters waste effort but do not corrupt good clusters)
- Mitigation: Filter entity list BEFORE embedding. Only embed entities that:
1. Have at least 2 predicates (not counting contains_prompt, has_intent, ran_session)
2. Are not prefixed with "session:", "prompt:", or "agent:" unless they also have Tier 1/2 edges
Expected: ~2,000-3,000 embeddable entities out of 10,781 total.
- Validation: Embedding count < 4,000. No cluster contains >50

MEDIUM RISKS

#### Risk 4: Cluster Labeling Produces Vague Names
- Probability: MEDIUM
- Impact: LOW (labels are human-readable convenience, not machine-critical)
- Mitigation: Three-pass labeling: (1) auto-label from shared predicates, (2) Gemini for
mixed clusters, (3) manual override list for top-20 clusters.

#### Risk 5: Predicate Registry Cold Start Misclassifies Important Predicates
- Probability: LOW (information content is a strong signal)
- Impact: MEDIUM (misclassified Tier 1 predicate treated as Tier 3, low confidence cap)
- Mitigation: Seed the registry with a hardcoded list of 7 known Core predicates.
All others are classified by information content.

#### Risk 6: KARL Boost Creates Positive Feedback Loop
- Probability: LOW (rate-limited to 0.05/day/triple)
- Impact: MEDIUM (frequently-used triples dominate, rare triples starve)
- Mitigation: Confidence ceiling of 0.99 (never reaches 1.0). Decay counterbalances boost.
Net equilibrium: actively-used triples stabilize at ~0.85-0.95, unused decay to threshold.

LOW RISKS

#### Risk 7: SQL Migration Downtime
- Probability: LOW (ALTER TABLE ADD COLUMN is non-blocking in PostgreSQL)
- Impact: LOW (GK is non-critical for <1 minute)
- Mitigation: Run migration during low-traffic period. Test on SQLite first.

#### Risk 8: Predicate similar_to Explodes Triple Count
- Probability: LOW (similarity threshold 0.7 limits pairs)
- Impact: LOW (estimated ~2,000 new triples on a 35K-triple graph)
- Mitigation: Hard cap: max 5 similar_to triples per entity. If >5, keep top-5 by score.

---

3b. Expanded Specifications

Spec 1: SQL Migration Script

sql
-- Migration: v1.1.0 -- Confidence Evolution
-- Run on cloud-vm PostgreSQL

-- Step 1: Add updated_at column
ALTER TABLE knowledge_graph
  ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW();

-- Backfill: set updated_at = created_at for existing rows
UPDATE knowledge_graph SET updated_at = created_at WHERE updated_at IS NULL;

-- Step 2: Add soft delete columns
ALTER TABLE knowledge_graph
  ADD COLUMN IF NOT EXISTS pruned_at TIMESTAMPTZ DEFAULT NULL;
ALTER TABLE knowledge_graph
  ADD COLUMN IF NOT EXISTS pruned_reason TEXT DEFAULT NULL;

-- Step 3: Partial index for active (non-pruned) triples
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_knowledge_active
  ON knowledge_graph (subject, predicate, object)
  WHERE pruned_at IS NULL;

-- Step 4: Index for decay queries (find stale triples)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_knowledge_updated
  ON knowledge_graph (updated_at)
  WHERE pruned_at IS NULL;

-- Step 5: Predicate registry table
CREATE TABLE IF NOT EXISTS predicate_registry (
  name TEXT PRIMARY KEY,
  tier INTEGER NOT NULL DEFAULT 3,  -- 1=Core, 2=Semi, 3=Freeform
  usage_count BIGINT NOT NULL DEFAULT 0,
  avg_confidence FLOAT NOT NULL DEFAULT 0.5,
  std_confidence FLOAT NOT NULL DEFAULT 0.0,
  information_content FLOAT NOT NULL DEFAULT 0.0,
  computed_threshold FLOAT NOT NULL DEFAULT 0.3,
  promoted_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Step 6: Update upsert to touch updated_at
-- This is handled in Rust code change, not SQL.
-- The ON CONFLICT clause becomes:
-- ON CONFLICT (subject, predicate, object) DO UPDATE SET
--   confidence = GREATEST(knowledge_graph.confidence, EXCLUDED.confidence),
--   source = EXCLUDED.source,
--   updated_at = NOW()

Spec 2: Confidence Decay Module (Rust)

rust
// File: src/decay.rs (~100 lines)

use chrono::{DateTime, Utc, Duration};

pub struct DecayConfig {
    pub half_life_days: f64,      // Default: 90.0
    pub min_confidence: f64,       // Default: 0.05 (below this = prune candidate)
    pub exempt_predicates: Vec<String>,  // Tier 1 predicates, no decay
    pub exempt_sources: Vec<String>,     // e.g., "manual", "human-curated"
}

impl Default for DecayConfig {
    fn default() -> Self {
        Self {
            half_life_days: 90.0,
            min_confidence: 0.05,
            exempt_predicates: vec![
                "is_a".into(), "part_of".into(), "evolved_from".into(),
                "works_on".into(), "built_with".into(), "deployed_on".into(),
                "depends_on".into(),
            ],
            exempt_sources: vec!["manual".into(), "human-curated".into()],
        }
    }
}

pub fn compute_decayed_confidence(
    current_confidence: f64,
    updated_at: DateTime<Utc>,
    now: DateTime<Utc>,
    config: &DecayConfig,
) -> f64 {
    let days_since_update = (now - updated_at).num_seconds() as f64 / 86400.0;
    if days_since_update <= 0.0 {
        return current_confidence;
    }
    let decay_factor = 0.5_f64.powf(days_since_update / config.half_life_days);
    let decayed = current_confidence * decay_factor;
    decayed.max(config.min_confidence)
}

// SQL for batch decay (called from endpoint):
// UPDATE knowledge_graph
// SET confidence = confidence * POWER(0.5, EXTRACT(EPOCH FROM (NOW() - updated_at)) / 86400.0 / $1),
//     source = 'decay:' || source
// WHERE pruned_at IS NULL
//   AND predicate NOT IN (SELECT name FROM predicate_registry WHERE tier = 1)
//   AND source NOT LIKE 'decay:%'
//   AND updated_at < NOW() - INTERVAL '$2 days'

Spec 3: Topology Metrics (Rust)

rust
// File: src/topology.rs (~200 lines)

pub struct EntityMetrics {
    pub entity: String,
    pub in_degree: usize,
    pub out_degree: usize,
    pub total_degree: usize,
    pub predicate_diversity: f64,  // Shannon entropy
    pub avg_confidence: f64,
    pub min_confidence: f64,
    pub staleness_days: f64,
    pub isolation_score: f64,  // 1.0 - (degree / max_degree)
}

pub struct PredicateMetrics {
    pub predicate: String,
    pub edge_count: usize,
    pub avg_confidence: f64,
    pub std_confidence: f64,
    pub unique_subjects: usize,
    pub unique_objects: usize,
    pub density: f64,  // edges / (subjects * objects)
    pub information_content: f64,  // -log2(edge_count / total_edges)
}

// SQL for entity metrics:
// SELECT subject as entity,
//   COUNT(*) FILTER (WHERE subject = e) as out_degree,
//   COUNT(*) FILTER (WHERE object = e) as in_degree,
//   COUNT(DISTINCT predicate) as predicate_count,
//   AVG(confidence) as avg_confidence,
//   MIN(confidence) as min_confidence,
//   EXTRACT(EPOCH FROM (NOW() - MAX(updated_at))) / 86400 as staleness_days
// FROM knowledge_graph
// WHERE pruned_at IS NULL AND (subject = $1 OR object = $1)
// GROUP BY entity

// SQL for predicate metrics:
// SELECT predicate,
//   COUNT(*) as edge_count,
//   AVG(confidence) as avg_confidence,
//   STDDEV(confidence) as std_confidence,
//   COUNT(DISTINCT subject) as unique_subjects,
//   COUNT(DISTINCT object) as unique_objects,
//   -LOG(2, COUNT(*)::float / (SELECT COUNT(*) FROM knowledge_graph WHERE pruned_at IS NULL)) as info_content
// FROM knowledge_graph
// WHERE pruned_at IS NULL
// GROUP BY predicate
// ORDER BY edge_count DESC

Spec 4: Entity Embedding Pipeline (Python)

python
# File: flows/feed-hub/gk_entity_embeddings.py (~180 lines)
# Prefect flow, runs weekly on cloud-vm

import httpx
import pickle
from pathlib import Path

GK_URL = "http://localhost:8001"
RAG_EMBED_URL = "http://localhost:8000/api/rag/embed"
OUTPUT_PATH = Path("/home/mohameddiomande/gk_entity_embeddings.pkl")

EXCLUDE_PREFIXES = ["session:", "prompt:", "agent:"]
NOISE_PREDICATES = {"contains_prompt", "has_intent", "ran_session", "has_path"}

def fetch_embeddable_entities():
    """Fetch entities with >= 2 non-noise predicates."""
    stats = httpx.get(f"{GK_URL}/api/knowledge/stats").json()
    total = stats["total_triples"]

    entities = {}  # entity -> list of (predicate, object, confidence)
    offset = 0
    while offset < total:
        resp = httpx.get(f"{GK_URL}/api/knowledge", params={"limit": 500}).json()
        for t in resp["triples"]:
            if t["predicate"] not in NOISE_PREDICATES:
                entities.setdefault(t["subject"], []).append(
                    (t["predicate"], t["object"], t["confidence"])
                )
        offset += 500
        if len(resp["triples"]) < 500:
            break

    # Filter: >= 2 non-noise predicates, no excluded prefixes
    return {
        e: preds for e, preds in entities.items()
        if len(preds) >= 2 and not any(e.startswith(p) for p in EXCLUDE_PREFIXES)
    }

def enrich_entity_text(entity, predicates):
    """Build context-rich text for embedding."""
    parts = [entity]
    for pred, obj, conf in sorted(predicates, key=lambda x: -x[2])[:10]:
        parts.append(f"{pred}: {obj} ({conf:.1f})")
    return ". ".join(parts)[:4000]

def embed_batch(texts, batch_size=50):
    """Embed texts via RAG++ endpoint."""
    embeddings = {}
    for i in range(0, len(texts), batch_size):
        batch = list(texts.items())[i:i+batch_size]
        for entity, text in batch:
            resp = httpx.post(RAG_EMBED_URL, json={"text": text}, timeout=10)
            if resp.status_code == 200:
                emb = resp.json().get("embedding", [])
                if len(emb) == 3072:
                    embeddings[entity] = emb
    return embeddings

Spec 5: Hierarchy Discovery (Python)

python
# File: flows/feed-hub/gk_hierarchy_discovery.py (~200 lines)
# Runs after gk_entity_embeddings.py

import numpy as np
from scipy.cluster.hierarchy import fcluster, linkage
from scipy.spatial.distance import pdist
import httpx

def discover_hierarchy(embeddings, n_coarse=30, n_fine_per_coarse=5):
    """Two-level agglomerative clustering."""
    entities = list(embeddings.keys())
    vectors = np.array([embeddings[e] for e in entities])

    # Level 1: Coarse clustering
    linkage_matrix = linkage(pdist(vectors, metric='cosine'), method='ward')
    coarse_labels = fcluster(linkage_matrix, t=n_coarse, criterion='maxclust')

    # Level 2: Fine clustering within each coarse cluster
    hierarchy = {}  # {coarse_label: {fine_label: [entities]}}
    for cluster_id in range(1, n_coarse + 1):
        mask = coarse_labels == cluster_id
        cluster_entities = [entities[i] for i, m in enumerate(mask) if m]
        cluster_vectors = vectors[mask]

        if len(cluster_entities) <= n_fine_per_coarse:
            hierarchy[cluster_id] = {0: cluster_entities}
            continue

        fine_linkage = linkage(pdist(cluster_vectors, metric='cosine'), method='ward')
        n_fine = min(n_fine_per_coarse, len(cluster_entities) // 2)
        fine_labels = fcluster(fine_linkage, t=max(2, n_fine), criterion='maxclust')

        fine_clusters = {}
        for entity, label in zip(cluster_entities, fine_labels):
            fine_clusters.setdefault(label, []).append(entity)
        hierarchy[cluster_id] = fine_clusters

    return hierarchy

def label_clusters(hierarchy, embeddings):
    """Label clusters using shared predicates + Gemini fallback."""
    labels = {}
    for coarse_id, fine_clusters in hierarchy.items():
        coarse_entities = [e for fc in fine_clusters.values() for e in fc]
        coarse_label = auto_label(coarse_entities) or gemini_label(coarse_entities)
        labels[coarse_id] = coarse_label

        for fine_id, entities in fine_clusters.items():
            fine_label = auto_label(entities) or gemini_label(entities)
            labels[(coarse_id, fine_id)] = fine_label

    return labels

def auto_label(entities):
    """Try to label from shared GK predicates."""
    # Fetch predicates for all entities
    shared = None
    for entity in entities[:10]:
        resp = httpx.get(f"{GK_URL}/api/knowledge",
                        params={"subject": entity, "limit": 50}).json()
        preds = {(t["predicate"], t["object"]) for t in resp["triples"]}
        shared = preds if shared is None else shared & preds

    if shared:
        # Most common shared predicate-object pair
        return max(shared, key=lambda x: x[1])[1]  # Use the object as label
    return None

---

3c. Master Execution Checklist

Wave 0: Foundation (Day 1-2)

IDTaskOwnerInputOutputValidationStatus
0.1Run SQL migration on cloud-vm PostgreSQLClaudeMigration script (Spec 1)3 new columns, 2 new indexes, 1 new table`\d knowledge_graph` shows updated_at, pruned_at, pruned_reason columns[ ]
0.2Update Rust upsert to set updated_at = NOW()Claudeknowledge_db_pg.rsModified upsert SQLInsert a triple, verify updated_at != created_at on re-insert[ ]
0.3Backfill updated_at = created_at for all existing rowsClaudeSQL one-liner47,125 rows updated`SELECT COUNT(*) WHERE updated_at IS NULL` = 0[ ]
0.4Add pruned_at IS NULL filter to all queriesClaudeknowledge_db_pg.rs, knowledge_db_sqlite.rsModified WHERE clausesPrune one triple manually, verify it disappears from default queries[ ]

Done gate: All columns exist, upsert touches updated_at, pruned triples are invisible.

Wave 1: Confidence Evolution (Day 3-5)

IDTaskOwnerInputOutputValidationStatus
1.1Implement decay module (src/decay.rs)ClaudeSpec 2New Rust moduleUnit tests: decay after 90 days = 50
1.2Add POST /api/knowledge/decay endpointClaudedecay.rsNew endpointcurl POST returns {"decayed": N, "exempt": M}[ ]
1.3Modify traversal to use confidence productClaudeknowledge_handlers.rsModified TraversalPathTraversal returns path_confidence field, product not min[ ]
1.4Add per-predicate threshold computationClaudepredicate_registry tableComputed thresholdsthreshold = max(0.1, avg - 2*std) for each predicate[ ]
1.5Default queries use computed thresholdClaudequery_knowledge_handlerModified WHERE clauseQuery for "has_intent" auto-filters below 0.6 threshold[ ]
1.6Run first decay on production graphMohamedEndpoint from 1.2Decayed triplesBefore/after confidence distribution comparison[ ]

Done gate: Confidence decays, traversal uses products, queries use thresholds.

Wave 2: Topology Pruning (Day 6-8)

IDTaskOwnerInputOutputValidationStatus
2.1Implement topology metrics module (src/topology.rs)ClaudeSpec 3New Rust moduleUnit tests for Shannon entropy, isolation score, info content[ ]
2.2Compute metrics for all entities and predicatesClaudetopology.rsMetrics in memory / APIGET /api/knowledge/topology/metrics returns all predicate metrics[ ]
2.3Classify predicates into signal tiersClaudePredicate metricsUpdated predicate_registryCore: 7 predicates, Semi: ~20, Freeform: ~2000[ ]
2.4Run RAG++ baseline test suite (20 queries)MohamedRAG++ endpointBaseline results savedResults saved as JSON for comparison[ ]
2.5Apply Tier 1 automatic pruningClaudePruning rulesPruned triples (soft delete)Count of pruned = ~12,000-15,000[ ]
2.6Re-run RAG++ test suite, compare overlapMohamedBaseline + post-prune resultsOverlap percentageOverlap >= 80
2.7Add GET /api/knowledge/topology/health endpointClaudetopology.rs, metricsNew endpointReturns predicate tiers, prune counts, health score[ ]

Done gate: Graph pruned by 25-32

Wave 3: Predicate Registry (Day 9-10)

IDTaskOwnerInputOutputValidationStatus
3.1Seed predicate_registry with initial classificationClaudeWave 2 metricsPopulated table7 Tier 1, ~20 Tier 2, rest Tier 3[ ]
3.2Add confidence capping for Tier 3 predicatesClaudeknowledge_handlers.rsModified upsertFreeform predicate insertion capped at 0.5[ ]
3.3Add GET /api/knowledge/predicates endpointClaudepredicate_registry tableNew endpointReturns all predicates with tier, usage, threshold[ ]
3.4Add promotion detection (flag candidates)Claudepredicate_registryPromotion flagsTier 3 predicates with usage>30 + avg_conf>0.4 flagged[ ]
3.5Rebuild and deploy updated GK binaryClaude+MohamedAll Wave 0-3 changesNew binary on cloud-vm`cargo build --release`, systemd restart, health check passes[ ]

Done gate: Registry populated, confidence capped, predicates endpoint live.

Wave 4: Hierarchy Discovery (Day 11-15)

IDTaskOwnerInputOutputValidationStatus
4.1Build entity embedding pipeline (Python)ClaudeSpec 4gk_entity_embeddings.pkl~2,000-3,000 entities embedded, 3072 dims each[ ]
4.2Run hierarchy discovery (agglomerative clustering)ClaudeSpec 5 + embeddingsHierarchy JSON20-40 coarse clusters, 3-8 fine per coarse[ ]
4.3Label clusters (auto + Gemini + manual override)Claude+MohamedHierarchy JSONLabeled hierarchyTop-20 clusters have meaningful names[ ]
4.4Ingest hierarchy as is_a / part_of triplesClaudeLabeled hierarchy~2,000-4,000 new triples in GKGET /api/knowledge?predicate=is_a returns results[ ]
4.5Generate similar_to triples from embedding similarityClaudeEmbeddings~1,500-2,500 new triplessimilar_to triples with confidence = cosine similarity[ ]
4.6Register is_a, part_of, similar_to in predicate registryClaudepredicate_registryUpdated registryis_a and part_of are Tier 1, similar_to is Tier 2[ ]
4.7Create Prefect weekly re-discovery flowClaudeSpecs 4+5New flow fileFlow registered in Prefect, runs Sundays at 4am[ ]

Done gate: Hierarchy exists in GK, similar_to connections discovered, weekly refresh scheduled.

Wave 5: KARL Integration + Query Planner (Day 16-19)

IDTaskOwnerInputOutputValidationStatus
5.1Add report_to_gk() in KARL trajectory_tap.pyClaudeKARL codebaseModified flush_sessionOn reward>0.7, entities boosted in GK[ ]
5.2Rate-limit KARL boost (0.05/day/triple)Claudereport_to_gk()Rate limiting logicSame triple boosted twice in a day: second boost rejected[ ]
5.3Add explain mode to traversal endpointClaudeknowledge_handlers.rsexplain field in responseTraversal with explain=true returns plan object[ ]
5.4Add Prometheus metrics for graph healthClaudetopology.rsMetrics endpointgk_total_triples, gk_pruned_triples, gk_avg_confidence, gk_predicate_count, gk_decay_runs_total[ ]
5.5Nexus Portal: add /graph-health pageClaudeNexus codebaseNew dashboard pageShows predicate tiers, pruning stats, confidence distribution[ ]
5.6End-to-end validation: full traversal testMohamedAll waves completeTest report10 traversals return confidence-weighted, hierarchy-aware results[ ]

Done gate: KARL feedback live, explain mode works, Prometheus metrics exported, Nexus shows health.

---

Prometheus Metrics

MetricTypeDescription
`gk_total_triples`GaugeTotal non-pruned triples
`gk_pruned_triples`GaugeSoft-deleted triples
`gk_avg_confidence`GaugeMean confidence of active triples
`gk_predicate_count_by_tier`Gauge (labeled)Predicates per tier
`gk_decay_runs_total`CounterNumber of decay runs executed
`gk_decay_triples_affected`CounterTriples affected by decay
`gk_karl_boosts_total`CounterKARL confidence boosts applied
`gk_hierarchy_entities`GaugeEntities with is_a/part_of triples
`gk_similar_pairs`GaugeEntity pairs connected by similar_to
`gk_traversal_explain_ms`HistogramTraversal time with explain mode

---

Kill Criteria

1. After Wave 2: If RAG++ quality drops below 80
pruned_at) and reassess pruning rules. Do not proceed to Wave 3.

2. After Wave 4: If hierarchy clustering produces <10 meaningful clusters (>80
are labeled "miscellaneous" or "unknown"), the embedding quality is insufficient. Pause
hierarchy and investigate embedding enrichment.

3. After Wave 5: If KARL boost creates >10
>0.1 in 30 days without new triple ingestion), reduce boost rate or disable.

---

Total Estimates

WaveTasksEstimated HoursOwner
Wave 044Claude
Wave 1614Claude
Wave 2716Claude + Mohamed
Wave 358Claude
Wave 4724Claude + Mohamed
Wave 5614Claude + Mohamed
Total35~80 hours

New Rust code: ~600 lines (decay, topology, registry, endpoints)
New Python code: ~500 lines (embedding pipeline, hierarchy discovery, KARL integration)
SQL migrations: ~30 lines
New Supabase table: 1 (predicate_registry)
New Prefect flows: 1 (weekly hierarchy re-discovery)
Modified files: ~8 Rust files, ~2 Python files

---

Rollback Plan

Every wave has an explicit rollback:

- Wave 0: DROP columns (updated_at, pruned_at, pruned_reason). Revert Rust upsert.
- Wave 1: Disable decay endpoint. Revert traversal to use MIN instead of product.
- Wave 2: `UPDATE knowledge_graph SET pruned_at = NULL, pruned_reason = NULL`.
Revert WHERE clause changes. Takes <5 seconds.
- Wave 3: DROP TABLE predicate_registry. Remove confidence cap. Takes <1 minute.
- Wave 4: Delete hierarchy/similarity triples:
`DELETE FROM knowledge_graph WHERE source IN ('hsae-discovery-v1', 'embedding-similarity-v1')`.
Disable Prefect flow.
- Wave 5: Remove report_to_gk() from KARL. Remove explain mode from traversal.
Delete Prometheus metrics. Revert Nexus page.

Each rollback is independent. Reverting Wave 3 does not require reverting Wave 2.

---

Dependency Map (What Blocks What)

Wave 0.1 (migration) ─────> Wave 0.2 (upsert) ─────> Wave 0.3 (backfill)
                                                            |
                                                      Wave 0.4 (filter)
                                                            |
                                              ┌─────────────┴──────────────┐
                                              |                            |
                                        Wave 1.1-1.6               Wave 2.1-2.3
                                        (Confidence)                (Topology)
                                              |                            |
                                              |                      Wave 2.4-2.7
                                              |                      (Pruning)
                                              |                            |
                                              └────────────┬───────────────┘
                                                           |
                                                     Wave 3.1-3.5
                                                     (Registry)
                                                           |
                                                     Wave 3.5
                                                     (Deploy)
                                                           |
                                              ┌────────────┴───────────────┐
                                              |                            |
                                        Wave 4.1-4.7                Wave 5.1-5.2
                                        (Hierarchy)                  (KARL Loop)
                                              |                            |
                                              └────────────┬───────────────┘
                                                           |
                                                     Wave 5.3-5.6
                                                     (Planner + Obs)

Promotion Decision

Attach run IDs, datasets, metrics, and reproduction commands.

Source Anchor

evo-cube-output/ai-generated-graph-topology/stage3-expand-master-plan.md

Detected Structure

Method · Evaluation · Figures · Code Anchors · Architecture · is Stage Research