@@ -33,6 +33,9 @@ struct EmittedSymbol {
3333 /// `[text](#frag)`). Linked as bidirectional `RelatesTo` edges against doc
3434 /// anchor fragments only.
3535 refs : Vec < String > ,
36+ /// `include::` document-composition targets (raw file paths from the
37+ /// `doc_includes` attribute). Linked as directional `Requires` edges.
38+ includes : Vec < String > ,
3639 /// `edge::implements[Trait::method]` references (trait impls) — linked as
3740 /// `Implements` edges so blast radius reaches implementors (Wave 1).
3841 implements : Vec < String > ,
@@ -220,6 +223,76 @@ fn extract_doc_refs(doc: &aden_core::Document) -> Vec<String> {
220223 extract_joined_attribute ( doc, "doc_refs" )
221224}
222225
226+ /// Document-composition targets, read from the `doc_includes` attribute the
227+ /// AsciiDoc parser fills for `include::` directives. Resolved file-wise (by
228+ /// stem) to directional `Requires` edges in [`link_include_edges`].
229+ fn extract_doc_includes ( doc : & aden_core:: Document ) -> Vec < String > {
230+ extract_joined_attribute ( doc, "doc_includes" )
231+ }
232+
233+ /// Resolve `include::` directives (the `doc_includes` channel) into directional
234+ /// `Requires` edges: an including document depends on each file it pulls in.
235+ /// Resolution is file-wise by stem (an include names a file, not a fragment),
236+ /// pointing at that file's representative (first) doc node. Runs as a separate
237+ /// additive pass after `link_store_edges`; `put_edges_bulk` appends, so edges
238+ /// already written are kept.
239+ fn link_include_edges < S : GraphStorage > (
240+ storage : & S ,
241+ include_records : & [ ( String , Vec < String > ) ] ,
242+ ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
243+ use aden_core:: EdgeType ;
244+ if include_records. is_empty ( ) {
245+ return Ok ( ( ) ) ;
246+ }
247+ let anchors = storage. get_all_anchors ( ) ?;
248+ // File stem -> declaring doc anchors (sorted/deduped for deterministic picks).
249+ let mut stem_index: HashMap < & str , Vec < & str > > = HashMap :: new ( ) ;
250+ for anchor in & anchors {
251+ if let Some ( file_key) = doc_anchor_file ( anchor)
252+ && let Some ( stem) = std:: path:: Path :: new ( file_key)
253+ . file_stem ( )
254+ . and_then ( |s| s. to_str ( ) )
255+ {
256+ stem_index. entry ( stem) . or_default ( ) . push ( anchor. as_str ( ) ) ;
257+ }
258+ }
259+ for cands in stem_index. values_mut ( ) {
260+ cands. sort_unstable ( ) ;
261+ cands. dedup ( ) ;
262+ }
263+ let mut edges: Vec < ( String , String , EdgeType ) > = Vec :: new ( ) ;
264+ for ( referrer, targets) in include_records {
265+ let ref_file = doc_anchor_file ( referrer) ;
266+ for target in targets {
267+ let Some ( stem) = std:: path:: Path :: new ( target)
268+ . file_stem ( )
269+ . and_then ( |s| s. to_str ( ) )
270+ else {
271+ continue ;
272+ } ;
273+ let Some ( cands) = stem_index. get ( stem) else {
274+ continue ;
275+ } ;
276+ // An include points at ANOTHER document: prefer a candidate in a
277+ // different file than the referrer; else the first (deterministic).
278+ let target_anchor = cands
279+ . iter ( )
280+ . copied ( )
281+ . find ( |a| doc_anchor_file ( a) != ref_file)
282+ . or_else ( || cands. first ( ) . copied ( ) ) ;
283+ if let Some ( ta) = target_anchor
284+ && ta != referrer. as_str ( )
285+ {
286+ edges. push ( ( referrer. clone ( ) , ta. to_string ( ) , EdgeType :: Requires ) ) ;
287+ }
288+ }
289+ }
290+ if !edges. is_empty ( ) {
291+ storage. put_edges_bulk ( & edges) ?;
292+ }
293+ Ok ( ( ) )
294+ }
295+
223296/// Backtick prose mentions, read from the `doc_mentions` attribute the format
224297/// parsers fill (Wave 2). Same division of labor as `doc_refs`: the parser
225298/// knows fence/backtick state; resolution in [`link_store_edges`] is
@@ -1500,6 +1573,7 @@ fn cmd_gen_inner(
15001573 let callees = extract_callees ( & doc_clone) ;
15011574 let uses = extract_uses ( & doc_clone) ;
15021575 let refs = extract_doc_refs ( & doc_clone) ;
1576+ let includes = extract_doc_includes ( & doc_clone) ;
15031577 let implements = extract_edge_macro ( & doc_clone, "implements" ) ;
15041578 let mutates = extract_edge_macro ( & doc_clone, "mutates" ) ;
15051579 let mentions = extract_doc_mentions ( & doc_clone) ;
@@ -1549,6 +1623,7 @@ fn cmd_gen_inner(
15491623 callees,
15501624 uses,
15511625 refs,
1626+ includes,
15521627 implements,
15531628 mutates,
15541629 mentions,
@@ -1581,6 +1656,7 @@ fn cmd_gen_inner(
15811656 let mut link_records: Vec < ( String , Vec < String > ) > = Vec :: new ( ) ;
15821657 let mut use_records: Vec < ( String , Vec < String > ) > = Vec :: new ( ) ;
15831658 let mut ref_records: Vec < ( String , Vec < String > ) > = Vec :: new ( ) ;
1659+ let mut include_records: Vec < ( String , Vec < String > ) > = Vec :: new ( ) ;
15841660 let mut impl_records: Vec < ( String , Vec < String > ) > = Vec :: new ( ) ;
15851661 let mut mutates_records: Vec < ( String , Vec < String > ) > = Vec :: new ( ) ;
15861662 let mut mention_records: Vec < ( String , Vec < String > ) > = Vec :: new ( ) ;
@@ -1672,6 +1748,9 @@ fn cmd_gen_inner(
16721748 if !sym. refs . is_empty ( ) {
16731749 ref_records. push ( ( sym. anchor . clone ( ) , sym. refs ) ) ;
16741750 }
1751+ if !sym. includes . is_empty ( ) {
1752+ include_records. push ( ( sym. anchor . clone ( ) , sym. includes ) ) ;
1753+ }
16751754 if !sym. uses . is_empty ( ) {
16761755 use_records. push ( ( sym. anchor . clone ( ) , sym. uses ) ) ;
16771756 }
@@ -1792,6 +1871,13 @@ fn cmd_gen_inner(
17921871 }
17931872 } ;
17941873
1874+ // Additive second pass: include:: directives -> Requires edges. Kept
1875+ // separate from link_store_edges (which works from anchors only); this
1876+ // resolves file-wise and put_edges_bulk appends, so prior edges persist.
1877+ if let Err ( e) = link_include_edges ( & storage, & include_records) {
1878+ eprintln ! ( "WARN: Failed to link include edges: {}" , e) ;
1879+ }
1880+
17951881 save_gen_cache ( & cache_path, & cache) ?;
17961882
17971883 // The summary is "summary only" output: shown under --quiet/regen, but
@@ -2292,6 +2378,44 @@ mod link_tests {
22922378 /// Prose `ref:` records resolve format-neutrally against DOC anchor
22932379 /// fragments only — bidirectional `RelatesTo` — and must NEVER attach to a
22942380 /// same-named code symbol (the fuzzy code path is off-limits to prose).
2381+ #[ test]
2382+ fn include_records_link_requires_edges ( ) {
2383+ use aden_core:: { Block , Document , EdgeType , NodeType } ;
2384+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
2385+ let storage = Storage :: new ( dir. path ( ) . to_str ( ) . unwrap ( ) ) . unwrap ( ) ;
2386+ let mk = |anchor : & str | Document {
2387+ anchor : anchor. into ( ) ,
2388+ node_type : NodeType :: Module ,
2389+ attributes : Default :: default ( ) ,
2390+ blocks : vec ! [ Block :: Paragraph ( "body" . into( ) ) ] ,
2391+ source_span : None ,
2392+ metadata : None ,
2393+ confidence : 1.0 ,
2394+ } ;
2395+ // A master doc and a chapter it includes, each a heading-section node.
2396+ let master = "aden://doc/p/master.adoc/h1master" ;
2397+ let chapter = "aden://doc/p/chapter-one.adoc/h1chapter" ;
2398+ for a in [ master, chapter] {
2399+ storage. put_document ( & mk ( a) ) . unwrap ( ) ;
2400+ }
2401+ storage. flush ( ) . unwrap ( ) ;
2402+
2403+ let include_records = vec ! [ ( master. to_string( ) , vec![ "chapter-one.adoc" . to_string( ) ] ) ] ;
2404+ link_include_edges ( & storage, & include_records) . unwrap ( ) ;
2405+
2406+ let out = storage. get_outgoing_edges ( master) . unwrap ( ) ;
2407+ assert ! (
2408+ out. contains( & ( chapter. to_string( ) , EdgeType :: Requires ) ) ,
2409+ "master must Requires the included chapter doc node; got {out:?}"
2410+ ) ;
2411+ // Directional: the chapter does not Requires the master.
2412+ let back = storage. get_outgoing_edges ( chapter) . unwrap ( ) ;
2413+ assert ! (
2414+ !back. iter( ) . any( |( t, _) | t == master) ,
2415+ "include edge must be directional (master->chapter only); got {back:?}"
2416+ ) ;
2417+ }
2418+
22952419 #[ test]
22962420 fn prose_ref_records_link_doc_anchors_only ( ) {
22972421 use aden_core:: { Block , Document , EdgeType , NodeType } ;
@@ -2509,6 +2633,33 @@ mod link_tests {
25092633 ) ;
25102634 }
25112635
2636+ #[ test]
2637+ fn extract_doc_includes_reads_doc_includes_attribute ( ) {
2638+ use aden_core:: { Block , Document , NodeType } ;
2639+ let mut attrs = std:: collections:: HashMap :: new ( ) ;
2640+ attrs. insert (
2641+ "doc_includes" . to_string ( ) ,
2642+ "chapter-two.adoc,chapter-one.adoc,chapter-two.adoc" . to_string ( ) ,
2643+ ) ;
2644+ let doc = Document {
2645+ anchor : "aden://doc/p/master.adoc/h2overview" . into ( ) ,
2646+ node_type : NodeType :: Module ,
2647+ attributes : attrs,
2648+ blocks : vec ! [ Block :: Paragraph ( "body" . into( ) ) ] ,
2649+ source_span : None ,
2650+ metadata : None ,
2651+ confidence : 1.0 ,
2652+ } ;
2653+ assert_eq ! (
2654+ extract_doc_includes( & doc) ,
2655+ vec![
2656+ "chapter-one.adoc" . to_string( ) ,
2657+ "chapter-two.adoc" . to_string( )
2658+ ] ,
2659+ "sorted, deduped, attribute-sourced"
2660+ ) ;
2661+ }
2662+
25122663 #[ test]
25132664 fn extract_uses_reads_edge_uses_listings ( ) {
25142665 use aden_core:: { Block , Document , NodeType } ;
0 commit comments