Research-Stack/scripts/analyze_concept_graph.py
allaun 779f4fe413 feat(data): add DB sync scripts and infra tracking
Add concept extraction/orchestration scripts under scripts/ and infra
metadata in infra.json. Generated extraction outputs live under extraction/
and are regenerated from scripts; add extraction/ to .gitignore.
2026-06-22 03:14:09 -05:00

212 lines
6 KiB
Python

#!/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.")