@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.
- package/PHILOSOPHY.md +201 -0
- package/README.md +168 -0
- package/REFERENCE.md +355 -0
- package/biome.json +24 -0
- package/package.json +30 -0
- package/repositories/sprig-repository-github/index.js +29 -0
- package/src/ast.js +257 -0
- package/src/cli.js +1510 -0
- package/src/graph.js +950 -0
- package/src/index.js +46 -0
- package/src/ir.js +121 -0
- package/src/parser.js +1656 -0
- package/src/scanner.js +255 -0
- package/src/scene-manifest.js +856 -0
- package/src/util/span.js +46 -0
- package/src/util/text.js +126 -0
- package/src/validator.js +862 -0
- package/src/validators/mysql/connection.js +154 -0
- package/src/validators/mysql/schema.js +209 -0
- package/src/validators/mysql/type-compat.js +219 -0
- package/src/validators/mysql/validator.js +332 -0
- package/test/fixtures/amaranthine-mini.prose +53 -0
- package/test/fixtures/conflicting-universes-a.prose +8 -0
- package/test/fixtures/conflicting-universes-b.prose +8 -0
- package/test/fixtures/duplicate-names.prose +20 -0
- package/test/fixtures/first-line-aware.prose +32 -0
- package/test/fixtures/indented-describe.prose +18 -0
- package/test/fixtures/multi-file-universe-a.prose +15 -0
- package/test/fixtures/multi-file-universe-b.prose +15 -0
- package/test/fixtures/multi-file-universe-conflict-desc.prose +12 -0
- package/test/fixtures/multi-file-universe-conflict-title.prose +4 -0
- package/test/fixtures/multi-file-universe-with-title.prose +10 -0
- package/test/fixtures/named-document.prose +17 -0
- package/test/fixtures/named-duplicate.prose +22 -0
- package/test/fixtures/named-reference.prose +17 -0
- package/test/fixtures/relates-errors.prose +38 -0
- package/test/fixtures/relates-tier1.prose +14 -0
- package/test/fixtures/relates-tier2.prose +16 -0
- package/test/fixtures/relates-tier3.prose +21 -0
- package/test/fixtures/sprig-meta-mini.prose +62 -0
- package/test/fixtures/unresolved-relates.prose +15 -0
- package/test/fixtures/using-in-references.prose +35 -0
- package/test/fixtures/using-unknown.prose +8 -0
- package/test/universe-basic.test.js +804 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview MySQL actor source validator
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
validateConnectionConfig,
|
|
7
|
+
getCachedConnection,
|
|
8
|
+
testConnection,
|
|
9
|
+
closeAllConnections,
|
|
10
|
+
} from './connection.js';
|
|
11
|
+
import { checkTableExists, getColumnMetadata } from './schema.js';
|
|
12
|
+
import { checkTypeCompatibility } from './type-compat.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validates a MySQL source for an actor
|
|
16
|
+
* @param {Object} actor - Actor from manifest
|
|
17
|
+
* @param {Object} mysqlSource - MySQL source block from manifest
|
|
18
|
+
* @param {Object} connections - Connections config from sprig.config.json
|
|
19
|
+
* @returns {Promise<Array>} Array of validation errors (empty if valid)
|
|
20
|
+
*/
|
|
21
|
+
export async function validateMysqlSource(actor, mysqlSource, connections) {
|
|
22
|
+
const errors = [];
|
|
23
|
+
const connectionName = mysqlSource.connection;
|
|
24
|
+
const tableName = mysqlSource.table;
|
|
25
|
+
const sourceLocation = mysqlSource.location || actor.location;
|
|
26
|
+
|
|
27
|
+
// Step 1: Validate connection config
|
|
28
|
+
const configResult = validateConnectionConfig(connectionName, connections);
|
|
29
|
+
if (!configResult.valid) {
|
|
30
|
+
errors.push({
|
|
31
|
+
actorName: actor.name,
|
|
32
|
+
sourceKind: 'mysql',
|
|
33
|
+
path: connectionName,
|
|
34
|
+
message: configResult.error,
|
|
35
|
+
location: sourceLocation,
|
|
36
|
+
fieldName: '<root>',
|
|
37
|
+
errorKind: 'mysql.connection',
|
|
38
|
+
expected: `valid MySQL connection '${connectionName}'`,
|
|
39
|
+
actual: configResult.error,
|
|
40
|
+
schemaLocation: sourceLocation || actor.location,
|
|
41
|
+
occurrence: {
|
|
42
|
+
dataFile: connectionName,
|
|
43
|
+
recordIndex: null,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
return errors; // Can't continue without valid config
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const config = configResult.config;
|
|
50
|
+
|
|
51
|
+
// Step 2: Test database connection and get actual database name
|
|
52
|
+
let connection;
|
|
53
|
+
let actualSchema;
|
|
54
|
+
try {
|
|
55
|
+
connection = await getCachedConnection(connectionName, config);
|
|
56
|
+
const testResult = await testConnection(connection, config);
|
|
57
|
+
if (!testResult.success) {
|
|
58
|
+
const connectionInfo = `${config.host}:${config.port}/${config.database} (user: ${config.user})`;
|
|
59
|
+
errors.push({
|
|
60
|
+
actorName: actor.name,
|
|
61
|
+
sourceKind: 'mysql',
|
|
62
|
+
path: connectionName,
|
|
63
|
+
message: `Failed to connect to MySQL: ${testResult.error}`,
|
|
64
|
+
location: sourceLocation,
|
|
65
|
+
fieldName: '<root>',
|
|
66
|
+
errorKind: 'mysql.connection',
|
|
67
|
+
expected: `successful connection to ${connectionInfo}`,
|
|
68
|
+
actual: testResult.error,
|
|
69
|
+
schemaLocation: sourceLocation || actor.location,
|
|
70
|
+
occurrence: {
|
|
71
|
+
dataFile: connectionName,
|
|
72
|
+
recordIndex: null,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
return errors; // Can't continue without connection
|
|
76
|
+
}
|
|
77
|
+
// Get the actual database name from the connection (handles case sensitivity)
|
|
78
|
+
actualSchema = testResult.databaseName || config.database;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const connectionInfo = `${config.host}:${config.port}/${config.database} (user: ${config.user})`;
|
|
81
|
+
errors.push({
|
|
82
|
+
actorName: actor.name,
|
|
83
|
+
sourceKind: 'mysql',
|
|
84
|
+
path: connectionName,
|
|
85
|
+
message: `Failed to connect to MySQL: ${error.message}`,
|
|
86
|
+
location: sourceLocation,
|
|
87
|
+
fieldName: '<root>',
|
|
88
|
+
errorKind: 'mysql.connection',
|
|
89
|
+
expected: `successful connection to ${connectionInfo}`,
|
|
90
|
+
actual: error.message,
|
|
91
|
+
schemaLocation: sourceLocation || actor.location,
|
|
92
|
+
occurrence: {
|
|
93
|
+
dataFile: connectionName,
|
|
94
|
+
recordIndex: null,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
return errors;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Step 3: Check table exists
|
|
101
|
+
let tableExists;
|
|
102
|
+
try {
|
|
103
|
+
tableExists = await checkTableExists(connection, actualSchema, tableName);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
errors.push({
|
|
106
|
+
actorName: actor.name,
|
|
107
|
+
sourceKind: 'mysql',
|
|
108
|
+
path: tableName,
|
|
109
|
+
message: `Failed to check table existence: ${error.message}`,
|
|
110
|
+
location: sourceLocation,
|
|
111
|
+
fieldName: '<root>',
|
|
112
|
+
errorKind: 'mysql.tableExists',
|
|
113
|
+
expected: `table '${tableName}' exists in schema '${actualSchema}'`,
|
|
114
|
+
actual: `error: ${error.message}`,
|
|
115
|
+
schemaLocation: sourceLocation || actor.location,
|
|
116
|
+
occurrence: {
|
|
117
|
+
dataFile: `${actualSchema}.${tableName}`,
|
|
118
|
+
recordIndex: null,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
return errors;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!tableExists) {
|
|
125
|
+
errors.push({
|
|
126
|
+
actorName: actor.name,
|
|
127
|
+
sourceKind: 'mysql',
|
|
128
|
+
path: tableName,
|
|
129
|
+
message: `Table '${tableName}' does not exist in schema '${actualSchema}'`,
|
|
130
|
+
location: sourceLocation,
|
|
131
|
+
fieldName: '<root>',
|
|
132
|
+
errorKind: 'mysql.tableExists',
|
|
133
|
+
expected: `table '${tableName}' exists in schema '${actualSchema}'`,
|
|
134
|
+
actual: 'table not found',
|
|
135
|
+
schemaLocation: sourceLocation || actor.location,
|
|
136
|
+
occurrence: {
|
|
137
|
+
dataFile: `${actualSchema}.${tableName}`,
|
|
138
|
+
recordIndex: null,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
return errors; // Can't continue without table
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Step 4: Get column metadata
|
|
145
|
+
let columnMetadata;
|
|
146
|
+
try {
|
|
147
|
+
columnMetadata = await getColumnMetadata(connection, actualSchema, tableName, connectionName);
|
|
148
|
+
|
|
149
|
+
// If table exists but no columns found, this indicates a problem
|
|
150
|
+
if (columnMetadata.size === 0) {
|
|
151
|
+
errors.push({
|
|
152
|
+
actorName: actor.name,
|
|
153
|
+
sourceKind: 'mysql',
|
|
154
|
+
path: tableName,
|
|
155
|
+
message: `Table '${tableName}' exists but no columns found in schema '${actualSchema}'. This may indicate a permissions issue or schema mismatch.`,
|
|
156
|
+
location: sourceLocation,
|
|
157
|
+
fieldName: '<root>',
|
|
158
|
+
errorKind: 'mysql.columns',
|
|
159
|
+
expected: `column metadata for table '${tableName}' in schema '${actualSchema}'`,
|
|
160
|
+
actual: '0 columns found',
|
|
161
|
+
schemaLocation: sourceLocation || actor.location,
|
|
162
|
+
occurrence: {
|
|
163
|
+
dataFile: `${actualSchema}.${tableName}`,
|
|
164
|
+
recordIndex: null,
|
|
165
|
+
},
|
|
166
|
+
hint: 'Verify the table exists and you have SELECT permissions on information_schema.columns',
|
|
167
|
+
});
|
|
168
|
+
return errors;
|
|
169
|
+
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
errors.push({
|
|
172
|
+
actorName: actor.name,
|
|
173
|
+
sourceKind: 'mysql',
|
|
174
|
+
path: tableName,
|
|
175
|
+
message: `Failed to get column metadata: ${error.message}`,
|
|
176
|
+
location: sourceLocation,
|
|
177
|
+
fieldName: '<root>',
|
|
178
|
+
errorKind: 'mysql.columns',
|
|
179
|
+
expected: `column metadata for table '${tableName}'`,
|
|
180
|
+
actual: `error: ${error.message}`,
|
|
181
|
+
schemaLocation: sourceLocation || actor.location,
|
|
182
|
+
occurrence: {
|
|
183
|
+
dataFile: `${actualSchema}.${tableName}`,
|
|
184
|
+
recordIndex: null,
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
return errors;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Step 5: Validate each field
|
|
191
|
+
const typeFields = actor.type?.fields || [];
|
|
192
|
+
for (const field of typeFields) {
|
|
193
|
+
const fieldName = field.name;
|
|
194
|
+
const fieldType = field.type;
|
|
195
|
+
const fieldLocation = field.location || sourceLocation;
|
|
196
|
+
|
|
197
|
+
// Skip nested objects for v1 (emit warning)
|
|
198
|
+
if (fieldType && (fieldType.kind === 'object' || fieldType.kind === 'reference')) {
|
|
199
|
+
errors.push({
|
|
200
|
+
actorName: actor.name,
|
|
201
|
+
sourceKind: 'mysql',
|
|
202
|
+
path: tableName,
|
|
203
|
+
message: `Field '${fieldName}' is a nested object (not validated for mysql yet)`,
|
|
204
|
+
location: fieldLocation,
|
|
205
|
+
fieldName: fieldName,
|
|
206
|
+
errorKind: 'mysql.columns',
|
|
207
|
+
expected: 'nested object validation (not implemented in v1)',
|
|
208
|
+
actual: 'skipped',
|
|
209
|
+
schemaLocation: fieldLocation || actor.location,
|
|
210
|
+
occurrence: {
|
|
211
|
+
dataFile: `${actualSchema}.${tableName}`,
|
|
212
|
+
recordIndex: null,
|
|
213
|
+
},
|
|
214
|
+
hint: 'Nested object validation for MySQL is not yet implemented',
|
|
215
|
+
});
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Check column exists (case-insensitive lookup)
|
|
220
|
+
let columnMeta = columnMetadata.get(fieldName);
|
|
221
|
+
|
|
222
|
+
// If not found, try case-insensitive lookup
|
|
223
|
+
if (!columnMeta && columnMetadata._lowerToOriginal) {
|
|
224
|
+
const lowerName = fieldName.toLowerCase();
|
|
225
|
+
const originalName = columnMetadata._lowerToOriginal.get(lowerName);
|
|
226
|
+
if (originalName) {
|
|
227
|
+
columnMeta = columnMetadata.get(originalName);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!columnMeta) {
|
|
232
|
+
// Get list of available columns for better error message
|
|
233
|
+
const availableColumns = Array.from(columnMetadata.keys())
|
|
234
|
+
.filter(k => typeof k === 'string' && !k.startsWith('__lower__'))
|
|
235
|
+
.join(', ');
|
|
236
|
+
|
|
237
|
+
errors.push({
|
|
238
|
+
actorName: actor.name,
|
|
239
|
+
sourceKind: 'mysql',
|
|
240
|
+
path: tableName,
|
|
241
|
+
message: `Missing column '${fieldName}' in table '${tableName}'. Available columns: ${availableColumns || '(none)'}`,
|
|
242
|
+
location: fieldLocation,
|
|
243
|
+
fieldName: fieldName,
|
|
244
|
+
errorKind: 'mysql.missingColumn',
|
|
245
|
+
expected: `column '${fieldName}' exists`,
|
|
246
|
+
actual: `column not found. Available: ${availableColumns || '(none)'}`,
|
|
247
|
+
schemaLocation: fieldLocation || actor.location,
|
|
248
|
+
occurrence: {
|
|
249
|
+
dataFile: `${actualSchema}.${tableName}`,
|
|
250
|
+
recordIndex: null,
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Check nullability compatibility (informational warning)
|
|
257
|
+
const isNullable = columnMeta.is_nullable === 'YES';
|
|
258
|
+
const isOptional = !field.required;
|
|
259
|
+
|
|
260
|
+
if (!isOptional && isNullable) {
|
|
261
|
+
// Required field but column is nullable - structural warning
|
|
262
|
+
// The database schema does not enforce the required constraint
|
|
263
|
+
errors.push({
|
|
264
|
+
actorName: actor.name,
|
|
265
|
+
sourceKind: 'mysql',
|
|
266
|
+
path: tableName,
|
|
267
|
+
message: `Database column '${fieldName}' is nullable but prose requires this field (DB does not enforce required field)`,
|
|
268
|
+
location: fieldLocation,
|
|
269
|
+
fieldName: fieldName,
|
|
270
|
+
errorKind: 'mysql.nullability',
|
|
271
|
+
severity: 'warning',
|
|
272
|
+
expected: 'required (NOT NULL)',
|
|
273
|
+
actual: 'nullable (NULL allowed)',
|
|
274
|
+
schemaLocation: fieldLocation || actor.location,
|
|
275
|
+
occurrence: {
|
|
276
|
+
dataFile: `${actualSchema}.${tableName}`,
|
|
277
|
+
recordIndex: null,
|
|
278
|
+
},
|
|
279
|
+
hint: 'The database allows NULLs for this field. If you rely on this field being present, consider enforcing NOT NULL after any backfill or migration.',
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Check type compatibility
|
|
284
|
+
const typeResult = checkTypeCompatibility(fieldType, columnMeta);
|
|
285
|
+
if (!typeResult.compatible) {
|
|
286
|
+
errors.push({
|
|
287
|
+
actorName: actor.name,
|
|
288
|
+
sourceKind: 'mysql',
|
|
289
|
+
path: tableName,
|
|
290
|
+
message: `Column '${fieldName}' type mismatch: ${typeResult.message}`,
|
|
291
|
+
location: fieldLocation,
|
|
292
|
+
fieldName: fieldName,
|
|
293
|
+
errorKind: 'mysql.typeMismatch',
|
|
294
|
+
expected: field.typeExpr || 'compatible type',
|
|
295
|
+
actual: columnMeta.column_type,
|
|
296
|
+
schemaLocation: fieldLocation || actor.location,
|
|
297
|
+
occurrence: {
|
|
298
|
+
dataFile: `${actualSchema}.${tableName}`,
|
|
299
|
+
recordIndex: null,
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
} else if (typeResult.severity === 'warning' && typeResult.message) {
|
|
303
|
+
// Add warning-level errors for type compatibility warnings
|
|
304
|
+
errors.push({
|
|
305
|
+
actorName: actor.name,
|
|
306
|
+
sourceKind: 'mysql',
|
|
307
|
+
path: tableName,
|
|
308
|
+
message: `Column '${fieldName}' type compatibility: ${typeResult.message}`,
|
|
309
|
+
location: fieldLocation,
|
|
310
|
+
fieldName: fieldName,
|
|
311
|
+
errorKind: 'mysql.typeMismatch',
|
|
312
|
+
expected: field.typeExpr || 'compatible type',
|
|
313
|
+
actual: columnMeta.column_type,
|
|
314
|
+
schemaLocation: fieldLocation || actor.location,
|
|
315
|
+
occurrence: {
|
|
316
|
+
dataFile: `${actualSchema}.${tableName}`,
|
|
317
|
+
recordIndex: null,
|
|
318
|
+
},
|
|
319
|
+
hint: typeResult.message,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return errors;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Cleanup function to close all connections
|
|
329
|
+
* Should be called after validation is complete
|
|
330
|
+
*/
|
|
331
|
+
export { closeAllConnections };
|
|
332
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
universe Amaranthine {
|
|
2
|
+
describe {
|
|
3
|
+
Amaranthine is a persistent text-based roleplaying game. At its core, it is
|
|
4
|
+
a semi-afk game where you can do any of the twelve in-game skills and
|
|
5
|
+
progress your character.
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
series Player {
|
|
9
|
+
describe {
|
|
10
|
+
The player is central to everything. From chat to skills to inventory,
|
|
11
|
+
nearly everything is based around the player.
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
references {
|
|
15
|
+
reference {
|
|
16
|
+
repository { 'amaranthine' }
|
|
17
|
+
paths { '/backends/api/src/routers/players.js' }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
series Item {
|
|
23
|
+
describe {
|
|
24
|
+
Items are physical objects in the game that players can interact with.
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
references {
|
|
28
|
+
reference {
|
|
29
|
+
repository { 'amaranthine' }
|
|
30
|
+
paths { '/data/items/*.yaml' }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
book Equipment in Item {
|
|
36
|
+
describe {
|
|
37
|
+
Equipment is a category of items that are used to equip a player.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
book Resource in Item {
|
|
42
|
+
describe {
|
|
43
|
+
Resources are items that are used to create other items.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
chapter Tool in Equipment {
|
|
48
|
+
describe {
|
|
49
|
+
Tools are categories of items that are used to perform a task.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
universe Test {
|
|
2
|
+
series Duplicate {
|
|
3
|
+
describe { First occurrence }
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
-- This is a duplicate name
|
|
7
|
+
series Duplicate {
|
|
8
|
+
describe { Second occurrence }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
book Book1 in Duplicate {
|
|
12
|
+
describe { Book 1 }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
-- Another duplicate (different kind, but same name)
|
|
16
|
+
book Book1 in Duplicate {
|
|
17
|
+
describe { Book 1 duplicate }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
universe Test {
|
|
2
|
+
describe {
|
|
3
|
+
This is a test universe
|
|
4
|
+
with multiple lines
|
|
5
|
+
and indented content
|
|
6
|
+
that should be dedented.
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
series Example {
|
|
10
|
+
describe {
|
|
11
|
+
This describe block
|
|
12
|
+
has leading indentation
|
|
13
|
+
that should be removed
|
|
14
|
+
but internal indentation preserved.
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
book SingleLine in Example {
|
|
19
|
+
describe {
|
|
20
|
+
Single line describe should remain unchanged.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
book AllIndented in Example {
|
|
25
|
+
describe {
|
|
26
|
+
All lines
|
|
27
|
+
are indented
|
|
28
|
+
consistently
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
universe Test {
|
|
2
|
+
describe {
|
|
3
|
+
This is a test universe
|
|
4
|
+
with multiple lines
|
|
5
|
+
and indented content
|
|
6
|
+
that should be dedented.
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
series Example {
|
|
10
|
+
describe {
|
|
11
|
+
This describe block
|
|
12
|
+
has leading indentation
|
|
13
|
+
that should be removed
|
|
14
|
+
but internal indentation preserved.
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
universe Test {
|
|
2
|
+
document ItemsDesignDoc {
|
|
3
|
+
kind { 'internal' }
|
|
4
|
+
path { '/docs/items/overview.md' }
|
|
5
|
+
describe { High-level items design notes. }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
series Items {
|
|
9
|
+
documentation {
|
|
10
|
+
document {
|
|
11
|
+
kind { 'internal' }
|
|
12
|
+
path { '/docs/items/details.md' }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
universe Test {
|
|
2
|
+
reference ItemRouter {
|
|
3
|
+
repository { 'amaranthine-backend' }
|
|
4
|
+
paths { '/src/routers/items.ts' }
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
reference ItemRouter {
|
|
8
|
+
repository { 'amaranthine-backend' }
|
|
9
|
+
paths { '/src/routers/items-v2.ts' }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
document ItemsDesignDoc {
|
|
13
|
+
kind { 'internal' }
|
|
14
|
+
path { '/docs/items/overview.md' }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
document ItemsDesignDoc {
|
|
18
|
+
kind { 'public' }
|
|
19
|
+
path { '/docs/items/public-overview.md' }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
universe Test {
|
|
2
|
+
reference ItemRouter {
|
|
3
|
+
repository { 'amaranthine-backend' }
|
|
4
|
+
paths { '/src/routers/items.ts' }
|
|
5
|
+
describe { Routes that implement item endpoints. }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
series Items {
|
|
9
|
+
references {
|
|
10
|
+
reference {
|
|
11
|
+
repository { 'amaranthine-backend' }
|
|
12
|
+
paths { '/src/items/helpers.ts' }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
universe Test {
|
|
2
|
+
series Item {
|
|
3
|
+
describe { Items are physical objects in the game. }
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
series Action {
|
|
7
|
+
describe { Actions are things players can do. }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
-- Unknown endpoint
|
|
11
|
+
relates Unknown and Item {
|
|
12
|
+
describe { This should produce a warning. }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
-- Invalid from endpoint
|
|
16
|
+
relates Item and Action {
|
|
17
|
+
from Tool {
|
|
18
|
+
relationships { 'invalid' }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
-- Duplicate describe
|
|
23
|
+
relates Item and Action {
|
|
24
|
+
describe { First describe. }
|
|
25
|
+
describe { Second describe. }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
-- Duplicate from
|
|
29
|
+
relates Item and Action {
|
|
30
|
+
from Item {
|
|
31
|
+
relationships { 'first' }
|
|
32
|
+
}
|
|
33
|
+
from Item {
|
|
34
|
+
relationships { 'second' }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
universe Test {
|
|
2
|
+
series Item {
|
|
3
|
+
describe { Items are physical objects in the game. }
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
series Action {
|
|
7
|
+
describe { Actions are things players can do. }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
relates Item and Action {
|
|
11
|
+
describe { Items can be consumed by actions and can also be produced by actions. }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
universe Test {
|
|
2
|
+
series Item {
|
|
3
|
+
describe { Items are physical objects in the game. }
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
series Action {
|
|
7
|
+
describe { Actions are things players can do. }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
relates Item and Action {
|
|
11
|
+
from Item { relationships { 'used in' } }
|
|
12
|
+
from Action { relationships { 'produces and consumes' } }
|
|
13
|
+
describe { Items can be consumed by actions and can also be produced by actions. }
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
universe Test {
|
|
2
|
+
series Item {
|
|
3
|
+
describe { Items are physical objects in the game. }
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
series Action {
|
|
7
|
+
describe { Actions are things players can do. }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
relates Item and Action {
|
|
11
|
+
from Item {
|
|
12
|
+
relationships { 'produced by', 'consumed by', }
|
|
13
|
+
describe { Items can be produced by and consumed by actions. }
|
|
14
|
+
}
|
|
15
|
+
from Action {
|
|
16
|
+
relationships { 'produces', 'consumes' }
|
|
17
|
+
describe { Actions can produce and consume items. }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|