Skip to content

Commit bf8abe7

Browse files
committed
[Codegraph] Add Obsidian entrypoints dashboard
Add ranked human and agent starting points to the Obsidian export Surface top features, layers, files, modules, and research links Cover entrypoint navigation with generic fixtures and field validation
1 parent d34fd42 commit bf8abe7

4 files changed

Lines changed: 154 additions & 11 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- Incremental refresh cache for safe changed-file rebuilds.
1212
- Extractor capability declarations in the manifest.
1313
- Markdown concept and claim extraction with inferred confidence.
14+
- Obsidian entrypoints dashboard for human and agent starts.
1415
- Obsidian research dashboard and indexes for concepts and claims.
1516
- Obsidian dashboards for architecture, features, and layers.
1617
- Operator commands for export rebuilds, Obsidian export path discovery, and

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ Important sections:
281281

282282
- `index.md`: freshness, quality, totals, and navigation.
283283
- `Dashboards/Architecture.md`: ranked architecture summary.
284+
- `Dashboards/Entrypoints.md`: recommended starts for humans and agents.
284285
- `Dashboards/Features.md`: ranked feature summary.
285286
- `Dashboards/Layers.md`: ranked layer summary.
286287
- `Dashboards/Research.md`: concepts and claims from documentation/research.

‎src/codegraph/scanner.py‎

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,7 @@ def write_obsidian_export(path: Path, graph_payload: dict[str, Any], manifest: d
798798
"## Navigation",
799799
"",
800800
"- [[Dashboards/Architecture|Architecture Dashboard]]",
801+
"- [[Dashboards/Entrypoints|Entrypoints Dashboard]]",
801802
"- [[Dashboards/Features|Feature Dashboard]]",
802803
"- [[Dashboards/Layers|Layer Dashboard]]",
803804
"- [[Dashboards/Research|Research Dashboard]]",
@@ -1025,12 +1026,22 @@ def write_obsidian_dashboards(
10251026
dashboards_dir = path / "Dashboards"
10261027
dashboards_dir.mkdir(exist_ok=True)
10271028
incoming_by_target: dict[str, list[dict[str, Any]]] = defaultdict(list)
1029+
outgoing_by_source: dict[str, list[dict[str, Any]]] = defaultdict(list)
10281030
for edge in graph_payload["edges"]:
10291031
incoming_by_target[edge["to"]].append(edge)
1032+
outgoing_by_source[edge["from"]].append(edge)
10301033
nodes_by_kind: dict[str, list[dict[str, Any]]] = defaultdict(list)
10311034
for node in graph_payload["nodes"]:
10321035
nodes_by_kind[node["kind"]].append(node)
10331036

1037+
write_entrypoints_dashboard(
1038+
dashboards_dir / "Entrypoints.md",
1039+
graph_payload,
1040+
node_note_paths,
1041+
nodes_by_kind,
1042+
incoming_by_target,
1043+
outgoing_by_source,
1044+
)
10341045
write_architecture_dashboard(
10351046
dashboards_dir / "Architecture.md",
10361047
graph_payload,
@@ -1112,6 +1123,73 @@ def write_ranked_architecture_dashboard(
11121123
path.write_text("\n".join(lines), encoding="utf-8")
11131124

11141125

1126+
def write_entrypoints_dashboard(
1127+
path: Path,
1128+
graph_payload: dict[str, Any],
1129+
node_note_paths: dict[str, str],
1130+
nodes_by_kind: dict[str, list[dict[str, Any]]],
1131+
incoming_by_target: dict[str, list[dict[str, Any]]],
1132+
outgoing_by_source: dict[str, list[dict[str, Any]]],
1133+
) -> None:
1134+
lines = [
1135+
"# Entrypoints Dashboard",
1136+
"",
1137+
"## Agent Sequence",
1138+
"",
1139+
"- Run `status` before trusting the graph.",
1140+
"- Run `doctor` before using the graph for planning.",
1141+
"- Start with architecture, feature, layer, and important file entrypoints.",
1142+
"- Use focused source inspection after the graph narrows the area.",
1143+
"",
1144+
"## Top Features",
1145+
"",
1146+
]
1147+
lines.extend(
1148+
render_ranked_architecture_links(
1149+
nodes_by_kind["feature"],
1150+
node_note_paths,
1151+
incoming_by_target,
1152+
limit=12,
1153+
)
1154+
)
1155+
lines.extend(["", "## Top Layers", ""])
1156+
lines.extend(
1157+
render_ranked_architecture_links(
1158+
nodes_by_kind["layer"],
1159+
node_note_paths,
1160+
incoming_by_target,
1161+
limit=12,
1162+
)
1163+
)
1164+
lines.extend(["", "## Important Files", ""])
1165+
lines.extend(
1166+
render_ranked_file_links(
1167+
nodes_by_kind["file"],
1168+
node_note_paths,
1169+
incoming_by_target,
1170+
outgoing_by_source,
1171+
limit=20,
1172+
)
1173+
)
1174+
lines.extend(["", "## External Modules", ""])
1175+
lines.extend(
1176+
render_ranked_links(
1177+
nodes_by_kind["module"],
1178+
node_note_paths,
1179+
incoming_by_target,
1180+
edge_kinds={"imports"},
1181+
empty_label="None",
1182+
limit=20,
1183+
suffix="importing edges",
1184+
)
1185+
)
1186+
if nodes_by_kind["concept"] or nodes_by_kind["claim"]:
1187+
lines.extend(["", "## Research", ""])
1188+
lines.append("- [[Dashboards/Research|Research Dashboard]]")
1189+
lines.append("")
1190+
path.write_text("\n".join(lines), encoding="utf-8")
1191+
1192+
11151193
def write_research_dashboard(
11161194
path: Path,
11171195
nodes_by_kind: dict[str, list[dict[str, Any]]],
@@ -1154,6 +1232,39 @@ def write_research_dashboard(
11541232
path.write_text("\n".join(lines), encoding="utf-8")
11551233

11561234

1235+
def render_ranked_file_links(
1236+
files: list[dict[str, Any]],
1237+
node_note_paths: dict[str, str],
1238+
incoming_by_target: dict[str, list[dict[str, Any]]],
1239+
outgoing_by_source: dict[str, list[dict[str, Any]]],
1240+
*,
1241+
limit: int,
1242+
) -> list[str]:
1243+
ranked = sorted(
1244+
files,
1245+
key=lambda node: (
1246+
-semantic_edge_count(node["id"], incoming_by_target, outgoing_by_source),
1247+
node["id"],
1248+
),
1249+
)[:limit]
1250+
if not ranked:
1251+
return ["- None"]
1252+
return [
1253+
f"- [[{node_note_paths[node['id']]}|{node['source_path']}]] "
1254+
f"({semantic_edge_count(node['id'], incoming_by_target, outgoing_by_source)} semantic edges)"
1255+
for node in ranked
1256+
]
1257+
1258+
1259+
def semantic_edge_count(
1260+
node_id: str,
1261+
incoming_by_target: dict[str, list[dict[str, Any]]],
1262+
outgoing_by_source: dict[str, list[dict[str, Any]]],
1263+
) -> int:
1264+
edges = incoming_by_target[node_id] + outgoing_by_source[node_id]
1265+
return sum(1 for edge in edges if edge["kind"] != "contains")
1266+
1267+
11571268
def render_ranked_links(
11581269
nodes: list[dict[str, Any]],
11591270
node_note_paths: dict[str, str],

‎tests/test_codegraph.py‎

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def test_scan_writes_graph_manifest_report_and_obsidian_export(self) -> None:
5353
self.assertTrue((output / "obsidian" / "Indexes" / "Features.md").is_file())
5454
self.assertTrue((output / "obsidian" / "Indexes" / "Layers.md").is_file())
5555
self.assertTrue((output / "obsidian" / "Dashboards" / "Architecture.md").is_file())
56+
self.assertTrue((output / "obsidian" / "Dashboards" / "Entrypoints.md").is_file())
5657
self.assertTrue((output / "obsidian" / "Dashboards" / "Features.md").is_file())
5758
self.assertTrue((output / "obsidian" / "Dashboards" / "Layers.md").is_file())
5859
self.assertTrue(
@@ -109,6 +110,35 @@ def test_scan_writes_graph_manifest_report_and_obsidian_export(self) -> None:
109110
self.assertEqual(open_code, 0)
110111
self.assertEqual(open_stdout.getvalue().strip(), str((output / "obsidian").resolve()))
111112

113+
def test_obsidian_entrypoints_dashboard_ranks_navigation_starts(self) -> None:
114+
with tempfile.TemporaryDirectory() as temp:
115+
root = Path(temp)
116+
target = root / "target"
117+
output = root / "graph"
118+
(target / "src" / "screens" / "Home").mkdir(parents=True)
119+
(target / "src" / "components").mkdir(parents=True)
120+
(target / "src" / "screens" / "Home" / "HomeScreen.tsx").write_text(
121+
"import { Button } from '../../components/Button'\n"
122+
"export function HomeScreen() { return <Button /> }\n",
123+
encoding="utf-8",
124+
)
125+
(target / "src" / "components" / "Button.tsx").write_text(
126+
"import { Clickable } from 'external-ui'\n"
127+
"export function Button() { return <Clickable /> }\n",
128+
encoding="utf-8",
129+
)
130+
131+
scan(ScanOptions(target=target, output=output, export_obsidian=True))
132+
entrypoints = (output / "obsidian" / "Dashboards" / "Entrypoints.md").read_text(
133+
encoding="utf-8"
134+
)
135+
index = (output / "obsidian" / "index.md").read_text(encoding="utf-8")
136+
137+
self.assertIn("[[Dashboards/Entrypoints|Entrypoints Dashboard]]", index)
138+
self.assertIn("## Agent Sequence", entrypoints)
139+
self.assertIn("## Important Files", entrypoints)
140+
self.assertIn("HomeScreen.tsx", entrypoints)
141+
112142
def test_architecture_enrichment_links_roles_layers_and_import_relationships(self) -> None:
113143
with tempfile.TemporaryDirectory() as temp:
114144
root = Path(temp)
@@ -605,28 +635,28 @@ def test_tsx_external_named_imports_are_queryable_and_rendered(self) -> None:
605635
output = root / "graph"
606636
(target / "src").mkdir(parents=True)
607637
(target / "src" / "Screen.tsx").write_text(
608-
"import { Pressable, TouchableOpacity } from 'react-native'\\n"
609-
"export function Screen() { return <TouchableOpacity><Pressable /></TouchableOpacity> }\\n",
638+
"import { Clickable, Surface } from 'external-ui'\\n"
639+
"export function Screen() { return <Surface><Clickable /></Surface> }\\n",
610640
encoding="utf-8",
611641
)
612642
scan(ScanOptions(target=target, output=output))
613643
graph = json.loads((output / "graph.json").read_text(encoding="utf-8"))
614644
node_ids = {node["id"] for node in graph["nodes"]}
615-
self.assertIn("imported-symbol:react-native#TouchableOpacity", node_ids)
616-
self.assertIn("imported-symbol:react-native#Pressable", node_ids)
645+
self.assertIn("imported-symbol:external-ui#Surface", node_ids)
646+
self.assertIn("imported-symbol:external-ui#Clickable", node_ids)
617647
self.assertTrue(
618648
any(
619649
edge["kind"] == "renders"
620650
and edge["from"] == "file:src/Screen.tsx"
621-
and edge["to"] == "imported-symbol:react-native#TouchableOpacity"
651+
and edge["to"] == "imported-symbol:external-ui#Surface"
622652
for edge in graph["edges"]
623653
)
624654
)
625655
self.assertTrue(
626656
any(
627657
edge["kind"] == "renders"
628658
and edge["from"] == "file:src/Screen.tsx"
629-
and edge["to"] == "imported-symbol:react-native#Pressable"
659+
and edge["to"] == "imported-symbol:external-ui#Clickable"
630660
for edge in graph["edges"]
631661
)
632662
)
@@ -662,8 +692,8 @@ def test_js_ts_module_forms_create_navigation_edges(self) -> None:
662692
"} from 'react'\n"
663693
"import type { ButtonProps } from './components/types'\n"
664694
"import { Button } from './components'\n"
665-
"const { TouchableOpacity, Pressable: RNPressable } = require('react-native')\n"
666-
"export default function Screen() { return <><Button /><TouchableOpacity /></> }\n",
695+
"const { Surface, Clickable: PrimaryClickable } = require('external-ui')\n"
696+
"export default function Screen() { return <><Button /><Surface /></> }\n",
667697
encoding="utf-8",
668698
)
669699

@@ -697,16 +727,16 @@ def test_js_ts_module_forms_create_navigation_edges(self) -> None:
697727
),
698728
edges,
699729
)
700-
self.assertIn("imported-symbol:react-native#TouchableOpacity", node_ids)
701-
self.assertIn("imported-symbol:react-native#RNPressable", node_ids)
730+
self.assertIn("imported-symbol:external-ui#Surface", node_ids)
731+
self.assertIn("imported-symbol:external-ui#PrimaryClickable", node_ids)
702732
self.assertIn("lexical-re-export", methods)
703733
self.assertIn("lexical-type-import", methods)
704734
self.assertIn("lexical-require", methods)
705735
self.assertTrue(
706736
any(
707737
edge["kind"] == "renders"
708738
and edge["from"] == "file:src/Screen.tsx"
709-
and edge["to"] == "imported-symbol:react-native#TouchableOpacity"
739+
and edge["to"] == "imported-symbol:external-ui#Surface"
710740
for edge in graph["edges"]
711741
)
712742
)

0 commit comments

Comments
 (0)