@sprig-and-prose/sprig-universe 0.4.4 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprig-and-prose/sprig-universe",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "type": "module",
5
5
  "description": "Minimal universe parser for sprig",
6
6
  "main": "src/index.js",
@@ -15,6 +15,7 @@
15
15
  "CanonicalUniverse:anthology:NestedAnthology",
16
16
  "CanonicalUniverse:relates:NestedConcept and ExternalConcept",
17
17
  "CanonicalUniverse:anthology:AnthologyWithRelationships",
18
+ "CanonicalUniverse:reference:GlobalReference",
18
19
  "CanonicalUniverse:anthology:ExternalAnthology",
19
20
  "CanonicalUniverse:repository:ExternalRepository"
20
21
  ],
@@ -931,7 +932,10 @@
931
932
  },
932
933
  "aliases": {
933
934
  "other": "book"
934
- }
935
+ },
936
+ "references": [
937
+ "CanonicalUniverse:reference:CanonicalUniverse:concept:ConceptWithDirectReference:main"
938
+ ]
935
939
  },
936
940
  "CanonicalUniverse:concept:ConceptWithReferences": {
937
941
  "id": "CanonicalUniverse:concept:ConceptWithReferences",
@@ -981,7 +985,10 @@
981
985
  },
982
986
  "aliases": {
983
987
  "other": "book"
984
- }
988
+ },
989
+ "references": [
990
+ "CanonicalUniverse:reference:CanonicalUniverse:concept:ConceptWithReferenceInRepository:file1"
991
+ ]
985
992
  },
986
993
  "CanonicalUniverse:anthology:ExternalAnthology": {
987
994
  "id": "CanonicalUniverse:anthology:ExternalAnthology",
@@ -995,14 +1002,14 @@
995
1002
  "source": {
996
1003
  "file": "/home/grumpy/git/sprig-universe/src/universe/universe.prose",
997
1004
  "start": {
998
- "line": 147,
1005
+ "line": 151,
999
1006
  "col": 1,
1000
- "offset": 3023
1007
+ "offset": 3112
1001
1008
  },
1002
1009
  "end": {
1003
- "line": 163,
1010
+ "line": 169,
1004
1011
  "col": 2,
1005
- "offset": 3340
1012
+ "offset": 3463
1006
1013
  }
1007
1014
  },
1008
1015
  "aliases": {
@@ -1017,14 +1024,14 @@
1017
1024
  "source": {
1018
1025
  "file": "/home/grumpy/git/sprig-universe/src/universe/universe.prose",
1019
1026
  "start": {
1020
- "line": 153,
1027
+ "line": 157,
1021
1028
  "col": 3,
1022
- "offset": 3145
1029
+ "offset": 3234
1023
1030
  },
1024
1031
  "end": {
1025
- "line": 160,
1032
+ "line": 164,
1026
1033
  "col": 4,
1027
- "offset": 3284
1034
+ "offset": 3373
1028
1035
  }
1029
1036
  }
1030
1037
  },
@@ -1035,17 +1042,20 @@
1035
1042
  "source": {
1036
1043
  "file": "/home/grumpy/git/sprig-universe/src/universe/universe.prose",
1037
1044
  "start": {
1038
- "line": 162,
1045
+ "line": 166,
1039
1046
  "col": 3,
1040
- "offset": 3288
1047
+ "offset": 3377
1041
1048
  },
1042
1049
  "end": {
1043
- "line": 162,
1050
+ "line": 166,
1044
1051
  "col": 53,
1045
- "offset": 3338
1052
+ "offset": 3427
1046
1053
  }
1047
1054
  }
1048
1055
  }
1056
+ ],
1057
+ "references": [
1058
+ "CanonicalUniverse:reference:GlobalReference"
1049
1059
  ]
1050
1060
  },
1051
1061
  "CanonicalUniverse:container:BookAlias": {
@@ -1060,14 +1070,14 @@
1060
1070
  "source": {
1061
1071
  "file": "/home/grumpy/git/sprig-universe/src/universe/universe.prose",
1062
1072
  "start": {
1063
- "line": 153,
1073
+ "line": 157,
1064
1074
  "col": 3,
1065
- "offset": 3145
1075
+ "offset": 3234
1066
1076
  },
1067
1077
  "end": {
1068
- "line": 160,
1078
+ "line": 164,
1069
1079
  "col": 4,
1070
- "offset": 3284
1080
+ "offset": 3373
1071
1081
  }
1072
1082
  },
1073
1083
  "spelledKind": "bookAlias",
@@ -1083,14 +1093,14 @@
1083
1093
  "source": {
1084
1094
  "file": "/home/grumpy/git/sprig-universe/src/universe/universe.prose",
1085
1095
  "start": {
1086
- "line": 154,
1096
+ "line": 158,
1087
1097
  "col": 5,
1088
- "offset": 3171
1098
+ "offset": 3260
1089
1099
  },
1090
1100
  "end": {
1091
- "line": 154,
1101
+ "line": 158,
1092
1102
  "col": 40,
1093
- "offset": 3206
1103
+ "offset": 3295
1094
1104
  }
1095
1105
  }
1096
1106
  }
@@ -1109,14 +1119,14 @@
1109
1119
  "source": {
1110
1120
  "file": "/home/grumpy/git/sprig-universe/src/universe/universe.prose",
1111
1121
  "start": {
1112
- "line": 154,
1122
+ "line": 158,
1113
1123
  "col": 5,
1114
- "offset": 3171
1124
+ "offset": 3260
1115
1125
  },
1116
1126
  "end": {
1117
- "line": 154,
1127
+ "line": 158,
1118
1128
  "col": 40,
1119
- "offset": 3206
1129
+ "offset": 3295
1120
1130
  }
1121
1131
  },
1122
1132
  "spelledKind": "chapterAlias",
@@ -1134,14 +1144,14 @@
1134
1144
  "source": {
1135
1145
  "file": "/home/grumpy/git/sprig-universe/src/universe/universe.prose",
1136
1146
  "start": {
1137
- "line": 162,
1147
+ "line": 166,
1138
1148
  "col": 3,
1139
- "offset": 3288
1149
+ "offset": 3377
1140
1150
  },
1141
1151
  "end": {
1142
- "line": 162,
1152
+ "line": 166,
1143
1153
  "col": 53,
1144
- "offset": 3338
1154
+ "offset": 3427
1145
1155
  }
1146
1156
  },
1147
1157
  "spelledKind": "chapterAlias",
@@ -1161,16 +1171,20 @@
1161
1171
  "source": {
1162
1172
  "file": "/home/grumpy/git/sprig-universe/src/universe/universe.prose",
1163
1173
  "start": {
1164
- "line": 165,
1174
+ "line": 171,
1165
1175
  "col": 1,
1166
- "offset": 3342
1176
+ "offset": 3465
1167
1177
  },
1168
1178
  "end": {
1169
- "line": 165,
1170
- "col": 47,
1171
- "offset": 3388
1179
+ "line": 175,
1180
+ "col": 2,
1181
+ "offset": 3612
1172
1182
  }
1173
- }
1183
+ },
1184
+ "references": [
1185
+ "CanonicalUniverse:reference:CanonicalUniverse:series:ExternalSeries:external-series",
1186
+ "CanonicalUniverse:reference:GlobalReference"
1187
+ ]
1174
1188
  },
1175
1189
  "CanonicalUniverse:book:ExternalBook": {
1176
1190
  "id": "CanonicalUniverse:book:ExternalBook",
@@ -1183,16 +1197,20 @@
1183
1197
  "source": {
1184
1198
  "file": "/home/grumpy/git/sprig-universe/src/universe/universe.prose",
1185
1199
  "start": {
1186
- "line": 167,
1200
+ "line": 177,
1187
1201
  "col": 1,
1188
- "offset": 3390
1202
+ "offset": 3614
1189
1203
  },
1190
1204
  "end": {
1191
- "line": 167,
1192
- "col": 40,
1193
- "offset": 3429
1205
+ "line": 181,
1206
+ "col": 2,
1207
+ "offset": 3752
1194
1208
  }
1195
- }
1209
+ },
1210
+ "references": [
1211
+ "CanonicalUniverse:reference:CanonicalUniverse:book:ExternalBook:external-book",
1212
+ "CanonicalUniverse:reference:GlobalReference"
1213
+ ]
1196
1214
  },
1197
1215
  "CanonicalUniverse:chapter:ExternalChapter": {
1198
1216
  "id": "CanonicalUniverse:chapter:ExternalChapter",
@@ -1205,16 +1223,20 @@
1205
1223
  "source": {
1206
1224
  "file": "/home/grumpy/git/sprig-universe/src/universe/universe.prose",
1207
1225
  "start": {
1208
- "line": 169,
1226
+ "line": 183,
1209
1227
  "col": 1,
1210
- "offset": 3431
1228
+ "offset": 3754
1211
1229
  },
1212
1230
  "end": {
1213
- "line": 169,
1214
- "col": 44,
1215
- "offset": 3474
1231
+ "line": 187,
1232
+ "col": 2,
1233
+ "offset": 3899
1216
1234
  }
1217
- }
1235
+ },
1236
+ "references": [
1237
+ "CanonicalUniverse:reference:CanonicalUniverse:chapter:ExternalChapter:external-chapter",
1238
+ "CanonicalUniverse:reference:GlobalReference"
1239
+ ]
1218
1240
  },
1219
1241
  "CanonicalUniverse:concept:ExternalConcept": {
1220
1242
  "id": "CanonicalUniverse:concept:ExternalConcept",
@@ -1225,16 +1247,20 @@
1225
1247
  "source": {
1226
1248
  "file": "/home/grumpy/git/sprig-universe/src/universe/universe.prose",
1227
1249
  "start": {
1228
- "line": 171,
1250
+ "line": 189,
1229
1251
  "col": 1,
1230
- "offset": 3476
1252
+ "offset": 3901
1231
1253
  },
1232
1254
  "end": {
1233
- "line": 171,
1234
- "col": 47,
1235
- "offset": 3522
1255
+ "line": 193,
1256
+ "col": 2,
1257
+ "offset": 4049
1236
1258
  }
1237
- }
1259
+ },
1260
+ "references": [
1261
+ "CanonicalUniverse:reference:CanonicalUniverse:concept:ExternalConcept:external-concept",
1262
+ "CanonicalUniverse:reference:GlobalReference"
1263
+ ]
1238
1264
  }
1239
1265
  },
1240
1266
  "edges": [
@@ -1761,14 +1787,14 @@
1761
1787
  "source": {
1762
1788
  "file": "/home/grumpy/git/sprig-universe/src/universe/universe.prose",
1763
1789
  "start": {
1764
- "line": 173,
1790
+ "line": 195,
1765
1791
  "col": 1,
1766
- "offset": 3524
1792
+ "offset": 4051
1767
1793
  },
1768
1794
  "end": {
1769
- "line": 176,
1795
+ "line": 198,
1770
1796
  "col": 2,
1771
- "offset": 3655
1797
+ "offset": 4182
1772
1798
  }
1773
1799
  },
1774
1800
  "title": "GitHub"
@@ -1810,7 +1836,8 @@
1810
1836
  "offset": 2292
1811
1837
  }
1812
1838
  }
1813
- }
1839
+ },
1840
+ "scopeDeclId": "CanonicalUniverse:anthology:AnthologyWithRelationships"
1814
1841
  },
1815
1842
  "CanonicalUniverse:reference:NamedReference2": {
1816
1843
  "id": "CanonicalUniverse:reference:NamedReference2",
@@ -1847,7 +1874,8 @@
1847
1874
  "offset": 2476
1848
1875
  }
1849
1876
  }
1850
- }
1877
+ },
1878
+ "scopeDeclId": "CanonicalUniverse:anthology:AnthologyWithRelationships"
1851
1879
  },
1852
1880
  "CanonicalUniverse:reference:NamedReferenceInRepository": {
1853
1881
  "id": "CanonicalUniverse:reference:NamedReferenceInRepository",
@@ -1874,7 +1902,163 @@
1874
1902
  "path/to/file1.md",
1875
1903
  "path/to/file2.md"
1876
1904
  ],
1905
+ "scopeDeclId": "CanonicalUniverse:anthology:AnthologyWithRelationships",
1877
1906
  "repositoryRef": "CanonicalUniverse:repository:InternalRepository"
1907
+ },
1908
+ "CanonicalUniverse:reference:CanonicalUniverse:concept:ConceptWithDirectReference:main": {
1909
+ "id": "CanonicalUniverse:reference:CanonicalUniverse:concept:ConceptWithDirectReference:main",
1910
+ "name": "main",
1911
+ "urls": [
1912
+ "https://github.com/owner/repository/tree/main"
1913
+ ],
1914
+ "source": {
1915
+ "file": "/home/grumpy/git/sprig-universe/src/universe/universe.prose",
1916
+ "start": {
1917
+ "line": 127,
1918
+ "col": 5,
1919
+ "offset": 2645
1920
+ },
1921
+ "end": {
1922
+ "line": 129,
1923
+ "col": 6,
1924
+ "offset": 2724
1925
+ }
1926
+ },
1927
+ "scopeDeclId": "CanonicalUniverse:concept:ConceptWithDirectReference"
1928
+ },
1929
+ "CanonicalUniverse:reference:CanonicalUniverse:concept:ConceptWithReferenceInRepository:file1": {
1930
+ "id": "CanonicalUniverse:reference:CanonicalUniverse:concept:ConceptWithReferenceInRepository:file1",
1931
+ "name": "file1",
1932
+ "urls": [
1933
+ "https://github.com/owner/repository/tree/main/path/to/file1.md",
1934
+ "https://github.com/owner/repository/tree/main/path/to/file2.md"
1935
+ ],
1936
+ "source": {
1937
+ "file": "/home/grumpy/git/sprig-universe/src/universe/universe.prose",
1938
+ "start": {
1939
+ "line": 137,
1940
+ "col": 5,
1941
+ "offset": 2869
1942
+ },
1943
+ "end": {
1944
+ "line": 143,
1945
+ "col": 6,
1946
+ "offset": 3015
1947
+ }
1948
+ },
1949
+ "repositoryName": "ExternalRepository",
1950
+ "kind": "documentation",
1951
+ "paths": [
1952
+ "path/to/file1.md",
1953
+ "path/to/file2.md"
1954
+ ],
1955
+ "scopeDeclId": "CanonicalUniverse:concept:ConceptWithReferenceInRepository",
1956
+ "repositoryRef": "CanonicalUniverse:repository:ExternalRepository"
1957
+ },
1958
+ "CanonicalUniverse:reference:GlobalReference": {
1959
+ "id": "CanonicalUniverse:reference:GlobalReference",
1960
+ "name": "GlobalReference",
1961
+ "urls": [
1962
+ "https://github.com/owner/repository/tree/main"
1963
+ ],
1964
+ "source": {
1965
+ "file": "/home/grumpy/git/sprig-universe/src/universe/universe.prose",
1966
+ "start": {
1967
+ "line": 147,
1968
+ "col": 1,
1969
+ "offset": 3023
1970
+ },
1971
+ "end": {
1972
+ "line": 149,
1973
+ "col": 2,
1974
+ "offset": 3110
1975
+ }
1976
+ },
1977
+ "scopeDeclId": "CanonicalUniverse:universe:CanonicalUniverse"
1978
+ },
1979
+ "CanonicalUniverse:reference:CanonicalUniverse:series:ExternalSeries:external-series": {
1980
+ "id": "CanonicalUniverse:reference:CanonicalUniverse:series:ExternalSeries:external-series",
1981
+ "name": "external-series",
1982
+ "urls": [
1983
+ "http://localhost:6336/external-series.md"
1984
+ ],
1985
+ "source": {
1986
+ "file": "/home/grumpy/git/sprig-universe/src/universe/universe.prose",
1987
+ "start": {
1988
+ "line": 174,
1989
+ "col": 3,
1990
+ "offset": 3546
1991
+ },
1992
+ "end": {
1993
+ "line": 174,
1994
+ "col": 67,
1995
+ "offset": 3610
1996
+ }
1997
+ },
1998
+ "scopeDeclId": "CanonicalUniverse:series:ExternalSeries"
1999
+ },
2000
+ "CanonicalUniverse:reference:CanonicalUniverse:book:ExternalBook:external-book": {
2001
+ "id": "CanonicalUniverse:reference:CanonicalUniverse:book:ExternalBook:external-book",
2002
+ "name": "external-book",
2003
+ "urls": [
2004
+ "http://localhost:6336/external-book.md"
2005
+ ],
2006
+ "source": {
2007
+ "file": "/home/grumpy/git/sprig-universe/src/universe/universe.prose",
2008
+ "start": {
2009
+ "line": 180,
2010
+ "col": 3,
2011
+ "offset": 3688
2012
+ },
2013
+ "end": {
2014
+ "line": 180,
2015
+ "col": 65,
2016
+ "offset": 3750
2017
+ }
2018
+ },
2019
+ "scopeDeclId": "CanonicalUniverse:book:ExternalBook"
2020
+ },
2021
+ "CanonicalUniverse:reference:CanonicalUniverse:chapter:ExternalChapter:external-chapter": {
2022
+ "id": "CanonicalUniverse:reference:CanonicalUniverse:chapter:ExternalChapter:external-chapter",
2023
+ "name": "external-chapter",
2024
+ "urls": [
2025
+ "http://localhost:6336/external-chapter.md"
2026
+ ],
2027
+ "source": {
2028
+ "file": "/home/grumpy/git/sprig-universe/src/universe/universe.prose",
2029
+ "start": {
2030
+ "line": 186,
2031
+ "col": 3,
2032
+ "offset": 3832
2033
+ },
2034
+ "end": {
2035
+ "line": 186,
2036
+ "col": 68,
2037
+ "offset": 3897
2038
+ }
2039
+ },
2040
+ "scopeDeclId": "CanonicalUniverse:chapter:ExternalChapter"
2041
+ },
2042
+ "CanonicalUniverse:reference:CanonicalUniverse:concept:ExternalConcept:external-concept": {
2043
+ "id": "CanonicalUniverse:reference:CanonicalUniverse:concept:ExternalConcept:external-concept",
2044
+ "name": "external-concept",
2045
+ "urls": [
2046
+ "http://localhost:6336/external-concept.md"
2047
+ ],
2048
+ "source": {
2049
+ "file": "/home/grumpy/git/sprig-universe/src/universe/universe.prose",
2050
+ "start": {
2051
+ "line": 192,
2052
+ "col": 3,
2053
+ "offset": 3982
2054
+ },
2055
+ "end": {
2056
+ "line": 192,
2057
+ "col": 68,
2058
+ "offset": 4047
2059
+ }
2060
+ },
2061
+ "scopeDeclId": "CanonicalUniverse:concept:ExternalConcept"
1878
2062
  }
1879
2063
  },
1880
2064
  "documentsByName": {},
@@ -1937,5 +2121,5 @@
1937
2121
  }
1938
2122
  }
1939
2123
  },
1940
- "generatedAt": "2026-01-27T17:35:36.143Z"
2124
+ "generatedAt": "2026-01-28T00:15:11.192Z"
1941
2125
  }
@@ -920,6 +920,18 @@ function getRawText(contentSpan, sourceText) {
920
920
  return sourceText.slice(contentSpan.startOffset, contentSpan.endOffset);
921
921
  }
922
922
 
923
+ /**
924
+ * @param {string | undefined} raw
925
+ * @returns {string | undefined}
926
+ */
927
+ function normalizeTitleValue(raw) {
928
+ if (!raw) return undefined;
929
+ const trimmed = raw.trim();
930
+ if (!trimmed) return undefined;
931
+ const unquoted = trimmed.replace(/^['"]|['"]$/g, '');
932
+ return unquoted.trim() || undefined;
933
+ }
934
+
923
935
  /**
924
936
  * @param {string} base
925
937
  * @param {string} path
@@ -935,6 +947,63 @@ function joinRepositoryUrl(base, path) {
935
947
  return base + path;
936
948
  }
937
949
 
950
+ /**
951
+ * @param {{ title?: string, paths?: string[], urls?: string[] }} ref
952
+ * @returns {string}
953
+ */
954
+ function deriveReferenceName(ref) {
955
+ if (ref.title) {
956
+ return ref.title;
957
+ }
958
+ if (ref.paths && ref.paths.length > 0) {
959
+ const rawPath = ref.paths[0];
960
+ const trimmed = rawPath.replace(/\/+$/, '');
961
+ const parts = trimmed.split('/').filter(Boolean);
962
+ if (parts.length > 0) {
963
+ const lastPart = parts[parts.length - 1];
964
+ const withoutExt = lastPart.replace(/\.[^/.]+$/, '');
965
+ return withoutExt.replace(/\./g, '-');
966
+ }
967
+ }
968
+ if (ref.urls && ref.urls.length > 0) {
969
+ try {
970
+ const parsed = new URL(ref.urls[0]);
971
+ const segments = parsed.pathname.split('/').filter(Boolean);
972
+ if (segments.length > 0) {
973
+ const lastSegment = segments[segments.length - 1];
974
+ const withoutExt = lastSegment.replace(/\.[^/.]+$/, '');
975
+ return withoutExt.replace(/\./g, '-');
976
+ }
977
+ if (parsed.hostname) {
978
+ return parsed.hostname.replace(/\./g, '-');
979
+ }
980
+ } catch {
981
+ // Ignore malformed URLs here; validation handles required fields
982
+ }
983
+ }
984
+ return 'reference';
985
+ }
986
+
987
+ /**
988
+ * @param {Map<string, true>} nameMap
989
+ * @param {string} baseName
990
+ * @returns {string}
991
+ */
992
+ function pickUniqueInlineReferenceName(nameMap, baseName) {
993
+ if (!nameMap.has(baseName)) {
994
+ nameMap.set(baseName, true);
995
+ return baseName;
996
+ }
997
+ let suffix = 2;
998
+ let candidate = `${baseName}-${suffix}`;
999
+ while (nameMap.has(candidate)) {
1000
+ suffix += 1;
1001
+ candidate = `${baseName}-${suffix}`;
1002
+ }
1003
+ nameMap.set(candidate, true);
1004
+ return candidate;
1005
+ }
1006
+
938
1007
  /**
939
1008
  * Pass 4: Build nodes
940
1009
  * @param {BoundDecl[]} boundDecls
@@ -942,11 +1011,12 @@ function joinRepositoryUrl(base, path) {
942
1011
  * @param {Array<{ sourceText: string, filePath: string }>} fileData
943
1012
  * @returns {{ nodes: Map<DeclId, NodeModel>, repositories: Map<DeclId, RepositoryModel>, references: Map<DeclId, ReferenceModel>, diagnostics: Diagnostic[] }}
944
1013
  */
945
- function buildNodes(boundDecls, schemas, fileData) {
1014
+ function buildNodes(boundDecls, schemas, fileData, universeName) {
946
1015
  const nodes = new Map();
947
1016
  const repositories = new Map();
948
1017
  const references = new Map();
949
1018
  const diagnostics = [];
1019
+ const inlineReferenceNamesByScope = new Map();
950
1020
 
951
1021
  // Build file data map
952
1022
  const fileDataMap = new Map();
@@ -969,6 +1039,47 @@ function buildNodes(boundDecls, schemas, fileData) {
969
1039
  source: block.span,
970
1040
  };
971
1041
  }
1042
+
1043
+ /**
1044
+ * @param {any} block
1045
+ * @param {string} sourceText
1046
+ * @returns {ReferenceModel}
1047
+ */
1048
+ function buildReferenceFromBlock(block, sourceText) {
1049
+ const ref = {
1050
+ id: '',
1051
+ name: '',
1052
+ urls: [],
1053
+ source: block.source || block.span,
1054
+ };
1055
+
1056
+ if (block.repositoryName) {
1057
+ ref.repositoryName = block.repositoryName;
1058
+ }
1059
+
1060
+ const children = block.children || [];
1061
+ for (const child of children) {
1062
+ if (child.kind === 'url' && child.value) {
1063
+ ref.urls = [child.value];
1064
+ } else if (child.kind === 'paths' && child.paths) {
1065
+ ref.paths = child.paths;
1066
+ } else if (child.kind === 'kind' && child.value) {
1067
+ ref.kind = child.value;
1068
+ } else if (child.kind === 'title') {
1069
+ const raw = getRawText(child.contentSpan, sourceText);
1070
+ const titleValue = normalizeTitleValue(raw);
1071
+ if (titleValue) {
1072
+ ref.title = titleValue;
1073
+ }
1074
+ } else if (child.kind === 'describe') {
1075
+ ref.describe = buildTextBlock(child, sourceText);
1076
+ } else if (child.kind === 'note') {
1077
+ ref.note = buildTextBlock(child, sourceText);
1078
+ }
1079
+ }
1080
+
1081
+ return ref;
1082
+ }
972
1083
 
973
1084
  for (const bound of boundDecls) {
974
1085
  const { id, info, decl } = bound;
@@ -1116,6 +1227,27 @@ function buildNodes(boundDecls, schemas, fileData) {
1116
1227
  .filter(item => item.name);
1117
1228
  }
1118
1229
 
1230
+ // Extract inline reference blocks and attach directly
1231
+ const inlineReferenceBlocks = (decl.body || []).filter(b => b.kind === 'reference' && b.children);
1232
+ if (inlineReferenceBlocks.length > 0) {
1233
+ const nameMap = inlineReferenceNamesByScope.get(id) || new Map();
1234
+ inlineReferenceNamesByScope.set(id, nameMap);
1235
+ for (const block of inlineReferenceBlocks) {
1236
+ const refModel = buildReferenceFromBlock(block, sourceText);
1237
+ const baseName = deriveReferenceName(refModel);
1238
+ const uniqueName = pickUniqueInlineReferenceName(nameMap, baseName);
1239
+ const refId = makeDeclId(universeName, 'reference', `${id}:${uniqueName}`);
1240
+ refModel.id = refId;
1241
+ refModel.name = uniqueName;
1242
+ refModel.scopeDeclId = id;
1243
+ references.set(refId, refModel);
1244
+ if (!node.references) {
1245
+ node.references = [];
1246
+ }
1247
+ node.references.push(refId);
1248
+ }
1249
+ }
1250
+
1119
1251
  // Extract ordering block (applied after all nodes are built)
1120
1252
  const orderingBlocks = (decl.body || []).filter(b => b.kind === 'ordering');
1121
1253
  if (orderingBlocks.length > 0) {
@@ -1230,6 +1362,8 @@ function buildNodes(boundDecls, schemas, fileData) {
1230
1362
  if (noteBlock) {
1231
1363
  ref.note = buildTextBlock(noteBlock, sourceText);
1232
1364
  }
1365
+
1366
+ ref.scopeDeclId = info.parentDeclId || id;
1233
1367
 
1234
1368
  references.set(id, ref);
1235
1369
  }
@@ -1274,13 +1408,10 @@ function resolveReferenceModels(boundDecls, references, repositories, index, par
1274
1408
  const diagnostics = [];
1275
1409
  const unknownRepoCounts = new Map();
1276
1410
 
1277
- for (const bound of boundDecls) {
1278
- const { id, info } = bound;
1279
- if (info.kindResolved !== 'reference') continue;
1280
- const ref = references.get(id);
1281
- if (!ref) continue;
1282
-
1283
- const displayName = ref.name || info.name || 'unnamed reference';
1411
+ for (const ref of references.values()) {
1412
+ const scopeDeclId = ref.scopeDeclId || ref.id;
1413
+ const source = ref.source;
1414
+ const displayName = ref.name || 'unnamed reference';
1284
1415
  const hasUrl = Array.isArray(ref.urls) && ref.urls.length > 0;
1285
1416
  const repositoryName = ref.repositoryName;
1286
1417
  let urls = hasUrl ? [...ref.urls] : [];
@@ -1290,19 +1421,19 @@ function resolveReferenceModels(boundDecls, references, repositories, index, par
1290
1421
  diagnostics.push({
1291
1422
  severity: 'error',
1292
1423
  message: `Reference "${displayName}" cannot include both url { ... } and in <Repository>`,
1293
- source: info.span,
1424
+ source,
1294
1425
  });
1295
1426
  }
1296
1427
 
1297
1428
  if (!hasUrl && repositoryName) {
1298
1429
  const pathSegments = repositoryName.split('.');
1299
1430
  if (pathSegments.length === 1) {
1300
- const resolved = resolveUnqualified(pathSegments[0], id, index, parentMap, info.span);
1431
+ const resolved = resolveUnqualified(pathSegments[0], scopeDeclId, index, parentMap, source);
1301
1432
  if (resolved.ambiguous) {
1302
1433
  diagnostics.push({
1303
1434
  severity: 'error',
1304
1435
  message: `Ambiguous repository "${repositoryName}" - use qualified path`,
1305
- source: info.span,
1436
+ source,
1306
1437
  });
1307
1438
  } else if (resolved.declId) {
1308
1439
  const repo = repositories.get(resolved.declId);
@@ -1310,13 +1441,13 @@ function resolveReferenceModels(boundDecls, references, repositories, index, par
1310
1441
  diagnostics.push({
1311
1442
  severity: 'error',
1312
1443
  message: `Reference "${displayName}" uses "${repositoryName}" which is not a repository`,
1313
- source: info.span,
1444
+ source,
1314
1445
  });
1315
1446
  } else if (!repo.url) {
1316
1447
  diagnostics.push({
1317
1448
  severity: 'error',
1318
1449
  message: `Repository "${repo.name}" is missing url { '...' }`,
1319
- source: info.span,
1450
+ source,
1320
1451
  });
1321
1452
  } else {
1322
1453
  repositoryRef = repo.id;
@@ -1326,7 +1457,7 @@ function resolveReferenceModels(boundDecls, references, repositories, index, par
1326
1457
  diagnostics.push({
1327
1458
  severity: 'error',
1328
1459
  message: `Reference "${displayName}" has an empty paths block`,
1329
- source: info.span,
1460
+ source,
1330
1461
  });
1331
1462
  } else {
1332
1463
  urls = [repo.url];
@@ -1335,29 +1466,29 @@ function resolveReferenceModels(boundDecls, references, repositories, index, par
1335
1466
  } else {
1336
1467
  const entry = unknownRepoCounts.get(repositoryName) || {
1337
1468
  count: 0,
1338
- source: info.span,
1469
+ source,
1339
1470
  };
1340
1471
  entry.count += 1;
1341
- if (!entry.source && info.span) {
1342
- entry.source = info.span;
1472
+ if (!entry.source && source) {
1473
+ entry.source = source;
1343
1474
  }
1344
1475
  unknownRepoCounts.set(repositoryName, entry);
1345
1476
  }
1346
1477
  } else {
1347
- const resolved = resolveQualified(pathSegments, index, info.span);
1478
+ const resolved = resolveQualified(pathSegments, index, source);
1348
1479
  if (resolved.declId) {
1349
1480
  const repo = repositories.get(resolved.declId);
1350
1481
  if (!repo) {
1351
1482
  diagnostics.push({
1352
1483
  severity: 'error',
1353
1484
  message: `Reference "${displayName}" uses "${repositoryName}" which is not a repository`,
1354
- source: info.span,
1485
+ source,
1355
1486
  });
1356
1487
  } else if (!repo.url) {
1357
1488
  diagnostics.push({
1358
1489
  severity: 'error',
1359
1490
  message: `Repository "${repo.name}" is missing url { '...' }`,
1360
- source: info.span,
1491
+ source,
1361
1492
  });
1362
1493
  } else {
1363
1494
  repositoryRef = repo.id;
@@ -1367,7 +1498,7 @@ function resolveReferenceModels(boundDecls, references, repositories, index, par
1367
1498
  diagnostics.push({
1368
1499
  severity: 'error',
1369
1500
  message: `Reference "${displayName}" has an empty paths block`,
1370
- source: info.span,
1501
+ source,
1371
1502
  });
1372
1503
  } else {
1373
1504
  urls = [repo.url];
@@ -1376,11 +1507,11 @@ function resolveReferenceModels(boundDecls, references, repositories, index, par
1376
1507
  } else {
1377
1508
  const entry = unknownRepoCounts.get(repositoryName) || {
1378
1509
  count: 0,
1379
- source: info.span,
1510
+ source,
1380
1511
  };
1381
1512
  entry.count += 1;
1382
- if (!entry.source && info.span) {
1383
- entry.source = info.span;
1513
+ if (!entry.source && source) {
1514
+ entry.source = source;
1384
1515
  }
1385
1516
  unknownRepoCounts.set(repositoryName, entry);
1386
1517
  }
@@ -1391,7 +1522,7 @@ function resolveReferenceModels(boundDecls, references, repositories, index, par
1391
1522
  diagnostics.push({
1392
1523
  severity: 'error',
1393
1524
  message: `Reference "${displayName}" must have url { ... } or declared as reference ${displayName} in <Repository>`,
1394
- source: info.span,
1525
+ source,
1395
1526
  });
1396
1527
  }
1397
1528
 
@@ -1430,6 +1561,7 @@ function resolveReferenceAttachments(nodes, references, index, parentMap) {
1430
1561
  for (const node of nodes.values()) {
1431
1562
  if (!node._pendingReferences) continue;
1432
1563
  const resolvedReferences = [];
1564
+ const existingReferences = Array.isArray(node.references) ? node.references : [];
1433
1565
 
1434
1566
  for (const item of node._pendingReferences) {
1435
1567
  const name = item.name;
@@ -1465,7 +1597,8 @@ function resolveReferenceAttachments(nodes, references, index, parentMap) {
1465
1597
  }
1466
1598
  }
1467
1599
 
1468
- node.references = resolvedReferences;
1600
+ const combined = [...existingReferences, ...resolvedReferences];
1601
+ node.references = Array.from(new Set(combined));
1469
1602
  delete node._pendingReferences;
1470
1603
  }
1471
1604
 
@@ -1789,7 +1922,12 @@ export function buildGraph(fileAstOrFileAsts, options) {
1789
1922
 
1790
1923
  // Pass 4: Build nodes
1791
1924
  const fileData = normalized.map(n => ({ sourceText: n.sourceText, filePath: n.filePath }));
1792
- const { nodes, repositories, references, diagnostics: nodeDiags } = buildNodes(boundDecls, schemas, fileData);
1925
+ const { nodes, repositories, references, diagnostics: nodeDiags } = buildNodes(
1926
+ boundDecls,
1927
+ schemas,
1928
+ fileData,
1929
+ universeName,
1930
+ );
1793
1931
 
1794
1932
  // Resolve reference models and attachments
1795
1933
  addImplicitReferenceAttachments(boundDecls, nodes);
@@ -144,6 +144,10 @@ anthology AnthologyWithRelationships in CanonicalUniverse {
144
144
  }
145
145
  }
146
146
 
147
+ reference GlobalReference {
148
+ url { 'https://github.com/owner/repository/tree/main' }
149
+ }
150
+
147
151
  anthology ExternalAnthology in CanonicalUniverse {
148
152
  aliases {
149
153
  bookAlias { book }
@@ -160,15 +164,33 @@ anthology ExternalAnthology in CanonicalUniverse {
160
164
  }
161
165
 
162
166
  chapterAlias ExternalChapterAlias in BookAlias { }
167
+
168
+ references { GlobalReference }
169
+ }
170
+
171
+ series ExternalSeries in ExternalAnthology {
172
+ references { GlobalReference }
173
+
174
+ reference { url { 'http://localhost:6336/external-series.md' } }
175
+ }
176
+
177
+ book ExternalBook in ExternalSeries {
178
+ references { GlobalReference }
179
+
180
+ reference { url { 'http://localhost:6336/external-book.md' } }
163
181
  }
164
182
 
165
- series ExternalSeries in ExternalAnthology { }
183
+ chapter ExternalChapter in ExternalBook {
184
+ references { GlobalReference }
166
185
 
167
- book ExternalBook in ExternalSeries { }
186
+ reference { url { 'http://localhost:6336/external-chapter.md' } }
187
+ }
168
188
 
169
- chapter ExternalChapter in ExternalBook { }
189
+ concept ExternalConcept in ExternalChapter {
190
+ references { GlobalReference }
170
191
 
171
- concept ExternalConcept in ExternalChapter { }
192
+ reference { url { 'http://localhost:6336/external-concept.md' } }
193
+ }
172
194
 
173
195
  repository ExternalRepository in CanonicalUniverse {
174
196
  title { GitHub }