@sprig-and-prose/sprig-universe 0.3.4 → 0.4.0
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 -1
- package/src/ast.js +51 -4
- package/src/graph.js +371 -12
- package/src/ir.js +38 -2
- package/src/parser.js +655 -52
- package/src/scanner.js +1 -0
- package/test/aliases.test.js +91 -0
- package/test/fixtures/aliases-basic.prose +7 -0
- package/test/fixtures/aliases-conflict.prose +6 -0
- package/test/fixtures/aliases-no-leak.prose +5 -0
- package/test/fixtures/aliases-shadowing.prose +11 -0
- package/test/fixtures/aliases-single-line.prose +7 -0
- package/test/fixtures/relationship-errors.prose +20 -0
- package/test/fixtures/relationship-paired.prose +28 -0
- package/test/fixtures/relationship-scoped.prose +23 -0
- package/test/fixtures/relationship-symmetric.prose +18 -0
- package/test/fixtures/relationship-usage.prose +31 -0
package/package.json
CHANGED
package/src/ast.js
CHANGED
|
@@ -113,25 +113,31 @@
|
|
|
113
113
|
/**
|
|
114
114
|
* @typedef {Object} UniverseDecl
|
|
115
115
|
* @property {string} kind - Always 'universe'
|
|
116
|
+
* @property {string} spelledKind - Kind token as written (may be alias)
|
|
116
117
|
* @property {string} name - Universe name
|
|
117
|
-
* @property {
|
|
118
|
+
* @property {Record<string, string>} [aliases] - Alias mappings declared in this scope
|
|
119
|
+
* @property {Array<AnthologyDecl | SeriesDecl | BookDecl | ChapterDecl | ConceptDecl | RelatesDecl | RelationshipDecl | DescribeBlock | TitleBlock | ReferencesBlock | OrderingBlock | RepositoryDecl | ReferenceDecl | NamedDocumentBlock | UnknownBlock>} body - Body declarations
|
|
118
120
|
* @property {SourceSpan} source - Source span
|
|
119
121
|
*/
|
|
120
122
|
|
|
121
123
|
/**
|
|
122
124
|
* @typedef {Object} AnthologyDecl
|
|
123
125
|
* @property {string} kind - Always 'anthology'
|
|
126
|
+
* @property {string} spelledKind - Kind token as written (may be alias)
|
|
124
127
|
* @property {string} name - Anthology name
|
|
125
128
|
* @property {string} [parentName] - Optional parent universe name (from "in UniverseName")
|
|
126
|
-
* @property {
|
|
129
|
+
* @property {Record<string, string>} [aliases] - Alias mappings declared in this scope
|
|
130
|
+
* @property {Array<SeriesDecl | BookDecl | ChapterDecl | ConceptDecl | RelatesDecl | RelationshipDecl | DescribeBlock | TitleBlock | ReferencesBlock | OrderingBlock | DocumentationBlock | RepositoryDecl | ReferenceDecl | UnknownBlock>} body - Body declarations
|
|
127
131
|
* @property {SourceSpan} source - Source span
|
|
128
132
|
*/
|
|
129
133
|
|
|
130
134
|
/**
|
|
131
135
|
* @typedef {Object} SeriesDecl
|
|
132
136
|
* @property {string} kind - Always 'series'
|
|
137
|
+
* @property {string} spelledKind - Kind token as written (may be alias)
|
|
133
138
|
* @property {string} name - Series name
|
|
134
139
|
* @property {string} [parentName] - Optional parent anthology name (from "in AnthologyName")
|
|
140
|
+
* @property {Record<string, string>} [aliases] - Alias mappings declared in this scope
|
|
135
141
|
* @property {Array<BookDecl | ChapterDecl | DescribeBlock | TitleBlock | ReferencesBlock | OrderingBlock | DocumentationBlock | RepositoryDecl | ReferenceDecl | UnknownBlock>} body - Body declarations
|
|
136
142
|
* @property {SourceSpan} source - Source span
|
|
137
143
|
*/
|
|
@@ -139,8 +145,10 @@
|
|
|
139
145
|
/**
|
|
140
146
|
* @typedef {Object} BookDecl
|
|
141
147
|
* @property {string} kind - Always 'book'
|
|
148
|
+
* @property {string} spelledKind - Kind token as written (may be alias)
|
|
142
149
|
* @property {string} name - Book name
|
|
143
150
|
* @property {string} [parentName] - Optional parent name (from "in ParentName")
|
|
151
|
+
* @property {Record<string, string>} [aliases] - Alias mappings declared in this scope
|
|
144
152
|
* @property {Array<ChapterDecl | DescribeBlock | TitleBlock | ReferencesBlock | OrderingBlock | DocumentationBlock | RepositoryDecl | ReferenceDecl | UnknownBlock>} body - Body declarations
|
|
145
153
|
* @property {SourceSpan} source - Source span
|
|
146
154
|
*/
|
|
@@ -148,8 +156,10 @@
|
|
|
148
156
|
/**
|
|
149
157
|
* @typedef {Object} ChapterDecl
|
|
150
158
|
* @property {string} kind - Always 'chapter'
|
|
159
|
+
* @property {string} spelledKind - Kind token as written (may be alias)
|
|
151
160
|
* @property {string} name - Chapter name
|
|
152
161
|
* @property {string} parentName - Parent name (from "in ParentName")
|
|
162
|
+
* @property {Record<string, string>} [aliases] - Alias mappings declared in this scope
|
|
153
163
|
* @property {Array<DescribeBlock | ReferencesBlock | OrderingBlock | DocumentationBlock | RepositoryDecl | ReferenceDecl | UnknownBlock>} body - Body declarations
|
|
154
164
|
* @property {SourceSpan} source - Source span
|
|
155
165
|
*/
|
|
@@ -157,16 +167,31 @@
|
|
|
157
167
|
/**
|
|
158
168
|
* @typedef {Object} ConceptDecl
|
|
159
169
|
* @property {string} kind - Always 'concept'
|
|
170
|
+
* @property {string} spelledKind - Kind token as written (may be alias)
|
|
160
171
|
* @property {string} name - Concept name
|
|
161
172
|
* @property {string} [parentName] - Optional parent name (from "in ParentName")
|
|
173
|
+
* @property {Record<string, string>} [aliases] - Alias mappings declared in this scope
|
|
162
174
|
* @property {Array<DescribeBlock | ReferencesBlock | OrderingBlock | DocumentationBlock | RepositoryDecl | ReferenceDecl | UnknownBlock>} body - Body declarations
|
|
163
175
|
* @property {SourceSpan} source - Source span
|
|
164
176
|
*/
|
|
165
177
|
|
|
178
|
+
/**
|
|
179
|
+
* @typedef {Object} RelationshipTarget
|
|
180
|
+
* @property {string} id - Target identifier
|
|
181
|
+
* @property {DescribeBlock} [metadata] - Optional metadata block (describe) attached to this target
|
|
182
|
+
*/
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @typedef {Object} RelationshipEntry
|
|
186
|
+
* @property {string} relationshipId - Relationship identifier being used
|
|
187
|
+
* @property {RelationshipTarget[]} targets - Array of target identifiers with optional metadata
|
|
188
|
+
*/
|
|
189
|
+
|
|
166
190
|
/**
|
|
167
191
|
* @typedef {Object} RelationshipsBlock
|
|
168
192
|
* @property {string} kind - Always 'relationships'
|
|
169
|
-
* @property {
|
|
193
|
+
* @property {RelationshipEntry[]} entries - Array of relationship entries (new syntax)
|
|
194
|
+
* @property {string[]} [values] - Array of string literal values (legacy syntax for relates blocks)
|
|
170
195
|
* @property {SourceSpan} source - Source span
|
|
171
196
|
*/
|
|
172
197
|
|
|
@@ -178,11 +203,33 @@
|
|
|
178
203
|
* @property {SourceSpan} source - Source span
|
|
179
204
|
*/
|
|
180
205
|
|
|
206
|
+
/**
|
|
207
|
+
* @typedef {Object} RelationshipFromSide
|
|
208
|
+
* @property {string} [label] - Optional label string
|
|
209
|
+
* @property {DescribeBlock} [describe] - Optional describe block
|
|
210
|
+
*/
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @typedef {Object} RelationshipDecl
|
|
214
|
+
* @property {string} kind - Always 'relationship'
|
|
215
|
+
* @property {string} spelledKind - Kind token as written (may be alias)
|
|
216
|
+
* @property {'symmetric' | 'paired'} type - Relationship type
|
|
217
|
+
* @property {string} [id] - Relationship identifier (for symmetric)
|
|
218
|
+
* @property {string} [leftId] - Left side identifier (for paired)
|
|
219
|
+
* @property {string} [rightId] - Right side identifier (for paired)
|
|
220
|
+
* @property {DescribeBlock} [describe] - Optional pair-level describe block
|
|
221
|
+
* @property {string} [label] - Optional label (for symmetric)
|
|
222
|
+
* @property {Record<string, RelationshipFromSide>} [from] - Per-side metadata (for paired)
|
|
223
|
+
* @property {SourceSpan} source - Source span
|
|
224
|
+
*/
|
|
225
|
+
|
|
181
226
|
/**
|
|
182
227
|
* @typedef {Object} RelatesDecl
|
|
183
228
|
* @property {string} kind - Always 'relates'
|
|
229
|
+
* @property {string} spelledKind - Kind token as written (may be alias)
|
|
184
230
|
* @property {string} a - First endpoint text
|
|
185
231
|
* @property {string} b - Second endpoint text
|
|
232
|
+
* @property {Record<string, string>} [aliases] - Alias mappings declared in this scope
|
|
186
233
|
* @property {Array<DescribeBlock | TitleBlock | FromBlock | RelationshipsBlock | UnknownBlock>} body - Body declarations
|
|
187
234
|
* @property {SourceSpan} source - Source span
|
|
188
235
|
*/
|
|
@@ -248,7 +295,7 @@
|
|
|
248
295
|
*/
|
|
249
296
|
|
|
250
297
|
/**
|
|
251
|
-
* @typedef {UniverseDecl | AnthologyDecl | SeriesDecl | BookDecl | ChapterDecl | ConceptDecl | RelatesDecl | DescribeBlock | NoteBlock | TitleBlock | FromBlock | RelationshipsBlock | ReferencesBlock | ReferenceDecl | DocumentationBlock | DocumentBlock | NamedDocumentBlock | RepositoryDecl | UnknownBlock | SceneDecl | UsingBlock | ActorDecl | TypeBlock | IdentityBlock | SourceBlock | TransformsBlock | TransformBlock} ASTNode
|
|
298
|
+
* @typedef {UniverseDecl | AnthologyDecl | SeriesDecl | BookDecl | ChapterDecl | ConceptDecl | RelatesDecl | RelationshipDecl | DescribeBlock | NoteBlock | TitleBlock | FromBlock | RelationshipsBlock | ReferencesBlock | ReferenceDecl | DocumentationBlock | DocumentBlock | NamedDocumentBlock | RepositoryDecl | UnknownBlock | SceneDecl | UsingBlock | ActorDecl | TypeBlock | IdentityBlock | SourceBlock | TransformsBlock | TransformBlock} ASTNode
|
|
252
299
|
*/
|
|
253
300
|
|
|
254
301
|
/**
|
package/src/graph.js
CHANGED
|
@@ -22,6 +22,7 @@ import { normalizeProseBlock } from './util/text.js';
|
|
|
22
22
|
* @typedef {import('./ast.js').ChapterDecl} ChapterDecl
|
|
23
23
|
* @typedef {import('./ast.js').ConceptDecl} ConceptDecl
|
|
24
24
|
* @typedef {import('./ast.js').RelatesDecl} RelatesDecl
|
|
25
|
+
* @typedef {import('./ast.js').RelationshipDecl} RelationshipDecl
|
|
25
26
|
* @typedef {import('./ast.js').DescribeBlock} DescribeBlock
|
|
26
27
|
* @typedef {import('./ast.js').TitleBlock} TitleBlock
|
|
27
28
|
* @typedef {import('./ast.js').FromBlock} FromBlock
|
|
@@ -66,6 +67,15 @@ export function buildGraph(fileASTs) {
|
|
|
66
67
|
// Track repositories and references per universe for scoped resolution
|
|
67
68
|
const repositoriesByUniverse = new Map(); // universeName -> Map<id, RepositoryDecl>
|
|
68
69
|
const referencesByUniverse = new Map(); // universeName -> Map<id, ReferenceDecl>
|
|
70
|
+
|
|
71
|
+
// Track relationship declarations per scope (containerNodeId -> Map<relationshipId, RelationshipDecl>)
|
|
72
|
+
const relationshipsByScope = new Map(); // containerNodeId -> Map<relationshipId, RelationshipDecl>
|
|
73
|
+
|
|
74
|
+
// Store relationship declarations by universe for UI access
|
|
75
|
+
const relationshipDeclsByUniverse = new Map(); // universeName -> Map<relationshipId, RelationshipDecl>
|
|
76
|
+
|
|
77
|
+
// Initialize relationshipDecls in graph
|
|
78
|
+
graph.relationshipDecls = {};
|
|
69
79
|
|
|
70
80
|
// Track entity kinds for duplicate detection (references/repositories)
|
|
71
81
|
const entityKinds = new Map(); // id -> kind
|
|
@@ -190,6 +200,8 @@ export function buildGraph(fileASTs) {
|
|
|
190
200
|
repositoriesByUniverse.get(universeName),
|
|
191
201
|
referencesByUniverse.get(universeName),
|
|
192
202
|
entityKinds,
|
|
203
|
+
relationshipsByScope,
|
|
204
|
+
relationshipDeclsByUniverse,
|
|
193
205
|
pendingReferenceDecls,
|
|
194
206
|
pendingReferenceAttachments,
|
|
195
207
|
pendingContainerResolutions,
|
|
@@ -220,6 +232,8 @@ export function buildGraph(fileASTs) {
|
|
|
220
232
|
repositoriesByUniverse.get(universeName),
|
|
221
233
|
referencesByUniverse.get(universeName),
|
|
222
234
|
entityKinds,
|
|
235
|
+
relationshipsByScope,
|
|
236
|
+
relationshipDeclsByUniverse,
|
|
223
237
|
pendingReferenceDecls,
|
|
224
238
|
pendingReferenceAttachments,
|
|
225
239
|
pendingContainerResolutions,
|
|
@@ -256,6 +270,7 @@ export function buildGraph(fileASTs) {
|
|
|
256
270
|
continue;
|
|
257
271
|
}
|
|
258
272
|
}
|
|
273
|
+
// Process the declaration itself first
|
|
259
274
|
processBody(
|
|
260
275
|
graph,
|
|
261
276
|
universeName,
|
|
@@ -268,6 +283,8 @@ export function buildGraph(fileASTs) {
|
|
|
268
283
|
repositoriesByUniverse.get(universeName),
|
|
269
284
|
referencesByUniverse.get(universeName),
|
|
270
285
|
entityKinds,
|
|
286
|
+
relationshipsByScope,
|
|
287
|
+
relationshipDeclsByUniverse,
|
|
271
288
|
pendingReferenceDecls,
|
|
272
289
|
pendingReferenceAttachments,
|
|
273
290
|
pendingContainerResolutions,
|
|
@@ -298,6 +315,34 @@ export function buildGraph(fileASTs) {
|
|
|
298
315
|
// Validate and apply ordering blocks after all nodes and relationships are established
|
|
299
316
|
validateOrderingBlocks(graph);
|
|
300
317
|
|
|
318
|
+
// Extract asserted edges from both relationships blocks and relates nodes
|
|
319
|
+
graph.edgesAsserted = extractAssertedEdges(graph, scopeIndex);
|
|
320
|
+
|
|
321
|
+
// Preserve existing edges object format for relates (for code that still uses it)
|
|
322
|
+
graph.edgesByRelates = graph.edges;
|
|
323
|
+
|
|
324
|
+
// Normalize edges (add inverse edges for paired/symmetric relationships)
|
|
325
|
+
const universeNames = Array.from(universeNameToFiles.keys());
|
|
326
|
+
if (universeNames.length > 0) {
|
|
327
|
+
// For now, assume single universe (as per buildGraph contract)
|
|
328
|
+
const universeName = universeNames[0];
|
|
329
|
+
graph.edges = normalizeEdges(
|
|
330
|
+
graph.edgesAsserted,
|
|
331
|
+
relationshipDeclsByUniverse,
|
|
332
|
+
universeName,
|
|
333
|
+
);
|
|
334
|
+
} else {
|
|
335
|
+
graph.edges = [];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Convert relationshipDeclsByUniverse Maps to plain objects for JSON serialization
|
|
339
|
+
// (already done above, but ensure all universes are initialized)
|
|
340
|
+
for (const [universeName, declsMap] of relationshipDeclsByUniverse.entries()) {
|
|
341
|
+
if (!graph.relationshipDecls[universeName]) {
|
|
342
|
+
graph.relationshipDecls[universeName] = {};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
301
346
|
return graph;
|
|
302
347
|
}
|
|
303
348
|
|
|
@@ -350,7 +395,7 @@ function addNameToScope(graph, scopeIndex, scopeNodeId, name, nodeId) {
|
|
|
350
395
|
*/
|
|
351
396
|
function resolveNameInScope(graph, scopeIndex, name, startScopeNodeId, source) {
|
|
352
397
|
// Handle qualified names (dot notation)
|
|
353
|
-
if (name.includes('.')) {
|
|
398
|
+
if (name && name.includes('.')) {
|
|
354
399
|
const parts = name.split('.');
|
|
355
400
|
// Start from universe and resolve each part
|
|
356
401
|
const universeName = startScopeNodeId.split(':')[0];
|
|
@@ -506,6 +551,8 @@ function resolveRelatesEndpoint(graph, scopeIndex, name, startScopeNodeId, sourc
|
|
|
506
551
|
* @param {Map<string, RepositoryDecl>} [reposMap] - Map for tracking repository declarations (universe scope only)
|
|
507
552
|
* @param {Map<string, ReferenceDecl>} [refsMap] - Map for tracking reference declarations (universe scope only)
|
|
508
553
|
* @param {Map<string, string>} [entityKinds] - Map for tracking non-node entity kinds by ID
|
|
554
|
+
* @param {Map<string, Map<string, RelationshipDecl>>} relationshipsByScope - Map for tracking relationship declarations per scope (required)
|
|
555
|
+
* @param {Map<string, Map<string, RelationshipDecl>>} relationshipDeclsByUniverse - Map for storing relationship declarations by universe for UI access
|
|
509
556
|
* @param {Array} [pendingReferenceDecls] - Reference declarations to resolve after indexing
|
|
510
557
|
* @param {Array} [pendingReferenceAttachments] - Reference attachments to resolve after indexing
|
|
511
558
|
* @param {Array} [pendingContainerResolutions] - Container resolutions to resolve after all nodes are created
|
|
@@ -522,6 +569,8 @@ function processBody(
|
|
|
522
569
|
reposMap,
|
|
523
570
|
refsMap,
|
|
524
571
|
entityKinds,
|
|
572
|
+
relationshipsByScope,
|
|
573
|
+
relationshipDeclsByUniverse,
|
|
525
574
|
pendingReferenceDecls,
|
|
526
575
|
pendingReferenceAttachments,
|
|
527
576
|
pendingContainerResolutions,
|
|
@@ -529,8 +578,88 @@ function processBody(
|
|
|
529
578
|
// Collect ordering blocks to process after all children are added
|
|
530
579
|
const orderingBlocks = [];
|
|
531
580
|
|
|
581
|
+
// Ensure relationshipsByScope is initialized as a Map
|
|
582
|
+
// This should always be provided, but check to prevent runtime errors
|
|
583
|
+
if (!relationshipsByScope) {
|
|
584
|
+
throw new Error(`relationshipsByScope is required but was ${relationshipsByScope}`);
|
|
585
|
+
}
|
|
586
|
+
if (typeof relationshipsByScope.has !== 'function' || typeof relationshipsByScope.set !== 'function' || typeof relationshipsByScope.get !== 'function') {
|
|
587
|
+
throw new Error(`relationshipsByScope must be a Map, got ${typeof relationshipsByScope} with has: ${typeof relationshipsByScope.has}, set: ${typeof relationshipsByScope.set}, get: ${typeof relationshipsByScope.get}`);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Initialize relationship scope for current container if not exists
|
|
591
|
+
if (!relationshipsByScope.has(currentNodeId)) {
|
|
592
|
+
relationshipsByScope.set(currentNodeId, new Map());
|
|
593
|
+
}
|
|
594
|
+
// Inherit parent relationships
|
|
595
|
+
if (parentNodeId && relationshipsByScope.has(parentNodeId)) {
|
|
596
|
+
const parentRelationships = relationshipsByScope.get(parentNodeId);
|
|
597
|
+
const currentRelationships = relationshipsByScope.get(currentNodeId);
|
|
598
|
+
if (parentRelationships && currentRelationships) {
|
|
599
|
+
for (const [id, rel] of parentRelationships) {
|
|
600
|
+
if (!currentRelationships.has(id)) {
|
|
601
|
+
currentRelationships.set(id, rel);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
532
607
|
for (const decl of body) {
|
|
533
|
-
if (decl.kind === '
|
|
608
|
+
if (decl.kind === 'relationship' && relationshipsByScope) {
|
|
609
|
+
// Track relationship declaration in current scope
|
|
610
|
+
const currentRelationships = relationshipsByScope.get(currentNodeId);
|
|
611
|
+
if (currentRelationships) {
|
|
612
|
+
if (decl.type === 'symmetric' && decl.id) {
|
|
613
|
+
currentRelationships.set(decl.id, decl);
|
|
614
|
+
// Also store in universe-level map for UI access
|
|
615
|
+
if (!relationshipDeclsByUniverse.has(universeName)) {
|
|
616
|
+
relationshipDeclsByUniverse.set(universeName, new Map());
|
|
617
|
+
graph.relationshipDecls[universeName] = {};
|
|
618
|
+
}
|
|
619
|
+
const universeDecls = relationshipDeclsByUniverse.get(universeName);
|
|
620
|
+
if (universeDecls) {
|
|
621
|
+
universeDecls.set(decl.id, {
|
|
622
|
+
type: decl.type,
|
|
623
|
+
id: decl.id,
|
|
624
|
+
describe: decl.describe,
|
|
625
|
+
label: decl.label,
|
|
626
|
+
source: decl.source,
|
|
627
|
+
});
|
|
628
|
+
graph.relationshipDecls[universeName][decl.id] = {
|
|
629
|
+
type: decl.type,
|
|
630
|
+
id: decl.id,
|
|
631
|
+
describe: decl.describe,
|
|
632
|
+
label: decl.label,
|
|
633
|
+
source: decl.source,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
} else if (decl.type === 'paired') {
|
|
637
|
+
if (decl.leftId) currentRelationships.set(decl.leftId, decl);
|
|
638
|
+
if (decl.rightId) currentRelationships.set(decl.rightId, decl);
|
|
639
|
+
// Also store in universe-level map for UI access (store once per pair)
|
|
640
|
+
if (!relationshipDeclsByUniverse.has(universeName)) {
|
|
641
|
+
relationshipDeclsByUniverse.set(universeName, new Map());
|
|
642
|
+
graph.relationshipDecls[universeName] = {};
|
|
643
|
+
}
|
|
644
|
+
const universeDecls = relationshipDeclsByUniverse.get(universeName);
|
|
645
|
+
if (universeDecls && decl.leftId && decl.rightId) {
|
|
646
|
+
// Store under both IDs for easy lookup
|
|
647
|
+
const declModel = {
|
|
648
|
+
type: decl.type,
|
|
649
|
+
leftId: decl.leftId,
|
|
650
|
+
rightId: decl.rightId,
|
|
651
|
+
describe: decl.describe,
|
|
652
|
+
from: decl.from,
|
|
653
|
+
source: decl.source,
|
|
654
|
+
};
|
|
655
|
+
universeDecls.set(decl.leftId, declModel);
|
|
656
|
+
universeDecls.set(decl.rightId, declModel);
|
|
657
|
+
graph.relationshipDecls[universeName][decl.leftId] = declModel;
|
|
658
|
+
graph.relationshipDecls[universeName][decl.rightId] = declModel;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
} else if (decl.kind === 'anthology') {
|
|
534
663
|
const nodeId = makeNodeId(universeName, 'anthology', decl.name);
|
|
535
664
|
checkDuplicateInScope(graph, scopeIndex, currentNodeId, decl.name, nodeId, 'anthology', decl.source);
|
|
536
665
|
let actualParentNodeId = parentNodeId;
|
|
@@ -556,6 +685,8 @@ function processBody(
|
|
|
556
685
|
reposMap,
|
|
557
686
|
refsMap,
|
|
558
687
|
entityKinds,
|
|
688
|
+
relationshipsByScope,
|
|
689
|
+
relationshipDeclsByUniverse,
|
|
559
690
|
pendingReferenceDecls,
|
|
560
691
|
pendingReferenceAttachments,
|
|
561
692
|
pendingContainerResolutions,
|
|
@@ -590,6 +721,8 @@ function processBody(
|
|
|
590
721
|
reposMap,
|
|
591
722
|
refsMap,
|
|
592
723
|
entityKinds,
|
|
724
|
+
relationshipsByScope,
|
|
725
|
+
relationshipDeclsByUniverse,
|
|
593
726
|
pendingReferenceDecls,
|
|
594
727
|
pendingReferenceAttachments,
|
|
595
728
|
pendingContainerResolutions,
|
|
@@ -633,6 +766,8 @@ function processBody(
|
|
|
633
766
|
reposMap,
|
|
634
767
|
refsMap,
|
|
635
768
|
entityKinds,
|
|
769
|
+
relationshipsByScope,
|
|
770
|
+
relationshipDeclsByUniverse,
|
|
636
771
|
pendingReferenceDecls,
|
|
637
772
|
pendingReferenceAttachments,
|
|
638
773
|
pendingContainerResolutions,
|
|
@@ -705,6 +840,8 @@ function processBody(
|
|
|
705
840
|
reposMap,
|
|
706
841
|
refsMap,
|
|
707
842
|
entityKinds,
|
|
843
|
+
relationshipsByScope,
|
|
844
|
+
relationshipDeclsByUniverse,
|
|
708
845
|
pendingReferenceDecls,
|
|
709
846
|
pendingReferenceAttachments,
|
|
710
847
|
pendingContainerResolutions,
|
|
@@ -727,6 +864,27 @@ function processBody(
|
|
|
727
864
|
graph.nodes[nodeId] = node;
|
|
728
865
|
graph.nodes[actualParentNodeId].children.push(nodeId);
|
|
729
866
|
addNameToScope(graph, scopeIndex, currentNodeId, decl.name, nodeId);
|
|
867
|
+
|
|
868
|
+
// Validate relationships blocks in concept body
|
|
869
|
+
const relationshipsBlocks = decl.body.filter((b) => b.kind === 'relationships');
|
|
870
|
+
for (const relBlock of relationshipsBlocks) {
|
|
871
|
+
if (relBlock.entries && relationshipsByScope) {
|
|
872
|
+
// New syntax: validate relationship IDs
|
|
873
|
+
const currentRelationships = relationshipsByScope.get(currentNodeId);
|
|
874
|
+
if (currentRelationships) {
|
|
875
|
+
for (const entry of relBlock.entries) {
|
|
876
|
+
if (!currentRelationships.has(entry.relationshipId)) {
|
|
877
|
+
graph.diagnostics.push({
|
|
878
|
+
severity: 'error',
|
|
879
|
+
message: `Undeclared relationship identifier "${entry.relationshipId}" in relationships block`,
|
|
880
|
+
source: relBlock.source,
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
730
888
|
processBody(
|
|
731
889
|
graph,
|
|
732
890
|
universeName,
|
|
@@ -739,8 +897,10 @@ function processBody(
|
|
|
739
897
|
reposMap,
|
|
740
898
|
refsMap,
|
|
741
899
|
entityKinds,
|
|
900
|
+
relationshipsByScope,
|
|
742
901
|
pendingReferenceDecls,
|
|
743
902
|
pendingReferenceAttachments,
|
|
903
|
+
pendingContainerResolutions,
|
|
744
904
|
);
|
|
745
905
|
} else if (decl.kind === 'relates') {
|
|
746
906
|
// Check for duplicate relates in reverse order
|
|
@@ -825,6 +985,8 @@ function processBody(
|
|
|
825
985
|
source: decl.source,
|
|
826
986
|
endpoints: [], // Will be populated during resolution
|
|
827
987
|
unresolvedEndpoints: [decl.a, decl.b], // Will be cleared during resolution
|
|
988
|
+
spelledKind: decl.spelledKind,
|
|
989
|
+
aliases: decl.aliases && Object.keys(decl.aliases).length > 0 ? decl.aliases : undefined,
|
|
828
990
|
};
|
|
829
991
|
|
|
830
992
|
// Add top-level describe if present
|
|
@@ -951,6 +1113,9 @@ function processBody(
|
|
|
951
1113
|
} else if (decl.kind === 'title') {
|
|
952
1114
|
// Title blocks are attached to their parent node
|
|
953
1115
|
// This is handled in createNode
|
|
1116
|
+
} else if (decl.kind === 'relationships') {
|
|
1117
|
+
// Relationships blocks are attached to their parent node
|
|
1118
|
+
// This is handled in createNode
|
|
954
1119
|
} else if (decl.kind === 'references') {
|
|
955
1120
|
if (pendingReferenceAttachments) {
|
|
956
1121
|
pendingReferenceAttachments.push({
|
|
@@ -1112,18 +1277,27 @@ function processBody(
|
|
|
1112
1277
|
graph.documentsByName[universeName][docName] = docModel;
|
|
1113
1278
|
}
|
|
1114
1279
|
}
|
|
1280
|
+
} else if (decl.kind === 'relationship') {
|
|
1281
|
+
// Relationship declarations are tracked in relationshipsByScope but don't create nodes
|
|
1282
|
+
// They're just declarations that can be referenced in relationships blocks
|
|
1115
1283
|
} else {
|
|
1116
1284
|
// UnknownBlock - attach to current node (the node whose body contains this block)
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1285
|
+
// Only process if it's actually an UnknownBlock (has keyword property, not kind)
|
|
1286
|
+
// Blocks with 'kind' property are known block types that should be handled above
|
|
1287
|
+
if (decl.keyword && !decl.kind) {
|
|
1288
|
+
const currentNode = graph.nodes[currentNodeId];
|
|
1289
|
+
if (!currentNode.unknownBlocks) {
|
|
1290
|
+
currentNode.unknownBlocks = [];
|
|
1291
|
+
}
|
|
1292
|
+
currentNode.unknownBlocks.push({
|
|
1293
|
+
keyword: decl.keyword,
|
|
1294
|
+
raw: decl.raw,
|
|
1295
|
+
normalized: normalizeProseBlock(decl.raw),
|
|
1296
|
+
source: decl.source,
|
|
1297
|
+
});
|
|
1120
1298
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
raw: decl.raw,
|
|
1124
|
-
normalized: normalizeProseBlock(decl.raw),
|
|
1125
|
-
source: decl.source,
|
|
1126
|
-
});
|
|
1299
|
+
// If it has a 'kind' property, it's a known block type that should have been handled above
|
|
1300
|
+
// Skip it silently (it may have been handled elsewhere, e.g., in createNode)
|
|
1127
1301
|
}
|
|
1128
1302
|
}
|
|
1129
1303
|
|
|
@@ -1170,6 +1344,8 @@ function createNode(nodeId, kind, name, parentNodeId, decl, containerNodeId) {
|
|
|
1170
1344
|
parent: parentNodeId,
|
|
1171
1345
|
children: [],
|
|
1172
1346
|
source: decl.source,
|
|
1347
|
+
spelledKind: decl.spelledKind,
|
|
1348
|
+
aliases: decl.aliases && Object.keys(decl.aliases).length > 0 ? decl.aliases : undefined,
|
|
1173
1349
|
};
|
|
1174
1350
|
|
|
1175
1351
|
// Always set container for book/chapter nodes (may be undefined if not resolved)
|
|
@@ -1200,6 +1376,34 @@ function createNode(nodeId, kind, name, parentNodeId, decl, containerNodeId) {
|
|
|
1200
1376
|
}
|
|
1201
1377
|
}
|
|
1202
1378
|
|
|
1379
|
+
// Extract relationships block if present
|
|
1380
|
+
const relationshipsBlock = decl.body?.find((b) => b.kind === 'relationships');
|
|
1381
|
+
if (relationshipsBlock) {
|
|
1382
|
+
if (relationshipsBlock.entries) {
|
|
1383
|
+
// New syntax: relationship ID + targets
|
|
1384
|
+
node.relationships = {
|
|
1385
|
+
entries: relationshipsBlock.entries.map((entry) => ({
|
|
1386
|
+
relationshipId: entry.relationshipId,
|
|
1387
|
+
targets: entry.targets.map((target) => ({
|
|
1388
|
+
id: target.id,
|
|
1389
|
+
metadata: target.metadata ? {
|
|
1390
|
+
raw: target.metadata.raw,
|
|
1391
|
+
normalized: normalizeProseBlock(target.metadata.raw),
|
|
1392
|
+
source: target.metadata.source,
|
|
1393
|
+
} : undefined,
|
|
1394
|
+
})),
|
|
1395
|
+
})),
|
|
1396
|
+
source: relationshipsBlock.source,
|
|
1397
|
+
};
|
|
1398
|
+
} else if (relationshipsBlock.values) {
|
|
1399
|
+
// String literals syntax (for relates blocks)
|
|
1400
|
+
node.relationships = {
|
|
1401
|
+
values: relationshipsBlock.values,
|
|
1402
|
+
source: relationshipsBlock.source,
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1203
1407
|
// Note: UnknownBlocks are handled in processBody and attached to the parent node
|
|
1204
1408
|
// They are not extracted here to avoid duplication
|
|
1205
1409
|
|
|
@@ -1277,7 +1481,7 @@ function resolveEdges(graph, scopeIndex) {
|
|
|
1277
1481
|
}
|
|
1278
1482
|
}
|
|
1279
1483
|
|
|
1280
|
-
// Resolve edges (
|
|
1484
|
+
// Resolve edges (relates edges in object format)
|
|
1281
1485
|
for (const edgeId in graph.edges) {
|
|
1282
1486
|
const edge = graph.edges[edgeId];
|
|
1283
1487
|
const universeName = edgeId.split(':')[0];
|
|
@@ -1433,6 +1637,161 @@ function resolveContainers(graph, scopeIndex, pending) {
|
|
|
1433
1637
|
}
|
|
1434
1638
|
}
|
|
1435
1639
|
|
|
1640
|
+
/**
|
|
1641
|
+
* Extracts asserted edges from both relationships {} blocks and relates nodes
|
|
1642
|
+
* @param {UniverseGraph} graph - The graph
|
|
1643
|
+
* @param {Map<string, Map<string, string[]>>} scopeIndex - Scope index
|
|
1644
|
+
* @returns {import('./ir.js').EdgeAssertedModel[]}
|
|
1645
|
+
*/
|
|
1646
|
+
function extractAssertedEdges(graph, scopeIndex) {
|
|
1647
|
+
/** @type {import('./ir.js').EdgeAssertedModel[]} */
|
|
1648
|
+
const assertedEdges = [];
|
|
1649
|
+
|
|
1650
|
+
// Extract from relationships {} blocks (new-style adjacency lists)
|
|
1651
|
+
for (const nodeId in graph.nodes) {
|
|
1652
|
+
const node = graph.nodes[nodeId];
|
|
1653
|
+
if (!node.relationships || !node.relationships.entries) continue;
|
|
1654
|
+
|
|
1655
|
+
// Resolve each target and create asserted edge
|
|
1656
|
+
for (const entry of node.relationships.entries) {
|
|
1657
|
+
for (const target of entry.targets) {
|
|
1658
|
+
// Resolve target ID using scope resolution
|
|
1659
|
+
const resolved = resolveNameInScope(
|
|
1660
|
+
graph,
|
|
1661
|
+
scopeIndex,
|
|
1662
|
+
target.id,
|
|
1663
|
+
nodeId,
|
|
1664
|
+
node.relationships.source,
|
|
1665
|
+
);
|
|
1666
|
+
const targetNodeId = resolved.nodeId && !resolved.ambiguous ? resolved.nodeId : null;
|
|
1667
|
+
if (!targetNodeId) continue;
|
|
1668
|
+
|
|
1669
|
+
assertedEdges.push({
|
|
1670
|
+
from: nodeId,
|
|
1671
|
+
via: entry.relationshipId,
|
|
1672
|
+
to: targetNodeId,
|
|
1673
|
+
meta: target.metadata,
|
|
1674
|
+
source: node.relationships.source,
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// Extract from relates nodes (bidirectional relationships)
|
|
1681
|
+
for (const nodeId in graph.nodes) {
|
|
1682
|
+
const node = graph.nodes[nodeId];
|
|
1683
|
+
if (node.kind !== 'relates' || !node.endpoints || node.endpoints.length !== 2) continue;
|
|
1684
|
+
|
|
1685
|
+
const [endpointA, endpointB] = node.endpoints;
|
|
1686
|
+
|
|
1687
|
+
// Relates are bidirectional - extract both directions as asserted edges
|
|
1688
|
+
// Use the relationship label from the relates node if available
|
|
1689
|
+
const viaLabel = node.relationships?.values?.[0] || 'related to';
|
|
1690
|
+
|
|
1691
|
+
// Create edge A -> B
|
|
1692
|
+
assertedEdges.push({
|
|
1693
|
+
from: endpointA,
|
|
1694
|
+
via: viaLabel, // Note: relates don't use relationship declarations, they use string labels
|
|
1695
|
+
to: endpointB,
|
|
1696
|
+
meta: node.from?.[endpointA]?.describe,
|
|
1697
|
+
source: node.source,
|
|
1698
|
+
});
|
|
1699
|
+
|
|
1700
|
+
// Create edge B -> A (relates are bidirectional)
|
|
1701
|
+
assertedEdges.push({
|
|
1702
|
+
from: endpointB,
|
|
1703
|
+
via: viaLabel,
|
|
1704
|
+
to: endpointA,
|
|
1705
|
+
meta: node.from?.[endpointB]?.describe,
|
|
1706
|
+
source: node.source,
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
return assertedEdges;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
/**
|
|
1714
|
+
* Normalizes edges by adding inverse edges for paired/symmetric relationships
|
|
1715
|
+
* @param {import('./ir.js').EdgeAssertedModel[]} assertedEdges - Asserted edges
|
|
1716
|
+
* @param {Map<string, Map<string, import('./ast.js').RelationshipDecl>>} relationshipDecls - Relationship declarations by universe
|
|
1717
|
+
* @param {string} universeName - Universe name
|
|
1718
|
+
* @returns {import('./ir.js').NormalizedEdgeModel[]}
|
|
1719
|
+
*/
|
|
1720
|
+
function normalizeEdges(assertedEdges, relationshipDecls, universeName) {
|
|
1721
|
+
/** @type {import('./ir.js').NormalizedEdgeModel[]} */
|
|
1722
|
+
const normalizedEdges = [];
|
|
1723
|
+
const relDeclsMap = relationshipDecls.get(universeName);
|
|
1724
|
+
const relDecls = relDeclsMap ? Object.fromEntries(relDeclsMap) : {};
|
|
1725
|
+
const seenEdges = new Set(); // Track edges to avoid duplicates
|
|
1726
|
+
|
|
1727
|
+
for (const asserted of assertedEdges) {
|
|
1728
|
+
const edgeKey = `${asserted.from}:${asserted.via}:${asserted.to}`;
|
|
1729
|
+
|
|
1730
|
+
// Skip if we've already added this exact edge (can happen with relates bidirectional edges)
|
|
1731
|
+
if (seenEdges.has(edgeKey)) continue;
|
|
1732
|
+
seenEdges.add(edgeKey);
|
|
1733
|
+
|
|
1734
|
+
// Get relationship declaration if this is a declared relationship
|
|
1735
|
+
// Note: relates edges use string labels, not relationship IDs, so they won't have a relDecl
|
|
1736
|
+
const relDecl = relDecls[asserted.via];
|
|
1737
|
+
|
|
1738
|
+
// Add asserted edge
|
|
1739
|
+
normalizedEdges.push({
|
|
1740
|
+
from: asserted.from,
|
|
1741
|
+
via: asserted.via,
|
|
1742
|
+
to: asserted.to,
|
|
1743
|
+
asserted: true,
|
|
1744
|
+
sourceRefs: [asserted.source],
|
|
1745
|
+
meta: asserted.meta,
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
// Add inverse edge if applicable (only for declared relationships, not relates)
|
|
1749
|
+
// Relates are already bidirectional, so both directions are in assertedEdges
|
|
1750
|
+
if (relDecl) {
|
|
1751
|
+
if (relDecl.type === 'paired') {
|
|
1752
|
+
// Determine inverse side
|
|
1753
|
+
const inverseVia =
|
|
1754
|
+
asserted.via === relDecl.leftId ? relDecl.rightId : relDecl.leftId;
|
|
1755
|
+
|
|
1756
|
+
if (inverseVia) {
|
|
1757
|
+
const inverseKey = `${asserted.to}:${inverseVia}:${asserted.from}`;
|
|
1758
|
+
// Only add inverse if not already present as an asserted edge
|
|
1759
|
+
if (!seenEdges.has(inverseKey)) {
|
|
1760
|
+
normalizedEdges.push({
|
|
1761
|
+
from: asserted.to,
|
|
1762
|
+
via: inverseVia,
|
|
1763
|
+
to: asserted.from,
|
|
1764
|
+
asserted: false,
|
|
1765
|
+
sourceRefs: [asserted.source], // Points back to asserted edge
|
|
1766
|
+
meta: asserted.meta, // Copy metadata
|
|
1767
|
+
});
|
|
1768
|
+
seenEdges.add(inverseKey);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
} else if (relDecl.type === 'symmetric') {
|
|
1772
|
+
// Symmetric: add reverse edge with same relationship ID
|
|
1773
|
+
const reverseKey = `${asserted.to}:${asserted.via}:${asserted.from}`;
|
|
1774
|
+
// Only add reverse if not already present as an asserted edge
|
|
1775
|
+
if (!seenEdges.has(reverseKey)) {
|
|
1776
|
+
normalizedEdges.push({
|
|
1777
|
+
from: asserted.to,
|
|
1778
|
+
via: asserted.via,
|
|
1779
|
+
to: asserted.from,
|
|
1780
|
+
asserted: false,
|
|
1781
|
+
sourceRefs: [asserted.source],
|
|
1782
|
+
meta: asserted.meta,
|
|
1783
|
+
});
|
|
1784
|
+
seenEdges.add(reverseKey);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
// Note: If no relDecl found, it's likely a relates edge (string label, not declared relationship)
|
|
1789
|
+
// Relates edges are already bidirectional in assertedEdges, so no inverse needed
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
return normalizedEdges;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1436
1795
|
/**
|
|
1437
1796
|
* @param {string | undefined} raw
|
|
1438
1797
|
* @returns {string | undefined}
|