#!/usr/bin/env -S uv run # /// script # requires-python = ">=3.11" # dependencies = ["gremlinpython", "python-dotenv"] # /// """ analyze_concept_graph.py — Graph topology analysis for cornfield concepts. Queries: Q1: Degree distribution (out + in) for all cornfield concepts Q2: Bridge concepts (relates_to degree > 2) Q3: Missing depends_on edges (has lean module but no edge) Q4: Active Lean modules most referenced by cornfield concepts Q5: relates_to pairs among cornfield concepts (cluster analysis) """ import os import sys from pathlib import Path from dotenv import load_dotenv load_dotenv(Path(__file__).parent.parent / ".env.gremlin") from gremlin_python.driver import client as gc, serializer c = gc.Client( os.environ["GREMLIN_ENDPOINT"], "g", username=os.environ["GREMLIN_USERNAME"], password=os.environ["GREMLIN_PASSWORD"], message_serializer=serializer.GraphSONSerializersV2d0(), ) def q(query, bindings=None): try: result = c.submitAsync(query, bindings or {}).result().all().result() return result except Exception as e: print(f" [ERROR] {e}") return [] SEP = "═" * 68 print(SEP) print("Q1: CORNFIELD CONCEPT DEGREE DISTRIBUTION") print("─" * 68) r = q( "g.V().has('cohort','cornfield')" ".project('vid','out_deg','in_deg')" ".by(id())" ".by(outE().count())" ".by(inE().count())" ) if r: # Sort by total degree desc rows = sorted(r, key=lambda x: x['out_deg'] + x['in_deg']) print(f" {'ID':<40} {'out':>4} {'in':>4} {'total':>6}") print(" " + "-" * 60) isolated = [] for row in rows: vid = str(row['vid']) out_d = row['out_deg'] in_d = row['in_deg'] total = out_d + in_d flag = " *** ISOLATED" if total <= 1 else (" * LOW" if total <= 2 else "") print(f" {vid:<40} {out_d:>4} {in_d:>4} {total:>6}{flag}") if total <= 1: isolated.append(vid) print() print(f" Total cornfield concepts queried: {len(rows)}") print(f" Isolated (degree <= 1): {isolated}") else: print(" No results or error.") print() print(SEP) print("Q2: BRIDGE CONCEPTS (relates_to out-degree > 2)") print("─" * 68) r2 = q( "g.V().has('cohort','cornfield')" ".filter(outE('relates_to').count().is(gt(2)))" ".project('vid','rt_count')" ".by(id())" ".by(outE('relates_to').count())" ".order().by(select('rt_count'), decr)" ) if r2: for row in r2: print(f" {str(row['vid']):<40} relates_to_out={row['rt_count']}") else: print(" None found (no concept has > 2 outgoing relates_to edges).") print() print(SEP) print("Q3: MISSING depends_on EDGES (has lean module, no depends_on edge)") print("─" * 68) r3 = q( "g.V().has('cohort','cornfield')" ".has('lean_status', within('LIBRARY','ARCHIVED','PARTIALLY_ACTIVE','PROVEN','DEFINED'))" ".not(outE('depends_on'))" ".project('vid','lean_status')" ".by(id())" ".by('lean_status')" ) if r3: for row in r3: print(f" {str(row['vid']):<40} lean_status={row.get('lean_status','?')}") print(f"\n Count: {len(r3)} concepts with Lean modules but no depends_on edge") else: print(" All concepts with Lean modules already have depends_on edges, or none found.") print() print(SEP) print("Q4: TOP ACTIVE LEAN MODULES (most cornfield depends_on edges incoming)") print("─" * 68) r4 = q( "g.V().has('cohort','cornfield').out('depends_on')" ".hasLabel('module')" ".groupCount().by(id())" ".order(local).by(values, decr).limit(local, 15)" ) if r4 and len(r4) > 0: result_map = r4[0] if isinstance(r4[0], dict) else {} for mod_id, count in sorted(result_map.items(), key=lambda x: -x[1]): print(f" {str(mod_id):<55} count={count}") else: print(" No depends_on edges to modules found, or error.") print() print(SEP) print("Q5: CORNFIELD CONCEPT CLUSTER ANALYSIS (relates_to pairs)") print("─" * 68) r5 = q( "g.V().has('cohort','cornfield').as('a')" ".out('relates_to').has('cohort','cornfield').as('b')" ".select('a','b').by(id())" ) if r5: print(f" Total relates_to edges between cornfield concepts: {len(r5)}") print() # Build adjacency for cluster analysis adj = {} for row in r5: a = str(row['a']) b = str(row['b']) adj.setdefault(a, set()).add(b) adj.setdefault(b, set()).add(a) print(" Pairs found:") for row in r5: a = str(row['a']) b = str(row['b']) print(f" {a:<40} → {b}") print() # Find concepts with multiple relates_to neighbors print(" Cluster hubs (2+ cornfield neighbours):") for node, neighbors in sorted(adj.items(), key=lambda x: -len(x[1])): if len(neighbors) >= 2: print(f" {node:<40} ({len(neighbors)} neighbors): {sorted(neighbors)[:5]}") else: print(" No relates_to edges between cornfield concepts found in graph.") print() print(SEP) print("Q6: ALL EDGE LABELS ON CORNFIELD VERTICES (to see what edges exist)") print("─" * 68) r6 = q( "g.V().has('cohort','cornfield').bothE()" ".groupCount().by(label())" ) if r6 and r6[0]: for label, count in sorted(r6[0].items()): print(f" {label:<30} count={count}") else: print(" No edges found on cornfield concepts.") print() print(SEP) print("Q7: FULL DEGREE TABLE SORTED BY TOTAL (ascending — find lowest)") print("─" * 68) # Show all vertex IDs with their property values to match to concept names r7 = q( "g.V().has('cohort','cornfield')" ".project('vid','name','lean_status','tags')" ".by(id())" ".by(coalesce(values('name'), constant('?')))" ".by(coalesce(values('lean_status'), constant('?')))" ".by(coalesce(values('tags'), constant('?')))" ) if r7: print(f" Total concepts: {len(r7)}") for row in r7: print(f" id={str(row['vid']):<45} name={str(row.get('name','?')):<40} status={str(row.get('lean_status','?')):<25} tags={str(row.get('tags','?'))[:60]}") else: print(" No property results.") c.close() print() print("Done.")