Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -746,13 +746,22 @@ graphify extract ./docs --google-workspace # export .gdoc/.gsheet/.gslides v
graphify extract ./src --no-gitignore # include git-ignored source; still honor .graphifyignore
graphify extract ./docs --mode deep # richer semantic extraction via extended system prompt
graphify extract ./docs --no-cluster # raw extraction only, skip clustering
graphify extract ./src --code-only --multigraph --out D:/graphs/my-project # preserve parallel edges outside the repo
graphify extract ./docs --timing # print per-stage wall-clock timings to stderr (also works on cluster-only)
graphify extract ./docs --force # overwrite graph.json even if new graph has fewer nodes (use after refactors or to clear ghost duplicates)
graphify extract ./docs --dedup-llm # LLM tiebreaker for ambiguous entity pairs (uses same API key)
graphify extract ./src --no-dedup # skip entity dedup; on an incremental merge this also arms the shrink guard that refuses to drop untouched files' nodes
graphify extract ./docs --global --as myrepo # extract and register into the cross-project global graph
GRAPHIFY_MAX_OUTPUT_TOKENS=32768 graphify extract ./docs --backend claude # raise output cap for dense corpora

`--multigraph` stores the authoritative graph as a directed `MultiDiGraph`, so
distinct relations and source locations between the same ordered node pair are
not overwritten. `--out` can point anywhere; Graphify remembers an explicit
selection per scanned source root under `~/.graphify/output-roots/`. Later
`extract`, `update`, watch, query, and export commands reuse that location, and
another explicit `--out` replaces it. Build settings such as `--multigraph` are
saved with the graph. The scanned repository remains unchanged.

graphify export callflow-html # graphify-out/<project>-callflow.html
graphify export callflow-html --max-sections 8 # cap generated architecture sections
graphify export callflow-html --output docs/arch.html
Expand Down
33 changes: 18 additions & 15 deletions graphify/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,12 +510,12 @@ def _run_cli() -> None:
print(" uninstall remove graphify from all detected platforms in one shot")
print(" --purge also delete graphify-out/ directory")
print(" path \"A\" \"B\" shortest path between two nodes in graph.json")
print(" --graph <path> path to graph.json (default graphify-out/graph.json)")
print(" --graph <path> path to graph.json (default: configured source output)")
print(" explain \"X\" plain-language explanation of a node and its neighbors")
print(" --graph <path> path to graph.json (default graphify-out/graph.json)")
print(" --graph <path> path to graph.json (default: configured source output)")
print(" diagnose multigraph report same-endpoint edge collapse risk in graph.json")
print(" --graph <path> path to graph/extraction JSON")
print(" (default graphify-out/graph.json)")
print(" (default: configured source output)")
print(" --json emit machine-readable JSON")
print(" --max-examples N max same-endpoint examples to print (default 5)")
print(" --directed force directed post-build simulation")
Expand All @@ -538,9 +538,10 @@ def _run_cli() -> None:
print(" --force overwrite graph.json even if the rebuild has fewer nodes")
print(" (also: GRAPHIFY_FORCE=1 env var; use after refactors that delete code)")
print(" --no-cluster skip clustering, write raw extraction only")
print(" --multigraph enable and persist parallel directed edge storage")
print(" cluster-only <path> rerun clustering on an existing graph.json and regenerate report")
print(" --no-viz skip graph.html generation (useful for >5000 node graphs / CI)")
print(" --graph <path> path to graph.json (default <path>/graphify-out/graph.json)")
print(" --graph <path> path to graph.json (default: configured source output)")
print(" --no-label keep 'Community N' placeholders (skip LLM community naming)")
print(" --backend=<name> backend to use for community naming (default: auto-detect)")
print(" --model=<name> model to use for community naming")
Expand All @@ -556,16 +557,16 @@ def _run_cli() -> None:
print(" --dfs use depth-first instead of breadth-first")
print(" --context C explicit edge-context filter (repeatable)")
print(" --budget N cap output at N tokens (default 2000)")
print(" --graph <path> path to graph.json (default graphify-out/graph.json)")
print(" --graph <path> path to graph.json (default: configured source output)")
print(" affected \"X\" reverse traversal to find nodes impacted by X")
print(" --relation R edge relation to traverse in reverse (repeatable)")
print(" --depth N reverse traversal depth (default 2)")
print(" --graph <path> path to graph.json (default graphify-out/graph.json)")
print(" --graph <path> path to graph.json (default: configured source output)")
print(" god-nodes list the most connected nodes (architectural hubs)")
print(" --top N how many to show (default 10)")
print(" --graph <path> path to graph.json (default graphify-out/graph.json)")
print(" --graph <path> path to graph.json (default: configured source output)")
print(" --json emit JSON instead of text")
print(" save-result save a Q&A result to graphify-out/memory/ for graph feedback loop")
print(" save-result save a Q&A result to the configured output memory")
print(" --question Q the question asked")
print(" --answer A the answer to save")
print(
Expand All @@ -574,19 +575,19 @@ def _run_cli() -> None:
print(" --nodes N1 N2 ... source node labels cited in the answer")
print(" --outcome O work-memory signal: useful|dead_end|corrected")
print(" --correction TEXT what the right answer was (pairs with --outcome corrected)")
print(" --memory-dir DIR memory directory (default: graphify-out/memory)")
print(" reflect aggregate graphify-out/memory/ outcomes into a deterministic lessons doc")
print(" --memory-dir DIR memory directory (default: graphify-out/memory)")
print(" --out FILE output path (default: graphify-out/reflections/LESSONS.md)")
print(" --memory-dir DIR memory directory (default: configured source output)")
print(" reflect aggregate configured output memory into a deterministic lessons doc")
print(" --memory-dir DIR memory directory (default: configured source output)")
print(" --out FILE output path (default: configured source output)")
print(" --graph PATH graph.json, for community grouping + dropping stale nodes (optional)")
print(" --analysis PATH .graphify_analysis.json (optional, auto-detected next to --graph)")
print(" --labels PATH .graphify_labels.json (optional, auto-detected next to --graph)")
print(" --half-life-days N signal weight halves every N days (default 30)")
print(" --min-corroboration N distinct useful results to prefer a node (default 2)")
print(" check-update <path> check needs_update flag and notify if semantic re-extraction is pending (cron-safe)")
print(" tree emit a D3 v7 collapsible-tree HTML for graph.json")
print(" --graph PATH path to graph.json (default graphify-out/graph.json)")
print(" --output HTML output path (default graphify-out/GRAPH_TREE.html)")
print(" --graph PATH path to graph.json (default: configured source output)")
print(" --output HTML output path (default: configured source output)")
print(" --root PATH filesystem root for the hierarchy")
print(" --max-children N cap children per node (default 200)")
print(" --top-k-edges N per-symbol outbound edges in inspector (default 12)")
Expand All @@ -606,10 +607,12 @@ def _run_cli() -> None:
print(" --token-budget N per-chunk token cap for semantic extraction (default: 60000)")
print(" --max-concurrency N parallel semantic chunks in flight (default: 4; set 1 for local LLMs)")
print(" --api-timeout S per-request timeout in seconds for the LLM client (default: 600)")
print(" --out DIR, --output DIR output dir (default: <path>); writes <DIR>/graphify-out/")
print(" --out DIR, --output DIR output dir; remembered per source root")
print(" writes <DIR>/graphify-out/ (default: <path>)")
print(" --google-workspace export .gdoc/.gsheet/.gslides shortcuts via gws before extraction")
print(" --no-gitignore ignore .gitignore and .git/info/exclude (prioritizes .graphifyignore)")
print(" --no-cluster skip clustering, write raw extraction only")
print(" --multigraph preserve parallel directed edge records (persisted per output)")
print(" --code-only index code (local AST, no API key) and skip doc/paper/image files")
print(" --postgres DSN extract schema from a live PostgreSQL database")
print(" maps tables, views, functions + FK relationships;")
Expand Down
25 changes: 17 additions & 8 deletions graphify/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from pathlib import Path
import networkx as nx

from graphify.build import edge_data
from graphify.build import analysis_projection, edge_data

# Builtin/mock names that can appear as annotation-derived nodes in pre-existing
# graphs. Excluded from god-node ranking so they don't displace real abstractions
Expand Down Expand Up @@ -112,7 +112,8 @@ def god_nodes(G: nx.Graph, top_n: int = 10) -> list[dict]:
File-level hub nodes are excluded: they accumulate import/contains edges
mechanically and don't represent meaningful architectural abstractions.
"""
degree = dict(G.degree())
topology = analysis_projection(G)
degree = {node: topology.degree(node) for node in G.nodes}
sorted_nodes = sorted(degree.items(), key=lambda x: x[1], reverse=True)
result = []
for node_id, deg in sorted_nodes:
Expand Down Expand Up @@ -289,7 +290,7 @@ def _cross_file_surprises(G: nx.Graph, communities: dict[int, list[str]], top_n:
Each result includes a 'why' field explaining what makes it non-obvious.
"""
node_community = _node_community_map(communities)
degrees = dict(G.degree())
degrees = dict(analysis_projection(G).degree())
candidates = []

for u, v, data in G.edges(data=True):
Expand Down Expand Up @@ -355,17 +356,25 @@ def _cross_community_surprises(
return []
if G.number_of_nodes() > 5000:
return []
betweenness = nx.edge_betweenness_centrality(G)
betweenness = nx.edge_betweenness_centrality(analysis_projection(G))
top_edges = sorted(betweenness.items(), key=lambda x: x[1], reverse=True)[:top_n]
result = []
for (u, v), score in top_edges:
if G.is_directed() and not G.has_edge(u, v):
u, v = v, u
data = edge_data(G, u, v)
src_id = data.get("_src", u)
if src_id not in G.nodes:
src_id = u
tgt_id = data.get("_tgt", v)
if tgt_id not in G.nodes:
tgt_id = v
result.append({
"source": G.nodes[u].get("label", u),
"target": G.nodes[v].get("label", v),
"source": G.nodes[src_id].get("label", src_id),
"target": G.nodes[tgt_id].get("label", tgt_id),
"source_files": [
G.nodes[u].get("source_file", ""),
G.nodes[v].get("source_file", ""),
G.nodes[src_id].get("source_file", ""),
G.nodes[tgt_id].get("source_file", ""),
],
"confidence": data.get("confidence", "EXTRACTED"),
"relation": data.get("relation", ""),
Expand Down
Loading