@sprig-and-prose/sprig-universe 0.1.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.
Files changed (45) hide show
  1. package/PHILOSOPHY.md +201 -0
  2. package/README.md +168 -0
  3. package/REFERENCE.md +355 -0
  4. package/biome.json +24 -0
  5. package/package.json +30 -0
  6. package/repositories/sprig-repository-github/index.js +29 -0
  7. package/src/ast.js +257 -0
  8. package/src/cli.js +1510 -0
  9. package/src/graph.js +950 -0
  10. package/src/index.js +46 -0
  11. package/src/ir.js +121 -0
  12. package/src/parser.js +1656 -0
  13. package/src/scanner.js +255 -0
  14. package/src/scene-manifest.js +856 -0
  15. package/src/util/span.js +46 -0
  16. package/src/util/text.js +126 -0
  17. package/src/validator.js +862 -0
  18. package/src/validators/mysql/connection.js +154 -0
  19. package/src/validators/mysql/schema.js +209 -0
  20. package/src/validators/mysql/type-compat.js +219 -0
  21. package/src/validators/mysql/validator.js +332 -0
  22. package/test/fixtures/amaranthine-mini.prose +53 -0
  23. package/test/fixtures/conflicting-universes-a.prose +8 -0
  24. package/test/fixtures/conflicting-universes-b.prose +8 -0
  25. package/test/fixtures/duplicate-names.prose +20 -0
  26. package/test/fixtures/first-line-aware.prose +32 -0
  27. package/test/fixtures/indented-describe.prose +18 -0
  28. package/test/fixtures/multi-file-universe-a.prose +15 -0
  29. package/test/fixtures/multi-file-universe-b.prose +15 -0
  30. package/test/fixtures/multi-file-universe-conflict-desc.prose +12 -0
  31. package/test/fixtures/multi-file-universe-conflict-title.prose +4 -0
  32. package/test/fixtures/multi-file-universe-with-title.prose +10 -0
  33. package/test/fixtures/named-document.prose +17 -0
  34. package/test/fixtures/named-duplicate.prose +22 -0
  35. package/test/fixtures/named-reference.prose +17 -0
  36. package/test/fixtures/relates-errors.prose +38 -0
  37. package/test/fixtures/relates-tier1.prose +14 -0
  38. package/test/fixtures/relates-tier2.prose +16 -0
  39. package/test/fixtures/relates-tier3.prose +21 -0
  40. package/test/fixtures/sprig-meta-mini.prose +62 -0
  41. package/test/fixtures/unresolved-relates.prose +15 -0
  42. package/test/fixtures/using-in-references.prose +35 -0
  43. package/test/fixtures/using-unknown.prose +8 -0
  44. package/test/universe-basic.test.js +804 -0
  45. package/tsconfig.json +15 -0
package/src/scanner.js ADDED
@@ -0,0 +1,255 @@
1
+ /**
2
+ * @fileoverview Tokenizer/scanner for Sprig universe syntax
3
+ */
4
+
5
+ /**
6
+ * @typedef {Object} Token
7
+ * @property {string} type - Token type
8
+ * @property {string} value - Token value
9
+ * @property {SourceSpan} span - Source span
10
+ */
11
+
12
+ /**
13
+ * @typedef {Object} SourceSpan
14
+ * @property {string} file - File path
15
+ * @property {{ line: number, col: number, offset: number }} start - Start position
16
+ * @property {{ line: number, col: number, offset: number }} end - End position
17
+ */
18
+
19
+ const KEYWORDS = new Set([
20
+ 'universe',
21
+ 'anthology',
22
+ 'series',
23
+ 'book',
24
+ 'chapter',
25
+ 'concept',
26
+ 'in',
27
+ 'relates',
28
+ 'and',
29
+ 'describe',
30
+ 'from',
31
+ 'relationships',
32
+ 'scene',
33
+ 'using',
34
+ 'actor',
35
+ 'type',
36
+ 'identity',
37
+ 'file',
38
+ 'sqlite',
39
+ 'mysql',
40
+ 'transforms',
41
+ 'transform',
42
+ 'for',
43
+ 'list',
44
+ 'record',
45
+ 'repository',
46
+ ]);
47
+
48
+ /**
49
+ * Scans input text and returns tokens with source spans
50
+ * @param {string} text - Input text
51
+ * @param {string} file - File path
52
+ * @returns {Token[]}
53
+ */
54
+ export function scan(text, file) {
55
+ const tokens = [];
56
+ let offset = 0;
57
+ let line = 1;
58
+ let col = 1;
59
+
60
+ while (offset < text.length) {
61
+ const startOffset = offset;
62
+ const startLine = line;
63
+ const startCol = col;
64
+
65
+ const ch = text[offset];
66
+
67
+ // Skip whitespace (but track newlines for span calculations)
68
+ if (/\s/.test(ch)) {
69
+ if (ch === '\n') {
70
+ line++;
71
+ col = 1;
72
+ } else {
73
+ col++;
74
+ }
75
+ offset++;
76
+ continue;
77
+ }
78
+
79
+ // Comments: -- to end of line
80
+ if (ch === '-' && offset + 1 < text.length && text[offset + 1] === '-') {
81
+ while (offset < text.length && text[offset] !== '\n') {
82
+ offset++;
83
+ col++;
84
+ }
85
+ // Don't create a token for comments, just skip
86
+ continue;
87
+ }
88
+
89
+ // Single-quoted strings
90
+ // Only treat as string delimiter if it's clearly a string (not a contraction)
91
+ // A contraction is when a quote appears between two letters/digits
92
+ if (ch === "'") {
93
+ const prevCh = startOffset > 0 ? text[startOffset - 1] : null;
94
+ const nextCh = offset + 1 < text.length ? text[offset + 1] : null;
95
+
96
+ // If quote is between letters/digits, it's a contraction - don't parse as string
97
+ // Let it be handled by identifier parsing below
98
+ if (prevCh && /[A-Za-z0-9]/.test(prevCh) && nextCh && /[A-Za-z0-9]/.test(nextCh)) {
99
+ // This is a contraction, fall through to identifier parsing
100
+ } else {
101
+ // This looks like a string delimiter
102
+ offset++;
103
+ col++;
104
+ let value = '';
105
+ let escaped = false;
106
+
107
+ while (offset < text.length) {
108
+ const c = text[offset];
109
+ if (escaped) {
110
+ if (c === "'" || c === '\\') {
111
+ value += c;
112
+ } else {
113
+ value += '\\' + c;
114
+ }
115
+ escaped = false;
116
+ offset++;
117
+ col++;
118
+ } else if (c === '\\') {
119
+ escaped = true;
120
+ offset++;
121
+ col++;
122
+ } else if (c === "'") {
123
+ offset++;
124
+ col++;
125
+ break;
126
+ } else {
127
+ value += c;
128
+ offset++;
129
+ col++;
130
+ }
131
+ }
132
+
133
+ tokens.push({
134
+ type: 'STRING',
135
+ value,
136
+ span: {
137
+ file,
138
+ start: { line: startLine, col: startCol, offset: startOffset },
139
+ end: { line, col, offset },
140
+ },
141
+ });
142
+ continue;
143
+ }
144
+ }
145
+
146
+ // Braces
147
+ if (ch === '{') {
148
+ tokens.push({
149
+ type: 'LBRACE',
150
+ value: '{',
151
+ span: {
152
+ file,
153
+ start: { line: startLine, col: startCol, offset: startOffset },
154
+ end: { line, col: col + 1, offset: offset + 1 },
155
+ },
156
+ });
157
+ offset++;
158
+ col++;
159
+ continue;
160
+ }
161
+
162
+ if (ch === '}') {
163
+ tokens.push({
164
+ type: 'RBRACE',
165
+ value: '}',
166
+ span: {
167
+ file,
168
+ start: { line: startLine, col: startCol, offset: startOffset },
169
+ end: { line, col: col + 1, offset: offset + 1 },
170
+ },
171
+ });
172
+ offset++;
173
+ col++;
174
+ continue;
175
+ }
176
+
177
+ // Dot (for namespace paths)
178
+ if (ch === '.') {
179
+ tokens.push({
180
+ type: 'DOT',
181
+ value: '.',
182
+ span: {
183
+ file,
184
+ start: { line: startLine, col: startCol, offset: startOffset },
185
+ end: { line, col: col + 1, offset: offset + 1 },
186
+ },
187
+ });
188
+ offset++;
189
+ col++;
190
+ continue;
191
+ }
192
+
193
+ // Comma
194
+ if (ch === ',') {
195
+ tokens.push({
196
+ type: 'COMMA',
197
+ value: ',',
198
+ span: {
199
+ file,
200
+ start: { line: startLine, col: startCol, offset: startOffset },
201
+ end: { line, col: col + 1, offset: offset + 1 },
202
+ },
203
+ });
204
+ offset++;
205
+ col++;
206
+ continue;
207
+ }
208
+
209
+ // Identifiers and keywords (including contractions with apostrophes)
210
+ if (/[A-Za-z_]/.test(ch)) {
211
+ let value = '';
212
+ while (
213
+ offset < text.length &&
214
+ (/[A-Za-z0-9_]/.test(text[offset]) ||
215
+ (text[offset] === "'" && offset + 1 < text.length && /[A-Za-z0-9]/.test(text[offset + 1])))
216
+ ) {
217
+ value += text[offset];
218
+ offset++;
219
+ col++;
220
+ }
221
+
222
+ const type = KEYWORDS.has(value) ? 'KEYWORD' : 'IDENTIFIER';
223
+
224
+ tokens.push({
225
+ type,
226
+ value,
227
+ span: {
228
+ file,
229
+ start: { line: startLine, col: startCol, offset: startOffset },
230
+ end: { line, col, offset },
231
+ },
232
+ });
233
+ continue;
234
+ }
235
+
236
+ // Unknown character - emit as error token or skip?
237
+ // For now, skip and continue (tolerant parsing)
238
+ offset++;
239
+ col++;
240
+ }
241
+
242
+ // EOF token
243
+ tokens.push({
244
+ type: 'EOF',
245
+ value: '',
246
+ span: {
247
+ file,
248
+ start: { line, col, offset },
249
+ end: { line, col, offset },
250
+ },
251
+ });
252
+
253
+ return tokens;
254
+ }
255
+