@@ -171,6 +171,61 @@ def read_page_text(rel_path: str) -> str:
171171 return okf .dump (page .frontmatter , page .body )
172172
173173
174+ def neighbors_text (rel_path : str ) -> str :
175+ """The link neighborhood of one wiki page — the text behind ``wiki_neighbors`` / ``citadel
176+ neighbors``, so an AI can walk the graph without doing relative-path math itself. Three sections:
177+ **Links out** (this page's resolved wiki cross-links, each flagged ``(missing)`` when it names no
178+ existing page), **Linked from** (the pages that link to this one — the backlink graph), and
179+ **Cites sources** (the distinct raw/docs source keys in its ``## Sources``, with how many
180+ footnotes cite each — the handoff key for ``wiki_raw``). Raises FileNotFoundError (no such page) /
181+ okf.OKFError (unsafe path), which the CLI/MCP surfaces map to an exit code / error string. ONE
182+ ``load()`` powers the target page, the backlink graph, and the link titles — the file is parsed
183+ once, not re-read on top of the corpus scan."""
184+ from . import grammar , linkgraph
185+
186+ okf .safe_join (config .WIKI_DIR , rel_path ) # validate the path (raises okf.OKFError on traversal/escape)
187+ pages = load ()
188+ by_path = {p .rel_path : p for p in pages }
189+ page = by_path .get (rel_path )
190+ if page is None : # safe but absent (or a skipped index.md/log.md) — same not-found contract as read_page
191+ raise FileNotFoundError (rel_path )
192+ titles = {rp : p .title for rp , p in by_path .items ()}
193+
194+ seen : set [str ] = set ()
195+ out_links : list [tuple [str , str | None ]] = []
196+ for _raw , resolved in grammar .resolved_md_links (rel_path , page .body ):
197+ if resolved == rel_path or resolved in seen :
198+ continue
199+ seen .add (resolved )
200+ out_links .append ((resolved , titles .get (resolved )))
201+
202+ inbound = linkgraph .inbound_map (pages ).get (rel_path , [])
203+
204+ cites : dict [str , int ] = {}
205+ for _marker , rest in grammar .source_definitions (page .body ):
206+ target = grammar .def_link_target (rest )
207+ if target is None or grammar .is_external (target ):
208+ continue
209+ abs_path = grammar .link_abs (rel_path , target )
210+ key = config .rel_or_abs_posix (abs_path ) if abs_path else target
211+ cites [key ] = cites .get (key , 0 ) + 1
212+
213+ lines = [f"# Neighbors of { rel_path } — { page .title } " , "" ]
214+ lines .append (f"## Links out ({ len (out_links )} )" )
215+ lines += [f"- { resolved } — { title if title is not None else '(missing)' } " for resolved , title in out_links ] or [
216+ "- (none)"
217+ ]
218+ lines .append ("" )
219+ lines .append (f"## Linked from ({ len (inbound )} )" )
220+ lines += [f"- { src } — { titles .get (src , '' )} " for src in inbound ] or ["- (none)" ]
221+ lines .append ("" )
222+ lines .append (f"## Cites sources ({ len (cites )} )" )
223+ lines += [f"- { key } — { cites [key ]} citation{ 's' if cites [key ] != 1 else '' } " for key in sorted (cites )] or [
224+ "- (none)"
225+ ]
226+ return "\n " .join (lines ) + "\n "
227+
228+
174229def index_text () -> str :
175230 """The generated ``wiki/index.md`` catalog text. Raises FileNotFoundError when no index exists
176231 yet (nothing ingested), or an OS error when the path is unreadable."""
0 commit comments