@sprig-and-prose/sprig-universe 0.3.2 → 0.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprig-and-prose/sprig-universe",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "type": "module",
5
5
  "description": "Minimal universe parser for sprig",
6
6
  "main": "src/index.js",
@@ -27,4 +27,3 @@
27
27
  "typescript": "^5.7.2"
28
28
  }
29
29
  }
30
-
package/src/ast.js CHANGED
@@ -43,6 +43,13 @@
43
43
  * @property {SourceSpan} source - Source span
44
44
  */
45
45
 
46
+ /**
47
+ * @typedef {Object} OrderingBlock
48
+ * @property {string} kind - Always 'ordering'
49
+ * @property {string[]} identifiers - Array of identifier names
50
+ * @property {SourceSpan} source - Source span
51
+ */
52
+
46
53
  /**
47
54
  * @typedef {Object} DocumentBlock
48
55
  * @property {string} kind - Always 'document'
@@ -107,7 +114,7 @@
107
114
  * @typedef {Object} UniverseDecl
108
115
  * @property {string} kind - Always 'universe'
109
116
  * @property {string} name - Universe name
110
- * @property {Array<AnthologyDecl | SeriesDecl | BookDecl | ChapterDecl | ConceptDecl | RelatesDecl | DescribeBlock | TitleBlock | RepositoryDecl | ReferenceDecl | NamedDocumentBlock | UnknownBlock>} body - Body declarations
117
+ * @property {Array<AnthologyDecl | SeriesDecl | BookDecl | ChapterDecl | ConceptDecl | RelatesDecl | DescribeBlock | TitleBlock | ReferencesBlock | OrderingBlock | RepositoryDecl | ReferenceDecl | NamedDocumentBlock | UnknownBlock>} body - Body declarations
111
118
  * @property {SourceSpan} source - Source span
112
119
  */
113
120
 
@@ -116,7 +123,7 @@
116
123
  * @property {string} kind - Always 'anthology'
117
124
  * @property {string} name - Anthology name
118
125
  * @property {string} [parentName] - Optional parent universe name (from "in UniverseName")
119
- * @property {Array<SeriesDecl | BookDecl | ChapterDecl | ConceptDecl | RelatesDecl | DescribeBlock | TitleBlock | ReferencesBlock | DocumentationBlock | RepositoryDecl | ReferenceDecl | UnknownBlock>} body - Body declarations
126
+ * @property {Array<SeriesDecl | BookDecl | ChapterDecl | ConceptDecl | RelatesDecl | DescribeBlock | TitleBlock | ReferencesBlock | OrderingBlock | DocumentationBlock | RepositoryDecl | ReferenceDecl | UnknownBlock>} body - Body declarations
120
127
  * @property {SourceSpan} source - Source span
121
128
  */
122
129
 
@@ -125,7 +132,7 @@
125
132
  * @property {string} kind - Always 'series'
126
133
  * @property {string} name - Series name
127
134
  * @property {string} [parentName] - Optional parent anthology name (from "in AnthologyName")
128
- * @property {Array<BookDecl | ChapterDecl | DescribeBlock | TitleBlock | ReferencesBlock | DocumentationBlock | RepositoryDecl | ReferenceDecl | UnknownBlock>} body - Body declarations
135
+ * @property {Array<BookDecl | ChapterDecl | DescribeBlock | TitleBlock | ReferencesBlock | OrderingBlock | DocumentationBlock | RepositoryDecl | ReferenceDecl | UnknownBlock>} body - Body declarations
129
136
  * @property {SourceSpan} source - Source span
130
137
  */
131
138
 
@@ -134,7 +141,7 @@
134
141
  * @property {string} kind - Always 'book'
135
142
  * @property {string} name - Book name
136
143
  * @property {string} [parentName] - Optional parent name (from "in ParentName")
137
- * @property {Array<ChapterDecl | DescribeBlock | TitleBlock | ReferencesBlock | DocumentationBlock | RepositoryDecl | ReferenceDecl | UnknownBlock>} body - Body declarations
144
+ * @property {Array<ChapterDecl | DescribeBlock | TitleBlock | ReferencesBlock | OrderingBlock | DocumentationBlock | RepositoryDecl | ReferenceDecl | UnknownBlock>} body - Body declarations
138
145
  * @property {SourceSpan} source - Source span
139
146
  */
140
147
 
@@ -143,7 +150,7 @@
143
150
  * @property {string} kind - Always 'chapter'
144
151
  * @property {string} name - Chapter name
145
152
  * @property {string} parentName - Parent name (from "in ParentName")
146
- * @property {Array<DescribeBlock | ReferencesBlock | DocumentationBlock | RepositoryDecl | ReferenceDecl | UnknownBlock>} body - Body declarations
153
+ * @property {Array<DescribeBlock | ReferencesBlock | OrderingBlock | DocumentationBlock | RepositoryDecl | ReferenceDecl | UnknownBlock>} body - Body declarations
147
154
  * @property {SourceSpan} source - Source span
148
155
  */
149
156
 
@@ -152,7 +159,7 @@
152
159
  * @property {string} kind - Always 'concept'
153
160
  * @property {string} name - Concept name
154
161
  * @property {string} [parentName] - Optional parent name (from "in ParentName")
155
- * @property {Array<DescribeBlock | ReferencesBlock | DocumentationBlock | RepositoryDecl | ReferenceDecl | UnknownBlock>} body - Body declarations
162
+ * @property {Array<DescribeBlock | ReferencesBlock | OrderingBlock | DocumentationBlock | RepositoryDecl | ReferenceDecl | UnknownBlock>} body - Body declarations
156
163
  * @property {SourceSpan} source - Source span
157
164
  */
158
165
 
@@ -176,7 +183,7 @@
176
183
  * @property {string} kind - Always 'relates'
177
184
  * @property {string} a - First endpoint text
178
185
  * @property {string} b - Second endpoint text
179
- * @property {Array<DescribeBlock | TitleBlock | FromBlock | UnknownBlock>} body - Body declarations
186
+ * @property {Array<DescribeBlock | TitleBlock | FromBlock | RelationshipsBlock | UnknownBlock>} body - Body declarations
180
187
  * @property {SourceSpan} source - Source span
181
188
  */
182
189
 
package/src/graph.js CHANGED
@@ -27,6 +27,7 @@ import { normalizeProseBlock } from './util/text.js';
27
27
  * @typedef {import('./ast.js').FromBlock} FromBlock
28
28
  * @typedef {import('./ast.js').RelationshipsBlock} RelationshipsBlock
29
29
  * @typedef {import('./ast.js').ReferencesBlock} ReferencesBlock
30
+ * @typedef {import('./ast.js').OrderingBlock} OrderingBlock
30
31
  * @typedef {import('./ast.js').ReferenceDecl} ReferenceDecl
31
32
  * @typedef {import('./ast.js').DocumentationBlock} DocumentationBlock
32
33
  * @typedef {import('./ast.js').DocumentBlock} DocumentBlock
@@ -74,6 +75,8 @@ export function buildGraph(fileASTs) {
74
75
  const pendingReferenceDecls = [];
75
76
  /** @type {Array<{ nodeId: string, items: Array<{ name: string, source: SourceSpan }>, universeName: string }>} */
76
77
  const pendingReferenceAttachments = [];
78
+ /** @type {Array<{ nodeId: string, parentName: string, scopeNodeId: string, source: SourceSpan, nodeKind: 'chapter' | 'book' }>} */
79
+ const pendingContainerResolutions = [];
77
80
 
78
81
  // First pass: collect all universe names with their file locations for validation
79
82
  const universeNameToFiles = new Map(); // universeName -> Set<file>
@@ -189,6 +192,7 @@ export function buildGraph(fileASTs) {
189
192
  entityKinds,
190
193
  pendingReferenceDecls,
191
194
  pendingReferenceAttachments,
195
+ pendingContainerResolutions,
192
196
  );
193
197
  } else {
194
198
  // First time seeing this universe - create node and process body
@@ -218,6 +222,7 @@ export function buildGraph(fileASTs) {
218
222
  entityKinds,
219
223
  pendingReferenceDecls,
220
224
  pendingReferenceAttachments,
225
+ pendingContainerResolutions,
221
226
  );
222
227
  }
223
228
  }
@@ -265,6 +270,7 @@ export function buildGraph(fileASTs) {
265
270
  entityKinds,
266
271
  pendingReferenceDecls,
267
272
  pendingReferenceAttachments,
273
+ pendingContainerResolutions,
268
274
  );
269
275
  }
270
276
  }
@@ -286,6 +292,12 @@ export function buildGraph(fileASTs) {
286
292
  // Resolve edge endpoints
287
293
  resolveEdges(graph, scopeIndex);
288
294
 
295
+ // Resolve container references (books/chapters) that may have forward references
296
+ resolveContainers(graph, scopeIndex, pendingContainerResolutions);
297
+
298
+ // Validate and apply ordering blocks after all nodes and relationships are established
299
+ validateOrderingBlocks(graph);
300
+
289
301
  return graph;
290
302
  }
291
303
 
@@ -496,6 +508,7 @@ function resolveRelatesEndpoint(graph, scopeIndex, name, startScopeNodeId, sourc
496
508
  * @param {Map<string, string>} [entityKinds] - Map for tracking non-node entity kinds by ID
497
509
  * @param {Array} [pendingReferenceDecls] - Reference declarations to resolve after indexing
498
510
  * @param {Array} [pendingReferenceAttachments] - Reference attachments to resolve after indexing
511
+ * @param {Array} [pendingContainerResolutions] - Container resolutions to resolve after all nodes are created
499
512
  */
500
513
  function processBody(
501
514
  graph,
@@ -511,7 +524,11 @@ function processBody(
511
524
  entityKinds,
512
525
  pendingReferenceDecls,
513
526
  pendingReferenceAttachments,
527
+ pendingContainerResolutions,
514
528
  ) {
529
+ // Collect ordering blocks to process after all children are added
530
+ const orderingBlocks = [];
531
+
515
532
  for (const decl of body) {
516
533
  if (decl.kind === 'anthology') {
517
534
  const nodeId = makeNodeId(universeName, 'anthology', decl.name);
@@ -541,6 +558,7 @@ function processBody(
541
558
  entityKinds,
542
559
  pendingReferenceDecls,
543
560
  pendingReferenceAttachments,
561
+ pendingContainerResolutions,
544
562
  );
545
563
  } else if (decl.kind === 'series') {
546
564
  const nodeId = makeNodeId(universeName, 'series', decl.name);
@@ -574,6 +592,7 @@ function processBody(
574
592
  entityKinds,
575
593
  pendingReferenceDecls,
576
594
  pendingReferenceAttachments,
595
+ pendingContainerResolutions,
577
596
  );
578
597
  } else if (decl.kind === 'book') {
579
598
  const nodeId = makeNodeId(universeName, 'book', decl.name);
@@ -616,6 +635,7 @@ function processBody(
616
635
  entityKinds,
617
636
  pendingReferenceDecls,
618
637
  pendingReferenceAttachments,
638
+ pendingContainerResolutions,
619
639
  );
620
640
  } else if (decl.kind === 'chapter') {
621
641
  const nodeId = makeNodeId(universeName, 'chapter', decl.name);
@@ -634,7 +654,9 @@ function processBody(
634
654
  const resolved = resolveNameInScope(graph, scopeIndex, decl.parentName, currentNodeId, decl.source);
635
655
  const containerNodeId = resolved.nodeId && !resolved.ambiguous ? resolved.nodeId : null;
636
656
 
637
- // Validate: chapter container must be a book
657
+ // Validate: chapter container must be a book (if found)
658
+ // Note: We defer "not found" validation until after all nodes are created
659
+ // to allow forward references (book defined after chapter)
638
660
  if (containerNodeId) {
639
661
  const containerNode = graph.nodes[containerNodeId];
640
662
  if (containerNode && containerNode.kind !== 'book') {
@@ -644,12 +666,14 @@ function processBody(
644
666
  source: decl.source,
645
667
  });
646
668
  }
647
- } else if (decl.parentName) {
648
- // Container not found
649
- graph.diagnostics.push({
650
- severity: 'error',
651
- message: `Chapter "${decl.name}" references unknown book "${decl.parentName}"`,
669
+ } else if (decl.parentName && pendingContainerResolutions) {
670
+ // Container not found yet - may be a forward reference, track for later resolution
671
+ pendingContainerResolutions.push({
672
+ nodeId,
673
+ parentName: decl.parentName,
674
+ scopeNodeId: currentNodeId,
652
675
  source: decl.source,
676
+ nodeKind: 'chapter',
653
677
  });
654
678
  }
655
679
 
@@ -683,6 +707,7 @@ function processBody(
683
707
  entityKinds,
684
708
  pendingReferenceDecls,
685
709
  pendingReferenceAttachments,
710
+ pendingContainerResolutions,
686
711
  );
687
712
  } else if (decl.kind === 'concept') {
688
713
  const nodeId = makeNodeId(universeName, 'concept', decl.name);
@@ -731,10 +756,13 @@ function processBody(
731
756
  finalRelatesNodeId = makeRelatesNodeId(universeName, decl.a, decl.b, index);
732
757
  }
733
758
 
734
- // Process relates body: extract describe, from blocks, and unknown blocks
759
+ // Process relates body: extract describe, relationships, from blocks, and unknown blocks
735
760
  const describeBlocks = decl.body.filter((b) => b.kind === 'describe');
761
+ const relationshipsBlocks = decl.body.filter((b) => b.kind === 'relationships');
736
762
  const fromBlocks = decl.body.filter((b) => b.kind === 'from');
737
- const unknownBlocks = decl.body.filter((b) => b.kind !== 'describe' && b.kind !== 'from');
763
+ const unknownBlocks = decl.body.filter(
764
+ (b) => b.kind !== 'describe' && b.kind !== 'from' && b.kind !== 'relationships',
765
+ );
738
766
 
739
767
  // Validate: only one top-level describe
740
768
  if (describeBlocks.length > 1) {
@@ -745,6 +773,24 @@ function processBody(
745
773
  });
746
774
  }
747
775
 
776
+ // Validate: only one top-level relationships block
777
+ if (relationshipsBlocks.length > 1) {
778
+ graph.diagnostics.push({
779
+ severity: 'error',
780
+ message: `Multiple relationships blocks in relates "${decl.a} and ${decl.b}"`,
781
+ source: relationshipsBlocks[1].source,
782
+ });
783
+ }
784
+
785
+ // Validate: relationships must have at least one value
786
+ if (relationshipsBlocks.length > 0 && relationshipsBlocks[0].values.length === 0) {
787
+ graph.diagnostics.push({
788
+ severity: 'error',
789
+ message: `Empty relationships block in relates "${decl.a} and ${decl.b}"`,
790
+ source: relationshipsBlocks[0].source,
791
+ });
792
+ }
793
+
748
794
  // Validate: from blocks must reference endpoints
749
795
  const endpointSet = new Set([decl.a, decl.b]);
750
796
  const fromByEndpoint = new Map();
@@ -790,6 +836,14 @@ function processBody(
790
836
  };
791
837
  }
792
838
 
839
+ // Add top-level relationships if present
840
+ if (relationshipsBlocks.length > 0) {
841
+ relatesNode.relationships = {
842
+ values: relationshipsBlocks[0].values,
843
+ source: relationshipsBlocks[0].source,
844
+ };
845
+ }
846
+
793
847
  // Process from blocks (will be resolved later)
794
848
  relatesNode.from = {};
795
849
  for (const fromBlock of fromBlocks) {
@@ -905,6 +959,9 @@ function processBody(
905
959
  universeName,
906
960
  });
907
961
  }
962
+ } else if (decl.kind === 'ordering') {
963
+ // Collect ordering blocks to process after all children are added
964
+ orderingBlocks.push(decl);
908
965
  } else if (decl.kind === 'documentation') {
909
966
  // DocumentationBlock - attach to current node
910
967
  const currentNode = graph.nodes[currentNodeId];
@@ -1069,6 +1126,30 @@ function processBody(
1069
1126
  });
1070
1127
  }
1071
1128
  }
1129
+
1130
+ // Store ordering blocks for later validation (after all nodes are created)
1131
+ // We'll validate them in a separate pass after the entire graph is built
1132
+ // This is necessary because children may be defined outside their parent's body
1133
+ // (e.g., "book X in Y" defined at anthology level, not inside series Y's body)
1134
+ if (orderingBlocks.length > 0) {
1135
+ const currentNode = graph.nodes[currentNodeId];
1136
+ if (!currentNode) {
1137
+ return;
1138
+ }
1139
+
1140
+ // Validate: only one ordering block per container
1141
+ if (orderingBlocks.length > 1) {
1142
+ graph.diagnostics.push({
1143
+ severity: 'warning',
1144
+ message: `Multiple ordering blocks in ${currentNode.kind} "${currentNode.name}". Using the first one.`,
1145
+ source: orderingBlocks[1].source,
1146
+ });
1147
+ }
1148
+
1149
+ // Store ordering block temporarily on the node for later validation
1150
+ // We'll process this in validateOrderingBlocks() after all nodes are created
1151
+ currentNode._pendingOrdering = orderingBlocks[0];
1152
+ }
1072
1153
  }
1073
1154
 
1074
1155
  /**
@@ -1293,6 +1374,65 @@ function resolveEdges(graph, scopeIndex) {
1293
1374
  }
1294
1375
  }
1295
1376
 
1377
+ /**
1378
+ * Resolves container references for chapters and books that may have forward references
1379
+ * @param {UniverseGraph} graph
1380
+ * @param {Map<string, Map<string, string[]>>} scopeIndex
1381
+ * @param {Array<{ nodeId: string, parentName: string, scopeNodeId: string, source: SourceSpan, nodeKind: 'chapter' | 'book' }>} pending
1382
+ */
1383
+ function resolveContainers(graph, scopeIndex, pending) {
1384
+ for (const item of pending) {
1385
+ const { nodeId, parentName, scopeNodeId, source, nodeKind } = item;
1386
+ const node = graph.nodes[nodeId];
1387
+ if (!node) continue;
1388
+
1389
+ // Try to resolve the container now that all nodes are created
1390
+ const resolved = resolveNameInScope(graph, scopeIndex, parentName, scopeNodeId, source);
1391
+ const containerNodeId = resolved.nodeId && !resolved.ambiguous ? resolved.nodeId : null;
1392
+
1393
+ if (containerNodeId) {
1394
+ const containerNode = graph.nodes[containerNodeId];
1395
+
1396
+ // Validate container kind
1397
+ if (nodeKind === 'chapter' && containerNode.kind !== 'book') {
1398
+ graph.diagnostics.push({
1399
+ severity: 'error',
1400
+ message: `Chapter "${node.name}" must belong to a book, but "${parentName}" is a ${containerNode.kind}`,
1401
+ source,
1402
+ });
1403
+ continue;
1404
+ }
1405
+
1406
+ // Update the node's container
1407
+ node.container = containerNodeId;
1408
+
1409
+ // Move the node from its temporary parent to the correct container
1410
+ const oldParent = node.parent;
1411
+ if (oldParent && graph.nodes[oldParent]) {
1412
+ const oldParentChildren = graph.nodes[oldParent].children;
1413
+ const idx = oldParentChildren.indexOf(nodeId);
1414
+ if (idx !== -1) {
1415
+ oldParentChildren.splice(idx, 1);
1416
+ }
1417
+ }
1418
+
1419
+ // Add to correct container
1420
+ node.parent = containerNodeId;
1421
+ if (!containerNode.children.includes(nodeId)) {
1422
+ containerNode.children.push(nodeId);
1423
+ }
1424
+ } else {
1425
+ // Still not found - report error
1426
+ const kindLabel = nodeKind === 'chapter' ? 'book' : 'container';
1427
+ graph.diagnostics.push({
1428
+ severity: 'error',
1429
+ message: `Chapter "${node.name}" references unknown ${kindLabel} "${parentName}"`,
1430
+ source,
1431
+ });
1432
+ }
1433
+ }
1434
+ }
1435
+
1296
1436
  /**
1297
1437
  * @param {string | undefined} raw
1298
1438
  * @returns {string | undefined}
@@ -1640,6 +1780,56 @@ function makeRelatesNodeId(universeName, a, b, index = 0) {
1640
1780
  return `${universeName}:relates:${a}:${b}:${index}`;
1641
1781
  }
1642
1782
 
1783
+ /**
1784
+ * Validates and applies ordering blocks after all nodes and relationships are established
1785
+ * This must run after all nodes are created because children may be defined outside their parent's body
1786
+ * (e.g., "book X in Y" defined at anthology level, not inside series Y's body)
1787
+ * @param {UniverseGraph} graph
1788
+ */
1789
+ function validateOrderingBlocks(graph) {
1790
+ for (const nodeId in graph.nodes) {
1791
+ const node = graph.nodes[nodeId];
1792
+ if (!node._pendingOrdering) {
1793
+ continue;
1794
+ }
1795
+
1796
+ const orderingBlock = node._pendingOrdering;
1797
+ const orderingIdentifiers = orderingBlock.identifiers;
1798
+
1799
+ // Build a map of child names to node IDs for validation
1800
+ const childNameToNodeId = new Map();
1801
+ for (const childId of node.children) {
1802
+ const childNode = graph.nodes[childId];
1803
+ if (childNode) {
1804
+ childNameToNodeId.set(childNode.name, childId);
1805
+ }
1806
+ }
1807
+
1808
+ // Validate ordering identifiers against actual children
1809
+ const validOrdering = [];
1810
+ for (const identifier of orderingIdentifiers) {
1811
+ if (childNameToNodeId.has(identifier)) {
1812
+ validOrdering.push(identifier);
1813
+ } else {
1814
+ // Unknown identifier - emit gentle warning
1815
+ graph.diagnostics.push({
1816
+ severity: 'warning',
1817
+ message: `Ordering block in ${node.kind} "${node.name}" references unknown child "${identifier}". This identifier will be ignored.`,
1818
+ source: orderingBlock.source,
1819
+ });
1820
+ }
1821
+ }
1822
+
1823
+ // Store ordering if there are any valid identifiers
1824
+ if (validOrdering.length > 0) {
1825
+ node.ordering = validOrdering;
1826
+ }
1827
+
1828
+ // Clean up temporary field
1829
+ delete node._pendingOrdering;
1830
+ }
1831
+ }
1832
+
1643
1833
  /**
1644
1834
  * Creates an edge ID
1645
1835
  * @param {string} universeName
package/src/ir.js CHANGED
@@ -61,6 +61,7 @@
61
61
  * @property {NodeId} [parent] - Tree parent node ID
62
62
  * @property {NodeId[]} children - Child node IDs
63
63
  * @property {NodeId} [container] - Container node ID (for book/chapter "in" relationship; always set for book/chapter nodes when resolved)
64
+ * @property {string[]} [ordering] - Optional ordering of direct children by identifier name
64
65
  * @property {TextBlock} [describe] - Describe block if present
65
66
  * @property {UnknownBlock[]} [unknownBlocks] - Unknown blocks if any
66
67
  * @property {string[]} [references] - Reference IDs attached to this node
@@ -69,6 +70,7 @@
69
70
  * @property {NodeId[]} [endpoints] - Endpoint node IDs (for relates nodes)
70
71
  * @property {string[]} [unresolvedEndpoints] - Unresolved endpoint names (for relates nodes)
71
72
  * @property {Record<string, FromView>} [from] - From blocks keyed by endpoint node ID (for relates nodes)
73
+ * @property {{ values: string[], source: SourceSpan }} [relationships] - Top-level relationships block (for relates nodes)
72
74
  */
73
75
 
74
76
  /**
package/src/parser.js CHANGED
@@ -16,6 +16,7 @@ import { mergeSpans } from './util/span.js';
16
16
  * @typedef {import('./ast.js').DescribeBlock} DescribeBlock
17
17
  * @typedef {import('./ast.js').UnknownBlock} UnknownBlock
18
18
  * @typedef {import('./ast.js').ReferencesBlock} ReferencesBlock
19
+ * @typedef {import('./ast.js').OrderingBlock} OrderingBlock
19
20
  * @typedef {import('./ast.js').RepositoryDecl} RepositoryDecl
20
21
  * @typedef {import('./ast.js').ReferenceDecl} ReferenceDecl
21
22
  * @typedef {import('./ast.js').DocumentationBlock} DocumentationBlock
@@ -177,7 +178,7 @@ class Parser {
177
178
  const startToken = this.expect('KEYWORD', 'universe');
178
179
  const nameToken = this.expect('IDENTIFIER');
179
180
  const lbrace = this.expect('LBRACE');
180
- const body = this.parseBlockBody(['anthology', 'series', 'book', 'chapter', 'concept', 'relates', 'describe', 'title', 'repository', 'reference', 'references', 'documentation']);
181
+ const body = this.parseBlockBody(['anthology', 'series', 'book', 'chapter', 'concept', 'relates', 'describe', 'title', 'repository', 'reference', 'references', 'ordering', 'documentation']);
181
182
  const rbrace = this.expect('RBRACE');
182
183
 
183
184
  return {
@@ -246,6 +247,8 @@ class Parser {
246
247
  body.push(this.parseRelationships());
247
248
  } else if (keyword === 'references' && allowedKeywords.includes('references')) {
248
249
  body.push(this.parseReferences());
250
+ } else if (keyword === 'ordering' && allowedKeywords.includes('ordering')) {
251
+ body.push(this.parseOrdering());
249
252
  } else if (keyword === 'documentation' && allowedKeywords.includes('documentation')) {
250
253
  body.push(this.parseDocumentation());
251
254
  } else {
@@ -290,6 +293,7 @@ class Parser {
290
293
  'concept',
291
294
  'relates',
292
295
  'references',
296
+ 'ordering',
293
297
  'documentation',
294
298
  'repository',
295
299
  'reference',
@@ -325,7 +329,7 @@ class Parser {
325
329
  }
326
330
 
327
331
  const lbrace = this.expect('LBRACE');
328
- const body = this.parseBlockBody(['book', 'chapter', 'describe', 'title', 'references', 'documentation', 'repository', 'reference']);
332
+ const body = this.parseBlockBody(['book', 'chapter', 'describe', 'title', 'references', 'ordering', 'documentation', 'repository', 'reference']);
329
333
  const rbrace = this.expect('RBRACE');
330
334
 
331
335
  return {
@@ -357,7 +361,7 @@ class Parser {
357
361
  }
358
362
 
359
363
  const lbrace = this.expect('LBRACE');
360
- const body = this.parseBlockBody(['chapter', 'describe', 'title', 'references', 'documentation', 'repository', 'reference']);
364
+ const body = this.parseBlockBody(['chapter', 'describe', 'title', 'references', 'ordering', 'documentation', 'repository', 'reference']);
361
365
  const rbrace = this.expect('RBRACE');
362
366
 
363
367
  return {
@@ -424,7 +428,7 @@ class Parser {
424
428
  }
425
429
 
426
430
  const lbrace = this.expect('LBRACE');
427
- const body = this.parseBlockBody(['describe', 'title', 'references', 'documentation', 'repository', 'reference']);
431
+ const body = this.parseBlockBody(['describe', 'title', 'references', 'ordering', 'documentation', 'repository', 'reference']);
428
432
  const rbrace = this.expect('RBRACE');
429
433
 
430
434
  return {
@@ -449,7 +453,7 @@ class Parser {
449
453
  this.expect('KEYWORD', 'and');
450
454
  const bToken = this.expect('IDENTIFIER');
451
455
  const lbrace = this.expect('LBRACE');
452
- const body = this.parseBlockBody(['describe', 'title', 'from']);
456
+ const body = this.parseBlockBody(['describe', 'title', 'from', 'relationships']);
453
457
  const rbrace = this.expect('RBRACE');
454
458
 
455
459
  return {
@@ -641,6 +645,62 @@ class Parser {
641
645
  };
642
646
  }
643
647
 
648
+ /**
649
+ * Parses an ordering block containing a list of identifiers
650
+ * @returns {OrderingBlock}
651
+ */
652
+ parseOrdering() {
653
+ const startToken = this.match('KEYWORD') ? this.expect('KEYWORD', 'ordering') : this.expect('IDENTIFIER', 'ordering');
654
+ const lbrace = this.expect('LBRACE');
655
+ const identifiers = [];
656
+ let depth = 1;
657
+
658
+ while (this.pos < this.tokens.length && !this.match('EOF')) {
659
+ if (this.match('LBRACE')) {
660
+ throw new Error(`Unexpected '{' in ordering block at ${this.file}:${this.peek()?.span.start.line}. Use identifiers only.`);
661
+ }
662
+
663
+ if (this.match('RBRACE')) {
664
+ depth -= 1;
665
+ this.advance();
666
+ if (depth === 0) {
667
+ break;
668
+ }
669
+ continue;
670
+ }
671
+
672
+ if (depth !== 1) {
673
+ this.advance();
674
+ continue;
675
+ }
676
+
677
+ if (this.match('COMMA')) {
678
+ this.advance();
679
+ continue;
680
+ }
681
+
682
+ if (this.match('IDENTIFIER') || this.match('KEYWORD')) {
683
+ const identifierToken = this.advance();
684
+ if (identifierToken) {
685
+ identifiers.push(identifierToken.value);
686
+ }
687
+ continue;
688
+ }
689
+
690
+ this.advance();
691
+ }
692
+
693
+ return {
694
+ kind: 'ordering',
695
+ identifiers,
696
+ source: {
697
+ file: this.file,
698
+ start: startToken.span.start,
699
+ end: this.tokens[this.pos - 1]?.span.end || lbrace.span.end,
700
+ },
701
+ };
702
+ }
703
+
644
704
  /**
645
705
  * Parses a references block containing a list of identifier paths
646
706
  * @returns {ReferencesBlock}