@sprig-and-prose/sprig-universe 0.3.3 → 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 +1 -2
- package/src/ast.js +13 -6
- package/src/graph.js +84 -0
- package/src/ir.js +1 -0
- package/src/parser.js +64 -4
package/package.json
CHANGED
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
|
|
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
|
|
@@ -294,6 +295,9 @@ export function buildGraph(fileASTs) {
|
|
|
294
295
|
// Resolve container references (books/chapters) that may have forward references
|
|
295
296
|
resolveContainers(graph, scopeIndex, pendingContainerResolutions);
|
|
296
297
|
|
|
298
|
+
// Validate and apply ordering blocks after all nodes and relationships are established
|
|
299
|
+
validateOrderingBlocks(graph);
|
|
300
|
+
|
|
297
301
|
return graph;
|
|
298
302
|
}
|
|
299
303
|
|
|
@@ -522,6 +526,9 @@ function processBody(
|
|
|
522
526
|
pendingReferenceAttachments,
|
|
523
527
|
pendingContainerResolutions,
|
|
524
528
|
) {
|
|
529
|
+
// Collect ordering blocks to process after all children are added
|
|
530
|
+
const orderingBlocks = [];
|
|
531
|
+
|
|
525
532
|
for (const decl of body) {
|
|
526
533
|
if (decl.kind === 'anthology') {
|
|
527
534
|
const nodeId = makeNodeId(universeName, 'anthology', decl.name);
|
|
@@ -952,6 +959,9 @@ function processBody(
|
|
|
952
959
|
universeName,
|
|
953
960
|
});
|
|
954
961
|
}
|
|
962
|
+
} else if (decl.kind === 'ordering') {
|
|
963
|
+
// Collect ordering blocks to process after all children are added
|
|
964
|
+
orderingBlocks.push(decl);
|
|
955
965
|
} else if (decl.kind === 'documentation') {
|
|
956
966
|
// DocumentationBlock - attach to current node
|
|
957
967
|
const currentNode = graph.nodes[currentNodeId];
|
|
@@ -1116,6 +1126,30 @@ function processBody(
|
|
|
1116
1126
|
});
|
|
1117
1127
|
}
|
|
1118
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
|
+
}
|
|
1119
1153
|
}
|
|
1120
1154
|
|
|
1121
1155
|
/**
|
|
@@ -1746,6 +1780,56 @@ function makeRelatesNodeId(universeName, a, b, index = 0) {
|
|
|
1746
1780
|
return `${universeName}:relates:${a}:${b}:${index}`;
|
|
1747
1781
|
}
|
|
1748
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
|
+
|
|
1749
1833
|
/**
|
|
1750
1834
|
* Creates an edge ID
|
|
1751
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
|
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 {
|
|
@@ -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}
|