@sprig-and-prose/sprig-universe 0.3.3 → 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/src/scanner.js CHANGED
@@ -25,6 +25,7 @@ const KEYWORDS = new Set([
25
25
  'concept',
26
26
  'in',
27
27
  'relates',
28
+ 'relationship',
28
29
  'and',
29
30
  'describe',
30
31
  'from',
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @fileoverview Tests for scoped aliases
3
+ */
4
+
5
+ import { test } from 'node:test';
6
+ import { readFileSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { fileURLToPath } from 'url';
9
+ import { dirname } from 'path';
10
+ import { parseFiles } from '../src/index.js';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = dirname(__filename);
14
+ const fixturesDir = join(__dirname, 'fixtures');
15
+
16
+ function loadGraph(fixtureName) {
17
+ const file = join(fixturesDir, fixtureName);
18
+ const text = readFileSync(file, 'utf-8');
19
+ return parseFiles([{ file, text }]);
20
+ }
21
+
22
+ test('basic alias in anthology resolves kind and preserves spelling', () => {
23
+ const graph = loadGraph('aliases-basic.prose');
24
+ const node = graph.nodes['Test:book:FrontendTeam'];
25
+ assert(node !== undefined, 'FrontendTeam node should exist');
26
+ assert(node.kind === 'book', 'FrontendTeam kind should resolve to book');
27
+ assert(node.spelledKind === 'team', 'FrontendTeam spelledKind should preserve alias');
28
+ });
29
+
30
+ test('aliases do not leak to sibling containers', () => {
31
+ const graph = loadGraph('aliases-no-leak.prose');
32
+ const t1 = graph.nodes['Test:book:T1'];
33
+ assert(t1 !== undefined, 'T1 should resolve via alias in anthology A');
34
+ const t2 = graph.nodes['Test:book:T2'];
35
+ assert(t2 === undefined, 'T2 should not resolve via alias from anthology A');
36
+ });
37
+
38
+ test('alias shadowing is scoped and does not affect siblings', () => {
39
+ const graph = loadGraph('aliases-shadowing.prose');
40
+ const outer = graph.nodes['Test:book:Outer'];
41
+ const inner = graph.nodes['Test:series:Inner'];
42
+ const anotherOuter = graph.nodes['Test:book:AnotherOuter'];
43
+
44
+ assert(outer !== undefined, 'Outer should resolve to book');
45
+ assert(outer.kind === 'book', 'Outer kind should be book');
46
+ assert(outer.spelledKind === 'team', 'Outer spelledKind should preserve alias');
47
+
48
+ assert(inner !== undefined, 'Inner should resolve to series in nested scope');
49
+ assert(inner.kind === 'series', 'Inner kind should be series');
50
+ assert(inner.spelledKind === 'team', 'Inner spelledKind should preserve alias');
51
+
52
+ assert(anotherOuter !== undefined, 'AnotherOuter should resolve to book in outer scope');
53
+ assert(anotherOuter.kind === 'book', 'AnotherOuter kind should be book');
54
+ assert(anotherOuter.spelledKind === 'team', 'AnotherOuter spelledKind should preserve alias');
55
+ });
56
+
57
+ test('single-line alias syntax matches block form', () => {
58
+ const graph = loadGraph('aliases-single-line.prose');
59
+ const node = graph.nodes['Test:book:X'];
60
+ assert(node !== undefined, 'X should resolve via single-line alias');
61
+ assert(node.kind === 'book', 'X kind should resolve to book');
62
+ assert(node.spelledKind === 'team', 'X spelledKind should preserve alias');
63
+ });
64
+
65
+ test('alias conflicts with built-in kind keyword errors clearly', () => {
66
+ const file = join(fixturesDir, 'aliases-conflict.prose');
67
+ const text = readFileSync(file, 'utf-8');
68
+ let error = null;
69
+ try {
70
+ parseFiles([{ file, text }]);
71
+ } catch (err) {
72
+ error = err;
73
+ }
74
+ assert(error, 'Expected alias conflict to throw');
75
+ assert(
76
+ String(error.message).includes('conflicts with built-in kind'),
77
+ 'Error message should explain the conflict',
78
+ );
79
+ });
80
+
81
+ /**
82
+ * Simple assertion helper
83
+ * @param {boolean} condition
84
+ * @param {string} message
85
+ */
86
+ function assert(condition, message) {
87
+ if (!condition) {
88
+ throw new Error(message);
89
+ }
90
+ }
91
+
@@ -0,0 +1,7 @@
1
+ universe Test {
2
+ anthology Organization {
3
+ aliases { team { book } }
4
+ team FrontendTeam { }
5
+ }
6
+ }
7
+
@@ -0,0 +1,6 @@
1
+ universe Test {
2
+ anthology Org {
3
+ aliases { book { concept } }
4
+ }
5
+ }
6
+
@@ -0,0 +1,5 @@
1
+ universe Test {
2
+ anthology A { aliases { team { book } } team T1 { } }
3
+ anthology B { team T2 { } }
4
+ }
5
+
@@ -0,0 +1,11 @@
1
+ universe Test {
2
+ anthology Org {
3
+ aliases { team { book } }
4
+ team Outer {
5
+ aliases { team { series } }
6
+ team Inner { }
7
+ }
8
+ team AnotherOuter { }
9
+ }
10
+ }
11
+
@@ -0,0 +1,7 @@
1
+ universe Test {
2
+ anthology Org {
3
+ alias team { book }
4
+ team X { }
5
+ }
6
+ }
7
+
@@ -0,0 +1,20 @@
1
+ universe TestErrors {
2
+ relationship duplicateId {
3
+ describe { First }
4
+ }
5
+
6
+ relationship duplicateId {
7
+ describe { Second }
8
+ }
9
+
10
+ series Teams {
11
+ team FrontendTeam {
12
+ relationships {
13
+ undeclaredRel { BackendTeam }
14
+ }
15
+ }
16
+
17
+ team BackendTeam { }
18
+ }
19
+ }
20
+
@@ -0,0 +1,28 @@
1
+ universe TestPaired {
2
+ relationship owns and ownedBy {
3
+ describe {
4
+ Ownership expresses long-term responsibility and stewardship.
5
+ }
6
+
7
+ from ownedBy {
8
+ label { 'owned by' }
9
+ describe {
10
+ Services are usually owned by a single team.
11
+ }
12
+ }
13
+ }
14
+
15
+ series TeamsPaired {
16
+ team FrontendTeam {
17
+ relationships {
18
+ owns { PaymentsApi WebApp }
19
+ }
20
+ }
21
+ }
22
+
23
+ series ServicesPaired {
24
+ service PaymentsApi { }
25
+ service WebApp { }
26
+ }
27
+ }
28
+
@@ -0,0 +1,23 @@
1
+ universe TestScoped {
2
+ relationship outerRel {
3
+ describe { Outer relationship }
4
+ }
5
+
6
+ anthology Inner {
7
+ relationship innerRel {
8
+ describe { Inner relationship }
9
+ }
10
+
11
+ series TeamsScoped {
12
+ team FrontendTeam {
13
+ relationships {
14
+ outerRel { BackendTeam }
15
+ innerRel { BackendTeam }
16
+ }
17
+ }
18
+
19
+ team BackendTeam { }
20
+ }
21
+ }
22
+ }
23
+
@@ -0,0 +1,18 @@
1
+ universe TestSymmetric {
2
+ relationship friend {
3
+ describe {
4
+ Teams have a peer relationship with no hierarchy.
5
+ }
6
+ }
7
+
8
+ series Teams {
9
+ team FrontendTeam {
10
+ relationships {
11
+ friend { BackendTeam }
12
+ }
13
+ }
14
+
15
+ team BackendTeam { }
16
+ }
17
+ }
18
+
@@ -0,0 +1,31 @@
1
+ universe TestUsage {
2
+ relationship ledBy and leads {
3
+ describe {
4
+ Teams are led by a single point of contact.
5
+ }
6
+ }
7
+
8
+ relationship hasMember and memberOf {
9
+ from memberOf {
10
+ label { 'is member of' }
11
+ }
12
+ }
13
+
14
+ series TeamsUsage {
15
+ team FrontendTeam {
16
+ relationships {
17
+ ledBy { Bryan }
18
+ hasMember {
19
+ Bryan
20
+ Michael { describe { Primary maintainer } }
21
+ }
22
+ }
23
+ }
24
+ }
25
+
26
+ series PeopleUsage {
27
+ person Bryan { }
28
+ person Michael { }
29
+ }
30
+ }
31
+