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

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.
@@ -670,31 +670,21 @@ function bindParentsAndKinds(index, normalizedDecls) {
670
670
  * @param {ScopeKey} scopeKey
671
671
  * @returns {string}
672
672
  */
673
- function resolveKind(spelledKind, scopeKey) {
673
+ function resolveKind(spelledKind, declId) {
674
674
  const baseKinds = ['universe', 'anthology', 'series', 'book', 'chapter', 'concept', 'relates', 'relationship', 'repository', 'reference'];
675
675
  if (baseKinds.includes(spelledKind)) {
676
676
  return spelledKind;
677
677
  }
678
678
 
679
- // Walk up scope chain to find alias
680
- let currentScope = scopeKey;
681
- while (currentScope) {
682
- const aliasTable = aliasTablesByScope.get(currentScope);
679
+ // Walk up syntactic parent chain to find alias
680
+ let currentDeclId = declId;
681
+ while (currentDeclId) {
682
+ const decl = declMap.get(currentDeclId);
683
+ const aliasTable = decl?.aliases;
683
684
  if (aliasTable && aliasTable.has(spelledKind)) {
684
685
  return aliasTable.get(spelledKind);
685
686
  }
686
-
687
- // Move to parent scope (extract parent from scope key)
688
- if (currentScope.startsWith('series:')) {
689
- // Parent would be anthology or universe - need to find it
690
- break; // For now, just check current scope
691
- } else if (currentScope.startsWith('anthology:')) {
692
- // Parent is universe
693
- const universeName = currentScope.split(':')[1];
694
- currentScope = `universe:${universeName}`;
695
- } else {
696
- break;
697
- }
687
+ currentDeclId = decl?.syntacticParentId;
698
688
  }
699
689
 
700
690
  return 'unknown';
@@ -702,7 +692,7 @@ function bindParentsAndKinds(index, normalizedDecls) {
702
692
 
703
693
  // First pass: resolve kinds
704
694
  for (const [declId, info] of declIndex.entries()) {
705
- const resolvedKind = resolveKind(info.kindSpelled, info.scopeKey);
695
+ const resolvedKind = resolveKind(info.kindSpelled, declId);
706
696
  info.kindResolved = resolvedKind;
707
697
 
708
698
  if (resolvedKind === 'unknown' && !['relates', 'relationshipDecl', 'repository', 'referenceDecl'].includes(info.kindSpelled)) {
@@ -814,13 +804,19 @@ function bindParentsAndKinds(index, normalizedDecls) {
814
804
 
815
805
  // Build children lists from resolved parent map, preserving source order
816
806
  const childrenByParent = new Map();
807
+ const childrenSeenByParent = new Map();
817
808
  for (const decl of normalizedDecls) {
818
809
  const info = declIndex.get(decl.id);
819
810
  if (!info || !info.parentDeclId) continue;
820
811
  if (!childrenByParent.has(info.parentDeclId)) {
821
812
  childrenByParent.set(info.parentDeclId, []);
813
+ childrenSeenByParent.set(info.parentDeclId, new Set());
814
+ }
815
+ const seen = childrenSeenByParent.get(info.parentDeclId);
816
+ if (!seen.has(decl.id)) {
817
+ childrenByParent.get(info.parentDeclId).push(decl.id);
818
+ seen.add(decl.id);
822
819
  }
823
- childrenByParent.get(info.parentDeclId).push(decl.id);
824
820
  }
825
821
 
826
822
  for (const bound of boundDecls) {
@@ -924,6 +920,21 @@ function getRawText(contentSpan, sourceText) {
924
920
  return sourceText.slice(contentSpan.startOffset, contentSpan.endOffset);
925
921
  }
926
922
 
923
+ /**
924
+ * @param {string} base
925
+ * @param {string} path
926
+ * @returns {string}
927
+ */
928
+ function joinRepositoryUrl(base, path) {
929
+ if (base.endsWith('/') && path.startsWith('/')) {
930
+ return base + path.slice(1);
931
+ }
932
+ if (!base.endsWith('/') && !path.startsWith('/')) {
933
+ return `${base}/${path}`;
934
+ }
935
+ return base + path;
936
+ }
937
+
927
938
  /**
928
939
  * Pass 4: Build nodes
929
940
  * @param {BoundDecl[]} boundDecls
@@ -1082,7 +1093,7 @@ function buildNodes(boundDecls, schemas, fileData) {
1082
1093
  // Extract unknown blocks (non-container, non-standard blocks)
1083
1094
  const unknownBlocks = (decl.body || []).filter(b => {
1084
1095
  if (!b.kind) return false;
1085
- const knownKinds = ['describe', 'title', 'note', 'relationships', 'from', 'references', 'reference'];
1096
+ const knownKinds = ['describe', 'title', 'note', 'relationships', 'from', 'references', 'reference', 'ordering'];
1086
1097
  return !knownKinds.includes(b.kind);
1087
1098
  });
1088
1099
  if (unknownBlocks.length > 0) {
@@ -1094,13 +1105,28 @@ function buildNodes(boundDecls, schemas, fileData) {
1094
1105
  }));
1095
1106
  }
1096
1107
 
1097
- // Extract references block
1108
+ // Extract references block (resolved after all nodes are built)
1098
1109
  const referencesBlock = (decl.body || []).find(b => b.kind === 'references');
1099
1110
  if (referencesBlock && referencesBlock.items) {
1100
- node.references = referencesBlock.items.map(item => {
1101
- // Will be resolved in Pass 5
1102
- return item.ref || item.name || '';
1103
- });
1111
+ node._pendingReferences = referencesBlock.items
1112
+ .map(item => ({
1113
+ name: item.ref || item.name || '',
1114
+ source: item.span || referencesBlock.source,
1115
+ }))
1116
+ .filter(item => item.name);
1117
+ }
1118
+
1119
+ // Extract ordering block (applied after all nodes are built)
1120
+ const orderingBlocks = (decl.body || []).filter(b => b.kind === 'ordering');
1121
+ if (orderingBlocks.length > 0) {
1122
+ if (orderingBlocks.length > 1) {
1123
+ diagnostics.push({
1124
+ severity: 'warning',
1125
+ message: `Multiple ordering blocks in ${node.kind} "${node.name}". Using the first one.`,
1126
+ source: orderingBlocks[1].source || orderingBlocks[0].source,
1127
+ });
1128
+ }
1129
+ node._pendingOrdering = orderingBlocks[0];
1104
1130
  }
1105
1131
 
1106
1132
  nodes.set(id, node);
@@ -1176,6 +1202,12 @@ function buildNodes(boundDecls, schemas, fileData) {
1176
1202
  if (pathsBlock && pathsBlock.paths) {
1177
1203
  ref.paths = pathsBlock.paths;
1178
1204
  }
1205
+
1206
+ // Extract kind block
1207
+ const kindBlock = (decl.body || []).find(b => b.kind === 'kind');
1208
+ if (kindBlock && kindBlock.value) {
1209
+ ref.kind = kindBlock.value;
1210
+ }
1179
1211
 
1180
1212
  // Extract title block
1181
1213
  const titleBlock = (decl.body || []).find(b => b.kind === 'title');
@@ -1206,6 +1238,293 @@ function buildNodes(boundDecls, schemas, fileData) {
1206
1238
  return { nodes, repositories, references, diagnostics };
1207
1239
  }
1208
1240
 
1241
+ /**
1242
+ * Attach named references declared inside containers to their parent nodes.
1243
+ * @param {BoundDecl[]} boundDecls
1244
+ * @param {Map<DeclId, NodeModel>} nodes
1245
+ */
1246
+ function addImplicitReferenceAttachments(boundDecls, nodes) {
1247
+ for (const bound of boundDecls) {
1248
+ const { info } = bound;
1249
+ if (info.kindResolved !== 'reference') continue;
1250
+ const parentId = info.parentDeclId;
1251
+ if (!parentId) continue;
1252
+ const parentNode = nodes.get(parentId);
1253
+ if (!parentNode || parentNode.kind === 'universe') continue;
1254
+ if (!parentNode._pendingReferences) {
1255
+ parentNode._pendingReferences = [];
1256
+ }
1257
+ parentNode._pendingReferences.push({
1258
+ name: info.name,
1259
+ source: info.span,
1260
+ });
1261
+ }
1262
+ }
1263
+
1264
+ /**
1265
+ * Resolve reference declarations to repository URLs and repository references.
1266
+ * @param {BoundDecl[]} boundDecls
1267
+ * @param {Map<DeclId, ReferenceModel>} references
1268
+ * @param {Map<DeclId, RepositoryModel>} repositories
1269
+ * @param {DeclIndex} index
1270
+ * @param {Map<DeclId, DeclId>} parentMap
1271
+ * @returns {Diagnostic[]}
1272
+ */
1273
+ function resolveReferenceModels(boundDecls, references, repositories, index, parentMap) {
1274
+ const diagnostics = [];
1275
+ const unknownRepoCounts = new Map();
1276
+
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';
1284
+ const hasUrl = Array.isArray(ref.urls) && ref.urls.length > 0;
1285
+ const repositoryName = ref.repositoryName;
1286
+ let urls = hasUrl ? [...ref.urls] : [];
1287
+ let repositoryRef = undefined;
1288
+
1289
+ if (hasUrl && repositoryName) {
1290
+ diagnostics.push({
1291
+ severity: 'error',
1292
+ message: `Reference "${displayName}" cannot include both url { ... } and in <Repository>`,
1293
+ source: info.span,
1294
+ });
1295
+ }
1296
+
1297
+ if (!hasUrl && repositoryName) {
1298
+ const pathSegments = repositoryName.split('.');
1299
+ if (pathSegments.length === 1) {
1300
+ const resolved = resolveUnqualified(pathSegments[0], id, index, parentMap, info.span);
1301
+ if (resolved.ambiguous) {
1302
+ diagnostics.push({
1303
+ severity: 'error',
1304
+ message: `Ambiguous repository "${repositoryName}" - use qualified path`,
1305
+ source: info.span,
1306
+ });
1307
+ } else if (resolved.declId) {
1308
+ const repo = repositories.get(resolved.declId);
1309
+ if (!repo) {
1310
+ diagnostics.push({
1311
+ severity: 'error',
1312
+ message: `Reference "${displayName}" uses "${repositoryName}" which is not a repository`,
1313
+ source: info.span,
1314
+ });
1315
+ } else if (!repo.url) {
1316
+ diagnostics.push({
1317
+ severity: 'error',
1318
+ message: `Repository "${repo.name}" is missing url { '...' }`,
1319
+ source: info.span,
1320
+ });
1321
+ } else {
1322
+ repositoryRef = repo.id;
1323
+ if (ref.paths && ref.paths.length > 0) {
1324
+ urls = ref.paths.map((path) => joinRepositoryUrl(repo.url, path));
1325
+ } else if (ref.paths && ref.paths.length === 0) {
1326
+ diagnostics.push({
1327
+ severity: 'error',
1328
+ message: `Reference "${displayName}" has an empty paths block`,
1329
+ source: info.span,
1330
+ });
1331
+ } else {
1332
+ urls = [repo.url];
1333
+ }
1334
+ }
1335
+ } else {
1336
+ const entry = unknownRepoCounts.get(repositoryName) || {
1337
+ count: 0,
1338
+ source: info.span,
1339
+ };
1340
+ entry.count += 1;
1341
+ if (!entry.source && info.span) {
1342
+ entry.source = info.span;
1343
+ }
1344
+ unknownRepoCounts.set(repositoryName, entry);
1345
+ }
1346
+ } else {
1347
+ const resolved = resolveQualified(pathSegments, index, info.span);
1348
+ if (resolved.declId) {
1349
+ const repo = repositories.get(resolved.declId);
1350
+ if (!repo) {
1351
+ diagnostics.push({
1352
+ severity: 'error',
1353
+ message: `Reference "${displayName}" uses "${repositoryName}" which is not a repository`,
1354
+ source: info.span,
1355
+ });
1356
+ } else if (!repo.url) {
1357
+ diagnostics.push({
1358
+ severity: 'error',
1359
+ message: `Repository "${repo.name}" is missing url { '...' }`,
1360
+ source: info.span,
1361
+ });
1362
+ } else {
1363
+ repositoryRef = repo.id;
1364
+ if (ref.paths && ref.paths.length > 0) {
1365
+ urls = ref.paths.map((path) => joinRepositoryUrl(repo.url, path));
1366
+ } else if (ref.paths && ref.paths.length === 0) {
1367
+ diagnostics.push({
1368
+ severity: 'error',
1369
+ message: `Reference "${displayName}" has an empty paths block`,
1370
+ source: info.span,
1371
+ });
1372
+ } else {
1373
+ urls = [repo.url];
1374
+ }
1375
+ }
1376
+ } else {
1377
+ const entry = unknownRepoCounts.get(repositoryName) || {
1378
+ count: 0,
1379
+ source: info.span,
1380
+ };
1381
+ entry.count += 1;
1382
+ if (!entry.source && info.span) {
1383
+ entry.source = info.span;
1384
+ }
1385
+ unknownRepoCounts.set(repositoryName, entry);
1386
+ }
1387
+ }
1388
+ }
1389
+
1390
+ if (!hasUrl && !repositoryName) {
1391
+ diagnostics.push({
1392
+ severity: 'error',
1393
+ message: `Reference "${displayName}" must have url { ... } or declared as reference ${displayName} in <Repository>`,
1394
+ source: info.span,
1395
+ });
1396
+ }
1397
+
1398
+ if (urls.length > 0) {
1399
+ ref.urls = urls;
1400
+ }
1401
+ if (repositoryRef) {
1402
+ ref.repositoryRef = repositoryRef;
1403
+ }
1404
+ }
1405
+
1406
+ for (const [repoName, info] of unknownRepoCounts.entries()) {
1407
+ const countText = info.count > 1 ? ` (${info.count} occurrences)` : '';
1408
+ diagnostics.push({
1409
+ severity: 'error',
1410
+ message: `Unknown repository "${repoName}" used by references${countText}.`,
1411
+ source: info.source,
1412
+ });
1413
+ }
1414
+
1415
+ return diagnostics;
1416
+ }
1417
+
1418
+ /**
1419
+ * Resolve references blocks to reference IDs.
1420
+ * @param {Map<DeclId, NodeModel>} nodes
1421
+ * @param {Map<DeclId, ReferenceModel>} references
1422
+ * @param {DeclIndex} index
1423
+ * @param {Map<DeclId, DeclId>} parentMap
1424
+ * @returns {Diagnostic[]}
1425
+ */
1426
+ function resolveReferenceAttachments(nodes, references, index, parentMap) {
1427
+ const diagnostics = [];
1428
+ const unknownReferenceCounts = new Map();
1429
+
1430
+ for (const node of nodes.values()) {
1431
+ if (!node._pendingReferences) continue;
1432
+ const resolvedReferences = [];
1433
+
1434
+ for (const item of node._pendingReferences) {
1435
+ const name = item.name;
1436
+ if (!name) continue;
1437
+
1438
+ let resolvedDeclId = null;
1439
+ const pathSegments = name.split('.');
1440
+ if (pathSegments.length === 1) {
1441
+ const resolved = resolveUnqualified(pathSegments[0], node.id, index, parentMap, item.source);
1442
+ if (resolved.ambiguous) {
1443
+ diagnostics.push({
1444
+ severity: 'error',
1445
+ message: `Ambiguous reference "${name}" - use qualified path`,
1446
+ source: item.source,
1447
+ });
1448
+ continue;
1449
+ }
1450
+ resolvedDeclId = resolved.declId;
1451
+ } else {
1452
+ const resolved = resolveQualified(pathSegments, index, item.source);
1453
+ resolvedDeclId = resolved.declId;
1454
+ }
1455
+
1456
+ if (resolvedDeclId && references.has(resolvedDeclId)) {
1457
+ resolvedReferences.push(resolvedDeclId);
1458
+ } else {
1459
+ const entry = unknownReferenceCounts.get(name) || { count: 0, source: item.source };
1460
+ entry.count += 1;
1461
+ if (!entry.source && item.source) {
1462
+ entry.source = item.source;
1463
+ }
1464
+ unknownReferenceCounts.set(name, entry);
1465
+ }
1466
+ }
1467
+
1468
+ node.references = resolvedReferences;
1469
+ delete node._pendingReferences;
1470
+ }
1471
+
1472
+ for (const [name, info] of unknownReferenceCounts.entries()) {
1473
+ const countText = info.count > 1 ? ` (${info.count} occurrences)` : '';
1474
+ diagnostics.push({
1475
+ severity: 'error',
1476
+ message: `Unknown reference "${name}" in references list${countText}. References must use reference names, not paths.`,
1477
+ source: info.source,
1478
+ });
1479
+ }
1480
+
1481
+ return diagnostics;
1482
+ }
1483
+
1484
+ /**
1485
+ * Validate and apply ordering blocks after all nodes are created.
1486
+ * @param {Map<DeclId, NodeModel>} nodes
1487
+ * @returns {Diagnostic[]}
1488
+ */
1489
+ function applyOrderingBlocks(nodes) {
1490
+ const diagnostics = [];
1491
+
1492
+ for (const node of nodes.values()) {
1493
+ if (!node._pendingOrdering) continue;
1494
+ const orderingBlock = node._pendingOrdering;
1495
+ const orderingIdentifiers = orderingBlock.identifiers || [];
1496
+
1497
+ const childNameToNodeId = new Map();
1498
+ for (const childId of node.children || []) {
1499
+ const childNode = nodes.get(childId);
1500
+ if (childNode) {
1501
+ childNameToNodeId.set(childNode.name, childId);
1502
+ }
1503
+ }
1504
+
1505
+ const validOrdering = [];
1506
+ for (const identifier of orderingIdentifiers) {
1507
+ if (childNameToNodeId.has(identifier)) {
1508
+ validOrdering.push(identifier);
1509
+ } else {
1510
+ diagnostics.push({
1511
+ severity: 'warning',
1512
+ message: `Ordering block in ${node.kind} "${node.name}" references unknown child "${identifier}". This identifier will be ignored.`,
1513
+ source: orderingBlock.source,
1514
+ });
1515
+ }
1516
+ }
1517
+
1518
+ if (validOrdering.length > 0) {
1519
+ node.ordering = validOrdering;
1520
+ }
1521
+
1522
+ delete node._pendingOrdering;
1523
+ }
1524
+
1525
+ return diagnostics;
1526
+ }
1527
+
1209
1528
  /**
1210
1529
  * Pass 5: Build edges from relationship usage
1211
1530
  * @param {BoundDecl[]} boundDecls
@@ -1471,6 +1790,14 @@ export function buildGraph(fileAstOrFileAsts, options) {
1471
1790
  // Pass 4: Build nodes
1472
1791
  const fileData = normalized.map(n => ({ sourceText: n.sourceText, filePath: n.filePath }));
1473
1792
  const { nodes, repositories, references, diagnostics: nodeDiags } = buildNodes(boundDecls, schemas, fileData);
1793
+
1794
+ // Resolve reference models and attachments
1795
+ addImplicitReferenceAttachments(boundDecls, nodes);
1796
+ const referenceModelDiags = resolveReferenceModels(boundDecls, references, repositories, index, parentMap);
1797
+ const referenceAttachmentDiags = resolveReferenceAttachments(nodes, references, index, parentMap);
1798
+
1799
+ // Apply ordering blocks after all nodes are available
1800
+ const orderingDiags = applyOrderingBlocks(nodes);
1474
1801
 
1475
1802
  // Resolve relates endpoints
1476
1803
  const relatesDiags = resolveRelatesEndpoints(nodes, index, parentMap, boundDecls);
@@ -1487,6 +1814,9 @@ export function buildGraph(fileAstOrFileAsts, options) {
1487
1814
  ...bindDiags,
1488
1815
  ...schemaDiags,
1489
1816
  ...nodeDiags,
1817
+ ...referenceModelDiags,
1818
+ ...referenceAttachmentDiags,
1819
+ ...orderingDiags,
1490
1820
  ...relatesDiags,
1491
1821
  ...edgeDiags,
1492
1822
  ];
@@ -342,6 +342,7 @@ const blockParselets = new Map([
342
342
  ['label', (p) => p.parseLabelBlock()],
343
343
  ['reference', (p) => p.parseReferenceBlock()],
344
344
  ['references', (p) => p.parseReferencesBlock()],
345
+ ['ordering', (p) => p.parseOrderingBlock()],
345
346
  ['relationships', (p) => p.parseRelationshipsBlock()],
346
347
  ]);
347
348
 
@@ -1074,6 +1075,54 @@ ParserCore.prototype.parseReferenceBlock = function () {
1074
1075
  };
1075
1076
  };
1076
1077
 
1078
+ /**
1079
+ * Parses an ordering block: ordering { <identifiers> }
1080
+ * Items are identifiers (not paths).
1081
+ * @returns {any | null}
1082
+ */
1083
+ ParserCore.prototype.parseOrderingBlock = function () {
1084
+ const startToken = this.peek();
1085
+ if (!startToken) return null;
1086
+
1087
+ const { token: orderingToken } = this.expectIdentifierOrKeyword('ordering');
1088
+ if (!orderingToken) return null;
1089
+
1090
+ const { token: lbrace } = this.expect('LBRACE');
1091
+ if (!lbrace) return null;
1092
+
1093
+ const identifiers = [];
1094
+
1095
+ // Parse items until closing brace
1096
+ while (!this.isAtEnd() && !this.match('RBRACE')) {
1097
+ // Skip optional commas
1098
+ if (this.match('COMMA')) {
1099
+ this.advance();
1100
+ continue;
1101
+ }
1102
+
1103
+ const itemToken = this.peek();
1104
+ if (!itemToken) break;
1105
+
1106
+ const identifier = this.readIdent();
1107
+ if (!identifier) {
1108
+ this.reportDiagnostic('error', 'Expected identifier in ordering block', itemToken.span);
1109
+ this.advance();
1110
+ continue;
1111
+ }
1112
+
1113
+ identifiers.push(identifier);
1114
+ }
1115
+
1116
+ const { token: rbrace } = this.expect('RBRACE');
1117
+ if (!rbrace) return null;
1118
+
1119
+ return {
1120
+ kind: 'ordering',
1121
+ identifiers,
1122
+ source: this.createSpan(orderingToken, rbrace),
1123
+ };
1124
+ };
1125
+
1077
1126
  /**
1078
1127
  * Parses a references block: references { <items> }
1079
1128
  * Items are identifier paths (reference names)
@@ -37,6 +37,7 @@ const KEYWORDS = new Set([
37
37
  'title',
38
38
  'reference',
39
39
  'references',
40
+ 'ordering',
40
41
  'paths',
41
42
  'url',
42
43
  ]);
@@ -67,14 +67,16 @@ anthology AnthologyWithRelationships in CanonicalUniverse {
67
67
  A chapter can still be nested under an aliased book.
68
68
  }
69
69
 
70
- owns {
71
- ExternalOtherChapter,
72
- ExternalBook,
73
- }
70
+ relationships {
71
+ owns {
72
+ ExternalOtherChapter,
73
+ ExternalBook,
74
+ }
74
75
 
75
- singleton {
76
- ExternalAnthology
77
- ExternalSeries
76
+ singleton {
77
+ ExternalAnthology
78
+ ExternalSeries
79
+ }
78
80
  }
79
81
  }
80
82
 
@@ -104,21 +106,21 @@ anthology AnthologyWithRelationships in CanonicalUniverse {
104
106
  }
105
107
 
106
108
  reference NamedReference1 {
107
- url { 'https://github.com/owner/repository/tree/main' }
109
+ url { 'https://github.com/owner/repository/tree/main/references/reference1.md' }
108
110
  describe {
109
111
  References can have describe blocks.
110
112
  }
111
113
  }
112
114
 
113
115
  reference NamedReference2 {
114
- url { 'https://github.com/owner/repository/tree/main' }
116
+ url { 'https://github.com/owner/repository/tree/main/references/reference2.md' }
115
117
  describe {
116
118
  References can have describe blocks.
117
119
  }
118
120
  }
119
121
 
120
122
  reference NamedReferenceInRepository in InternalRepository {
121
- url { 'https://github.com/owner/repository/tree/main' }
123
+ paths { 'path/to/file1.md' 'path/to/file2.md' }
122
124
  }
123
125
 
124
126
  concept ConceptWithDirectReference {
@@ -150,6 +152,11 @@ anthology ExternalAnthology in CanonicalUniverse {
150
152
 
151
153
  bookAlias BookAlias {
152
154
  chapterAlias NestedChapterAlias { }
155
+
156
+ ordering {
157
+ NestedChapterAlias
158
+ ExternalChapterAlias
159
+ }
153
160
  }
154
161
 
155
162
  chapterAlias ExternalChapterAlias in BookAlias { }