@@ -368,6 +368,69 @@ fn parse_strict_json(bytes: &[u8]) -> Result<serde_json::Value, EngramError> {
368368 Ok ( v)
369369}
370370
371+ // ── Reference extraction (`[[slug]]`) ───────────────────────────────────────
372+
373+ /// Extract `[[slug]]` references from a body's free-form text field
374+ /// (`profile` for [`Body::Core`], `value` for [`Body::Memory`]).
375+ ///
376+ /// Per *NIP-AE: References*, references are literal substrings of the form
377+ /// `[[<slug>]]` where `<slug>` matches the *Slugs* grammar. Bare slug-shaped
378+ /// strings without brackets are NOT references. The spec defines no escaping
379+ /// mechanism and no markup-aware exclusion, so this scan is purely textual.
380+ ///
381+ /// Returns the slugs in **first-occurrence order**, deduplicated. Candidates
382+ /// that fail [`validate_slug`] are silently dropped: callers building a
383+ /// reachability graph want only well-formed targets, and an empty `[[]]` or
384+ /// a `[[bogus slug!]]` is treated the same as ordinary text.
385+ ///
386+ /// This function performs no I/O and no allocation beyond the returned
387+ /// `Vec<String>`.
388+ pub fn extract_refs ( body : & str ) -> Vec < String > {
389+ let bytes = body. as_bytes ( ) ;
390+ let mut out: Vec < String > = Vec :: new ( ) ;
391+ let mut i = 0 ;
392+ while i + 3 < bytes. len ( ) {
393+ // Need at least `[[x]]` — 5 bytes — to contain a non-empty payload.
394+ if bytes[ i] == b'[' && bytes[ i + 1 ] == b'[' {
395+ let start = i + 2 ;
396+ // Find the next `]]` after `start`. We do not allow nesting:
397+ // the first `]]` closes the reference. If we hit another `[[`
398+ // before `]]`, restart the scan from that inner `[[` so e.g.
399+ // `[[outer [[mem/x]]` still surfaces `mem/x`.
400+ let mut j = start;
401+ let mut closed = false ;
402+ while j + 1 < bytes. len ( ) {
403+ if bytes[ j] == b'[' && bytes[ j + 1 ] == b'[' {
404+ // Inner `[[` — abandon the outer match, restart there.
405+ break ;
406+ }
407+ if bytes[ j] == b']' && bytes[ j + 1 ] == b']' {
408+ closed = true ;
409+ break ;
410+ }
411+ j += 1 ;
412+ }
413+ if closed {
414+ // SAFETY: `start` and `j` both sit on ASCII bracket
415+ // boundaries; the slice between them is UTF-8 because the
416+ // input is.
417+ let candidate = & body[ start..j] ;
418+ if validate_slug ( candidate) . is_ok ( ) && !out. iter ( ) . any ( |s| s == candidate) {
419+ out. push ( candidate. to_string ( ) ) ;
420+ }
421+ i = j + 2 ;
422+ continue ;
423+ }
424+ // Either we hit an inner `[[` or ran off the end without a
425+ // closing `]]`. Advance past the opening `[[` and keep looking.
426+ i = start;
427+ continue ;
428+ }
429+ i += 1 ;
430+ }
431+ out
432+ }
433+
371434// ── Envelope build / parse ──────────────────────────────────────────────────
372435
373436/// Build a signed `kind:30174` event for a given body.
@@ -831,4 +894,166 @@ mod tests {
831894 let err = build_event ( & agent, & owner. public_key ( ) , & body, 1 ) . unwrap_err ( ) ;
832895 assert ! ( matches!( err, EngramError :: BodyTooLarge ( _) ) ) ;
833896 }
897+
898+ // ── extract_refs ────────────────────────────────────────────────────────
899+
900+ #[ test]
901+ fn extract_refs_empty_body ( ) {
902+ assert ! ( extract_refs( "" ) . is_empty( ) ) ;
903+ }
904+
905+ #[ test]
906+ fn extract_refs_no_refs ( ) {
907+ assert ! ( extract_refs( "plain prose with no brackets" ) . is_empty( ) ) ;
908+ assert ! ( extract_refs( "single [bracket] only" ) . is_empty( ) ) ;
909+ assert ! ( extract_refs( "mem/example without brackets" ) . is_empty( ) ) ;
910+ }
911+
912+ #[ test]
913+ fn extract_refs_basic_memory ( ) {
914+ assert_eq ! (
915+ extract_refs( "see [[mem/example]] for context" ) ,
916+ vec![ "mem/example" . to_string( ) ]
917+ ) ;
918+ }
919+
920+ #[ test]
921+ fn extract_refs_basic_core ( ) {
922+ assert_eq ! (
923+ extract_refs( "rooted at [[core]] yo" ) ,
924+ vec![ "core" . to_string( ) ]
925+ ) ;
926+ }
927+
928+ #[ test]
929+ fn extract_refs_multiple_in_order ( ) {
930+ assert_eq ! (
931+ extract_refs( "[[mem/a]] then [[mem/b]] then [[mem/c]]" ) ,
932+ vec![
933+ "mem/a" . to_string( ) ,
934+ "mem/b" . to_string( ) ,
935+ "mem/c" . to_string( ) ,
936+ ]
937+ ) ;
938+ }
939+
940+ #[ test]
941+ fn extract_refs_dedupes_preserving_first_occurrence ( ) {
942+ assert_eq ! (
943+ extract_refs( "[[mem/a]] [[mem/b]] [[mem/a]] [[mem/c]] [[mem/b]]" ) ,
944+ vec![
945+ "mem/a" . to_string( ) ,
946+ "mem/b" . to_string( ) ,
947+ "mem/c" . to_string( ) ,
948+ ]
949+ ) ;
950+ }
951+
952+ #[ test]
953+ fn extract_refs_nested_segments ( ) {
954+ assert_eq ! (
955+ extract_refs( "see [[mem/notes/2026-05-12]]" ) ,
956+ vec![ "mem/notes/2026-05-12" . to_string( ) ]
957+ ) ;
958+ }
959+
960+ #[ test]
961+ fn extract_refs_spec_fixture_core_profile ( ) {
962+ // Body 4 from the NIP-AE reference vectors.
963+ assert_eq ! (
964+ extract_refs( "test agent. see [[mem/example]] and [[mem/notes/2026-05-12]]." ) ,
965+ vec![
966+ "mem/example" . to_string( ) ,
967+ "mem/notes/2026-05-12" . to_string( ) ,
968+ ]
969+ ) ;
970+ }
971+
972+ #[ test]
973+ fn extract_refs_drops_empty_brackets ( ) {
974+ assert ! ( extract_refs( "[[]]" ) . is_empty( ) ) ;
975+ assert ! ( extract_refs( "ends with [[]] yo" ) . is_empty( ) ) ;
976+ }
977+
978+ #[ test]
979+ fn extract_refs_drops_invalid_slugs ( ) {
980+ // Uppercase, spaces, leading dash, missing `mem/` — all rejected by validate_slug.
981+ assert ! ( extract_refs( "[[Mem/Example]]" ) . is_empty( ) ) ;
982+ assert ! ( extract_refs( "[[mem/with spaces]]" ) . is_empty( ) ) ;
983+ assert ! ( extract_refs( "[[mem/-leading-dash]]" ) . is_empty( ) ) ;
984+ assert ! ( extract_refs( "[[example]]" ) . is_empty( ) ) ;
985+ assert ! ( extract_refs( "[[mem/]]" ) . is_empty( ) ) ;
986+ }
987+
988+ #[ test]
989+ fn extract_refs_unclosed_brackets ( ) {
990+ assert ! ( extract_refs( "[[mem/x with no closing" ) . is_empty( ) ) ;
991+ assert_eq ! (
992+ extract_refs( "[[mem/x with no close, but [[mem/y]] is fine" ) ,
993+ vec![ "mem/y" . to_string( ) ]
994+ ) ;
995+ }
996+
997+ #[ test]
998+ fn extract_refs_single_brackets_dont_match ( ) {
999+ assert ! ( extract_refs( "[mem/x]" ) . is_empty( ) ) ;
1000+ assert ! ( extract_refs( "[mem/x] and [mem/y]" ) . is_empty( ) ) ;
1001+ }
1002+
1003+ #[ test]
1004+ fn extract_refs_triple_brackets_match_inner ( ) {
1005+ // `[[[mem/x]]]` — the outer `[[` at position 0 opens; the inner
1006+ // scan walks through `[mem/x` and finds `]]` at positions 8-9, so
1007+ // the candidate is `[mem/x` (with the leading `[`), which fails
1008+ // `validate_slug` and is dropped. Surplus opening brackets without
1009+ // matching `]]` boundaries are noise.
1010+ assert ! ( extract_refs( "[[[mem/x]]]" ) . is_empty( ) ) ;
1011+ // `[[mem/x]]]` — `mem/x` matches; trailing `]` is just text.
1012+ assert_eq ! ( extract_refs( "[[mem/x]]]" ) , vec![ "mem/x" . to_string( ) ] ) ;
1013+ }
1014+
1015+ #[ test]
1016+ fn extract_refs_handles_utf8_around_brackets ( ) {
1017+ assert_eq ! (
1018+ extract_refs( "héllo [[mem/example]] wörld 🎉" ) ,
1019+ vec![ "mem/example" . to_string( ) ]
1020+ ) ;
1021+ }
1022+
1023+ #[ test]
1024+ fn extract_refs_long_slug_at_limit ( ) {
1025+ // Build a slug that's exactly SLUG_MAX_LEN bytes — must extract.
1026+ let segment = "a" . repeat ( 64 ) ;
1027+ // mem/ (4) + segment (64) = 68 — well under the limit, but exercises
1028+ // a long single-segment slug.
1029+ let slug = format ! ( "mem/{segment}" ) ;
1030+ let body = format ! ( "[[{slug}]]" ) ;
1031+ assert_eq ! ( extract_refs( & body) , vec![ slug] ) ;
1032+ }
1033+
1034+ #[ test]
1035+ fn extract_refs_oversized_slug_dropped ( ) {
1036+ // mem/ + 256 bytes = 260 bytes total > SLUG_MAX_LEN (255).
1037+ let slug = format ! ( "mem/{}" , "a" . repeat( 64 ) ) ;
1038+ // Repeat enough segments to bust the cap; each segment is 64+1 = 65 bytes.
1039+ let oversized = format ! (
1040+ "{slug}/{}/{}/{}" ,
1041+ "b" . repeat( 64 ) ,
1042+ "c" . repeat( 64 ) ,
1043+ "d" . repeat( 64 )
1044+ ) ;
1045+ assert ! ( oversized. len( ) > SLUG_MAX_LEN ) ;
1046+ let body = format ! ( "[[{oversized}]]" ) ;
1047+ assert ! ( extract_refs( & body) . is_empty( ) ) ;
1048+ }
1049+
1050+ #[ test]
1051+ fn extract_refs_self_reference_is_allowed ( ) {
1052+ // The function does not know "self"; consumers handle that. We just
1053+ // surface every well-formed `[[slug]]`.
1054+ assert_eq ! (
1055+ extract_refs( "I refer to [[mem/me]] from inside mem/me's value" ) ,
1056+ vec![ "mem/me" . to_string( ) ]
1057+ ) ;
1058+ }
8341059}
0 commit comments