@protoyard/blueprint 0.0.0-stage → 1.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 (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1149 -2
  3. package/dist/aggregate.d.ts +83 -0
  4. package/dist/aggregate.js +268 -0
  5. package/dist/app-model.d.ts +76 -0
  6. package/dist/app-model.js +287 -0
  7. package/dist/archetypes/index.d.ts +1 -0
  8. package/dist/archetypes/index.js +1 -0
  9. package/dist/archetypes/schema.d.ts +126 -0
  10. package/dist/archetypes/schema.js +77 -0
  11. package/dist/audit.d.ts +52 -0
  12. package/dist/audit.js +149 -0
  13. package/dist/authoring/deck-lint.d.ts +32 -0
  14. package/dist/authoring/deck-lint.js +567 -0
  15. package/dist/authoring/index.d.ts +1 -0
  16. package/dist/authoring/index.js +1 -0
  17. package/dist/authoring/select-deck-layouts.d.ts +50 -0
  18. package/dist/authoring/select-deck-layouts.js +213 -0
  19. package/dist/authoring-apis.d.ts +13 -0
  20. package/dist/authoring-apis.js +13 -0
  21. package/dist/authoring-validation.d.ts +65 -0
  22. package/dist/authoring-validation.js +267 -0
  23. package/dist/avatar.d.ts +25 -0
  24. package/dist/avatar.js +30 -0
  25. package/dist/brand.d.ts +70 -0
  26. package/dist/brand.js +294 -0
  27. package/dist/brief/archetypes.d.ts +28 -0
  28. package/dist/brief/archetypes.js +798 -0
  29. package/dist/brief/content-synthesize.d.ts +31 -0
  30. package/dist/brief/content-synthesize.js +210 -0
  31. package/dist/brief/deck-outlines.d.ts +96 -0
  32. package/dist/brief/deck-outlines.js +561 -0
  33. package/dist/brief/draft-bodies.d.ts +29 -0
  34. package/dist/brief/draft-bodies.js +224 -0
  35. package/dist/brief/fields.d.ts +19 -0
  36. package/dist/brief/fields.js +229 -0
  37. package/dist/brief/index.d.ts +8 -0
  38. package/dist/brief/index.js +8 -0
  39. package/dist/brief/kind-detection.d.ts +69 -0
  40. package/dist/brief/kind-detection.js +353 -0
  41. package/dist/brief/limits.d.ts +117 -0
  42. package/dist/brief/limits.js +217 -0
  43. package/dist/brief/page-doc-outlines.d.ts +80 -0
  44. package/dist/brief/page-doc-outlines.js +485 -0
  45. package/dist/brief/synthesize.d.ts +59 -0
  46. package/dist/brief/synthesize.js +511 -0
  47. package/dist/brief/text.d.ts +13 -0
  48. package/dist/brief/text.js +59 -0
  49. package/dist/canvas-screen.d.ts +31 -0
  50. package/dist/canvas-screen.js +33 -0
  51. package/dist/canvas-seed.d.ts +58 -0
  52. package/dist/canvas-seed.js +82 -0
  53. package/dist/computed/evaluate.d.ts +11 -0
  54. package/dist/computed/evaluate.js +101 -0
  55. package/dist/computed/index.d.ts +4 -0
  56. package/dist/computed/index.js +3 -0
  57. package/dist/computed/public.d.ts +14 -0
  58. package/dist/computed/public.js +19 -0
  59. package/dist/computed/schema.d.ts +10 -0
  60. package/dist/computed/schema.js +140 -0
  61. package/dist/computed/types.d.ts +70 -0
  62. package/dist/computed/types.js +1 -0
  63. package/dist/computed/validate.d.ts +6 -0
  64. package/dist/computed/validate.js +146 -0
  65. package/dist/csv-guard.d.ts +15 -0
  66. package/dist/csv-guard.js +29 -0
  67. package/dist/csv.d.ts +85 -0
  68. package/dist/csv.js +288 -0
  69. package/dist/files.d.ts +35 -0
  70. package/dist/files.js +246 -0
  71. package/dist/form-schema.d.ts +124 -0
  72. package/dist/form-schema.js +300 -0
  73. package/dist/generator-version.d.ts +11 -0
  74. package/dist/generator-version.js +51 -0
  75. package/dist/generator.d.ts +34 -0
  76. package/dist/generator.js +445 -0
  77. package/dist/identity.d.ts +34 -0
  78. package/dist/identity.js +91 -0
  79. package/dist/import/csv-shape.d.ts +40 -0
  80. package/dist/import/csv-shape.js +265 -0
  81. package/dist/import/json-shape.d.ts +55 -0
  82. package/dist/import/json-shape.js +491 -0
  83. package/dist/import/limits.d.ts +13 -0
  84. package/dist/import/limits.js +14 -0
  85. package/dist/import/sqlite-schema.d.ts +36 -0
  86. package/dist/import/sqlite-schema.js +729 -0
  87. package/dist/index.d.ts +60 -0
  88. package/dist/index.js +58 -0
  89. package/dist/kinds/anchors.d.ts +18 -0
  90. package/dist/kinds/anchors.js +46 -0
  91. package/dist/kinds/bodies.d.ts +6 -0
  92. package/dist/kinds/bodies.js +32 -0
  93. package/dist/kinds/brand-runtime.d.ts +19 -0
  94. package/dist/kinds/brand-runtime.js +35 -0
  95. package/dist/kinds/deck.d.ts +8 -0
  96. package/dist/kinds/deck.js +34 -0
  97. package/dist/kinds/doc.d.ts +19 -0
  98. package/dist/kinds/doc.js +41 -0
  99. package/dist/kinds/embed/index.d.ts +39 -0
  100. package/dist/kinds/embed/index.js +91 -0
  101. package/dist/kinds/envelope.d.ts +159 -0
  102. package/dist/kinds/envelope.js +78 -0
  103. package/dist/kinds/index.d.ts +11 -0
  104. package/dist/kinds/index.js +11 -0
  105. package/dist/kinds/kind.d.ts +36 -0
  106. package/dist/kinds/kind.js +55 -0
  107. package/dist/kinds/limits.d.ts +64 -0
  108. package/dist/kinds/limits.js +190 -0
  109. package/dist/kinds/page-doc-runtime.d.ts +37 -0
  110. package/dist/kinds/page-doc-runtime.js +55 -0
  111. package/dist/kinds/page.d.ts +16 -0
  112. package/dist/kinds/page.js +67 -0
  113. package/dist/kinds/parse.d.ts +18 -0
  114. package/dist/kinds/parse.js +27 -0
  115. package/dist/kinds/runtime.d.ts +128 -0
  116. package/dist/kinds/runtime.js +206 -0
  117. package/dist/kinds/services.d.ts +49 -0
  118. package/dist/kinds/services.js +249 -0
  119. package/dist/kinds/testing.d.ts +9 -0
  120. package/dist/kinds/testing.js +16 -0
  121. package/dist/migrations.d.ts +97 -0
  122. package/dist/migrations.js +351 -0
  123. package/dist/motion.d.ts +50 -0
  124. package/dist/motion.js +52 -0
  125. package/dist/notifications.d.ts +19 -0
  126. package/dist/notifications.js +177 -0
  127. package/dist/patch.d.ts +122 -0
  128. package/dist/patch.js +322 -0
  129. package/dist/policy.d.ts +23 -0
  130. package/dist/policy.js +105 -0
  131. package/dist/runtime-definition.d.ts +39 -0
  132. package/dist/runtime-definition.js +74 -0
  133. package/dist/scenarios.d.ts +9 -0
  134. package/dist/scenarios.js +49 -0
  135. package/dist/schema/blueprint.schema.json +10191 -0
  136. package/dist/schema-json.d.ts +6 -0
  137. package/dist/schema-json.js +36 -0
  138. package/dist/schema.d.ts +6348 -0
  139. package/dist/schema.js +1269 -0
  140. package/dist/search.d.ts +16 -0
  141. package/dist/search.js +96 -0
  142. package/dist/services.d.ts +89 -0
  143. package/dist/services.js +898 -0
  144. package/dist/share.d.ts +78 -0
  145. package/dist/share.js +234 -0
  146. package/dist/sheet-screen.d.ts +49 -0
  147. package/dist/sheet-screen.js +91 -0
  148. package/dist/storage.d.ts +43 -0
  149. package/dist/storage.js +179 -0
  150. package/dist/stream.d.ts +26 -0
  151. package/dist/stream.js +173 -0
  152. package/dist/untrusted.d.ts +63 -0
  153. package/dist/untrusted.js +502 -0
  154. package/dist/validation-tasks.d.ts +178 -0
  155. package/dist/validation-tasks.js +199 -0
  156. package/dist/validation.d.ts +35 -0
  157. package/dist/validation.js +312 -0
  158. package/dist/variants.d.ts +112 -0
  159. package/dist/variants.js +283 -0
  160. package/dist/workflows/index.d.ts +3 -0
  161. package/dist/workflows/index.js +3 -0
  162. package/dist/workflows/schema.d.ts +50 -0
  163. package/dist/workflows/schema.js +34 -0
  164. package/dist/workflows/transition.d.ts +15 -0
  165. package/dist/workflows/transition.js +25 -0
  166. package/dist/workflows/validate.d.ts +17 -0
  167. package/dist/workflows/validate.js +140 -0
  168. package/package.json +120 -3
@@ -0,0 +1,729 @@
1
+ import { parseBlueprint } from "../schema.js";
2
+ import { IMPORT_MAX_COLUMNS, IMPORT_MAX_DEPTH, IMPORT_SQLITE_MAX_BYTES, IMPORT_SQLITE_MAX_TABLES, IMPORT_SQLITE_MAX_TOKENS, } from "./limits.js";
3
+ export const SQLITE_SCHEMA_MAX_BYTES = IMPORT_SQLITE_MAX_BYTES;
4
+ export const SQLITE_SCHEMA_MAX_TABLES = IMPORT_SQLITE_MAX_TABLES;
5
+ export const SQLITE_SCHEMA_MAX_COLUMNS = IMPORT_MAX_COLUMNS;
6
+ export const SQLITE_SCHEMA_MAX_TOKENS = IMPORT_SQLITE_MAX_TOKENS;
7
+ export class SqliteSchemaError extends Error {
8
+ code;
9
+ path;
10
+ constructor(code, path, message) {
11
+ super(`${path}: ${message}`);
12
+ this.name = 'SqliteSchemaError';
13
+ this.code = code;
14
+ this.path = path;
15
+ }
16
+ }
17
+ const RESERVED_FIELDS = new Set(['id', 'createdAt', 'updatedAt', 'deletedAt', 'deletedBy']);
18
+ const CONSTRAINT_WORDS = new Set([
19
+ 'CONSTRAINT',
20
+ 'PRIMARY',
21
+ 'NOT',
22
+ 'NULL',
23
+ 'UNIQUE',
24
+ 'CHECK',
25
+ 'DEFAULT',
26
+ 'COLLATE',
27
+ 'REFERENCES',
28
+ 'GENERATED',
29
+ ]);
30
+ function fail(code, path, message) {
31
+ throw new SqliteSchemaError(code, path, message);
32
+ }
33
+ function bounded(value, fallback, maximum) {
34
+ if (value === undefined)
35
+ return fallback;
36
+ if (!Number.isFinite(value) || value < 1) {
37
+ return fail('invalid-options', 'options', 'limits must be positive finite numbers');
38
+ }
39
+ return Math.min(Math.floor(value), maximum);
40
+ }
41
+ function parseLimits(options) {
42
+ return {
43
+ maxBytes: bounded(options.maxBytes, SQLITE_SCHEMA_MAX_BYTES, SQLITE_SCHEMA_MAX_BYTES),
44
+ maxTables: bounded(options.maxTables, SQLITE_SCHEMA_MAX_TABLES, SQLITE_SCHEMA_MAX_TABLES),
45
+ maxColumns: bounded(options.maxColumns, SQLITE_SCHEMA_MAX_COLUMNS, SQLITE_SCHEMA_MAX_COLUMNS),
46
+ };
47
+ }
48
+ function tokenize(sql) {
49
+ const tokens = [];
50
+ let parenDepth = 0;
51
+ let index = 0;
52
+ while (index < sql.length) {
53
+ const start = index;
54
+ const char = sql[index] ?? '';
55
+ if (/\s/.test(char)) {
56
+ index += 1;
57
+ continue;
58
+ }
59
+ if (char === '-' && sql[index + 1] === '-') {
60
+ index += 2;
61
+ while (index < sql.length && sql[index] !== '\n')
62
+ index += 1;
63
+ continue;
64
+ }
65
+ if (char === '/' && sql[index + 1] === '*') {
66
+ const end = sql.indexOf('*/', index + 2);
67
+ if (end < 0)
68
+ fail('invalid-ddl', 'sql', 'unterminated comment');
69
+ index = end + 2;
70
+ continue;
71
+ }
72
+ if (char === '"' || char === '`' || char === '[' || char === "'") {
73
+ const closing = char === '[' ? ']' : char;
74
+ const kind = char === "'" ? 'string' : 'identifier';
75
+ index += 1;
76
+ let value = '';
77
+ let closed = false;
78
+ while (index < sql.length) {
79
+ const current = sql[index] ?? '';
80
+ if (current === closing) {
81
+ if (sql[index + 1] === closing && char !== '[') {
82
+ value += closing;
83
+ index += 2;
84
+ continue;
85
+ }
86
+ index += 1;
87
+ closed = true;
88
+ break;
89
+ }
90
+ value += current;
91
+ index += 1;
92
+ }
93
+ if (!closed)
94
+ fail('invalid-ddl', 'sql', 'unterminated quoted token');
95
+ tokens.push({ kind, value, offset: start });
96
+ }
97
+ else if (/[A-Za-z_$]/.test(char)) {
98
+ index += 1;
99
+ while (index < sql.length && /[A-Za-z0-9_$]/.test(sql[index] ?? ''))
100
+ index += 1;
101
+ tokens.push({ kind: 'identifier', value: sql.slice(start, index), offset: start });
102
+ }
103
+ else if (/[0-9]/.test(char) ||
104
+ ((char === '+' || char === '-') && /[0-9]/.test(sql[index + 1] ?? ''))) {
105
+ const number = sql.slice(index).match(/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/)?.[0];
106
+ if (!number)
107
+ fail('invalid-ddl', 'sql', 'contains an invalid numeric token');
108
+ index += number.length;
109
+ tokens.push({ kind: 'number', value: number, offset: start });
110
+ }
111
+ else if ('(),.;+-*/%<>=!|&~'.includes(char)) {
112
+ if (char === '(') {
113
+ parenDepth += 1;
114
+ if (parenDepth > IMPORT_MAX_DEPTH) {
115
+ fail('too-deep', 'sql', `exceeds the maximum nesting depth of ${IMPORT_MAX_DEPTH}`);
116
+ }
117
+ }
118
+ else if (char === ')' && parenDepth > 0) {
119
+ parenDepth -= 1;
120
+ }
121
+ tokens.push({ kind: 'symbol', value: char, offset: start });
122
+ index += 1;
123
+ }
124
+ else {
125
+ fail('unsupported-ddl', 'sql', 'contains an unsupported token');
126
+ }
127
+ if (tokens.length > SQLITE_SCHEMA_MAX_TOKENS) {
128
+ fail('too-many-tokens', 'sql', `exceeds the ${SQLITE_SCHEMA_MAX_TOKENS} token limit`);
129
+ }
130
+ }
131
+ return tokens;
132
+ }
133
+ function isWord(token, word) {
134
+ return token?.kind === 'identifier' && token.value.toUpperCase() === word;
135
+ }
136
+ function safeName(source, fallback) {
137
+ const words = source
138
+ .normalize('NFKD')
139
+ .replace(/[\u0300-\u036f]/g, '')
140
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
141
+ .replace(/[^A-Za-z0-9]+/g, ' ')
142
+ .trim()
143
+ .split(/\s+/)
144
+ .filter(Boolean);
145
+ const label = (words.join(' ') || fallback).slice(0, 60);
146
+ let name = words.map((part) => part.toLowerCase()).join('-') || fallback;
147
+ if (!/^[a-z]/.test(name))
148
+ name = `imported-${name}`;
149
+ return { name: name.slice(0, 60).replace(/-+$/g, '') || fallback, label };
150
+ }
151
+ function fieldName(source, fallback, used) {
152
+ const words = source
153
+ .normalize('NFKD')
154
+ .replace(/[\u0300-\u036f]/g, '')
155
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
156
+ .replace(/[^A-Za-z0-9]+/g, ' ')
157
+ .trim()
158
+ .split(/\s+/)
159
+ .filter(Boolean);
160
+ let name = words
161
+ .map((part, index) => {
162
+ const lower = part.toLowerCase();
163
+ return index === 0 ? lower : `${lower[0]?.toUpperCase() ?? ''}${lower.slice(1)}`;
164
+ })
165
+ .join('');
166
+ if (!name)
167
+ name = fallback;
168
+ if (/^\d/.test(name))
169
+ name = `field${name}`;
170
+ if (RESERVED_FIELDS.has(name))
171
+ name = `field${name[0]?.toUpperCase() ?? ''}${name.slice(1)}`;
172
+ name = name.slice(0, 64);
173
+ const base = name;
174
+ let suffix = 2;
175
+ while (used.has(name))
176
+ name = `${base.slice(0, 64 - String(suffix).length)}${suffix++}`;
177
+ used.add(name);
178
+ return name;
179
+ }
180
+ class Parser {
181
+ index = 0;
182
+ tables = [];
183
+ tokens;
184
+ limits;
185
+ notes;
186
+ constructor(tokens, limits, notes) {
187
+ this.tokens = tokens;
188
+ this.limits = limits;
189
+ this.notes = notes;
190
+ }
191
+ parse() {
192
+ while (this.index < this.tokens.length) {
193
+ if (this.peek()?.value === ';') {
194
+ this.index += 1;
195
+ continue;
196
+ }
197
+ const table = this.parseTable();
198
+ if (this.tables.some((existing) => existing.sourceName.toLowerCase() === table.sourceName.toLowerCase())) {
199
+ fail('invalid-ddl', 'tables', 'contains duplicate table declarations');
200
+ }
201
+ this.tables.push(table);
202
+ if (this.tables.length > this.limits.maxTables) {
203
+ fail('too-many-tables', 'tables', `exceeds the ${this.limits.maxTables} table limit`);
204
+ }
205
+ if (this.peek()?.value === ';')
206
+ this.index += 1;
207
+ else if (this.index < this.tokens.length)
208
+ fail('unsupported-ddl', 'sql', 'expected a semicolon between CREATE TABLE statements');
209
+ }
210
+ if (this.tables.length === 0)
211
+ fail('invalid-ddl', 'sql', 'expected at least one CREATE TABLE declaration');
212
+ return this.tables;
213
+ }
214
+ peek(offset = 0) {
215
+ return this.tokens[this.index + offset];
216
+ }
217
+ take() {
218
+ const token = this.peek();
219
+ if (!token)
220
+ fail('invalid-ddl', 'sql', 'ended before the declaration was complete');
221
+ this.index += 1;
222
+ return token;
223
+ }
224
+ expectSymbol(value, path) {
225
+ if (this.peek()?.value !== value)
226
+ fail('invalid-ddl', path, `expected ${value}`);
227
+ this.index += 1;
228
+ }
229
+ identifier(path) {
230
+ const token = this.take();
231
+ if (token.kind !== 'identifier')
232
+ fail('invalid-ddl', path, 'expected an identifier');
233
+ return token.value;
234
+ }
235
+ parseTable() {
236
+ const path = `tables[${this.tables.length}]`;
237
+ if (!isWord(this.take(), 'CREATE'))
238
+ fail('unsupported-ddl', path, 'only CREATE TABLE is supported');
239
+ if (isWord(this.peek(), 'TEMP') || isWord(this.peek(), 'TEMPORARY'))
240
+ this.index += 1;
241
+ if (!isWord(this.take(), 'TABLE'))
242
+ fail('unsupported-ddl', path, 'only CREATE TABLE is supported');
243
+ if (isWord(this.peek(), 'IF')) {
244
+ this.index += 1;
245
+ if (!isWord(this.take(), 'NOT') || !isWord(this.take(), 'EXISTS')) {
246
+ fail('invalid-ddl', path, 'malformed IF NOT EXISTS clause');
247
+ }
248
+ }
249
+ const sourceName = this.identifier(`${path}.name`);
250
+ const normalized = safeName(sourceName, `table-${this.tables.length + 1}`);
251
+ this.expectSymbol('(', `${path}.columns`);
252
+ const definitions = [];
253
+ let current = [];
254
+ let depth = 0;
255
+ while (this.index < this.tokens.length) {
256
+ const token = this.take();
257
+ if (token.value === '(')
258
+ depth += 1;
259
+ if (token.value === ')') {
260
+ if (depth === 0) {
261
+ if (current.length > 0)
262
+ definitions.push(current);
263
+ break;
264
+ }
265
+ depth -= 1;
266
+ }
267
+ if (token.value === ',' && depth === 0) {
268
+ if (current.length === 0)
269
+ fail('invalid-ddl', `${path}.columns`, 'contains an empty declaration');
270
+ definitions.push(current);
271
+ current = [];
272
+ }
273
+ else {
274
+ current.push(token);
275
+ }
276
+ }
277
+ if (this.tokens[this.index - 1]?.value !== ')')
278
+ fail('invalid-ddl', `${path}.columns`, 'missing closing parenthesis');
279
+ const columns = [];
280
+ const primaryKey = [];
281
+ const foreignKeys = [];
282
+ for (const definition of definitions) {
283
+ if (isWord(definition[0], 'CONSTRAINT')) {
284
+ definition.shift();
285
+ definition.shift();
286
+ }
287
+ if (isWord(definition[0], 'PRIMARY')) {
288
+ this.parseTablePrimaryKey(definition, primaryKey, normalized.name);
289
+ }
290
+ else if (isWord(definition[0], 'FOREIGN')) {
291
+ foreignKeys.push(this.parseTableForeignKey(definition, normalized.name));
292
+ }
293
+ else if (isWord(definition[0], 'UNIQUE') || isWord(definition[0], 'CHECK')) {
294
+ this.notes.push(`${normalized.label}: a table constraint is omitted because blueprint drafts do not model it.`);
295
+ }
296
+ else {
297
+ columns.push(this.parseColumn(definition, normalized.name, columns.length));
298
+ }
299
+ }
300
+ if (columns.length > this.limits.maxColumns) {
301
+ fail('too-many-columns', `${path}.columns`, `exceeds the ${this.limits.maxColumns} column limit`);
302
+ }
303
+ if (columns.length === 0)
304
+ fail('invalid-ddl', `${path}.columns`, 'a table must declare at least one column');
305
+ for (const column of columns) {
306
+ if (column.primaryKey && !primaryKey.includes(column.sourceName))
307
+ primaryKey.push(column.sourceName);
308
+ if (primaryKey.some((key) => key.toLowerCase() === column.sourceName.toLowerCase())) {
309
+ column.primaryKey = true;
310
+ }
311
+ }
312
+ return {
313
+ sourceName,
314
+ name: normalized.name,
315
+ label: normalized.label,
316
+ columns,
317
+ primaryKey,
318
+ foreignKeys,
319
+ };
320
+ }
321
+ parseTablePrimaryKey(definition, keys, entity) {
322
+ if (!isWord(definition[1], 'KEY'))
323
+ fail('invalid-ddl', `tables.${entity}.primaryKey`, 'malformed PRIMARY KEY');
324
+ let cursor = 2;
325
+ if (definition[cursor]?.value.toUpperCase() === 'ASC' ||
326
+ definition[cursor]?.value.toUpperCase() === 'DESC')
327
+ cursor += 1;
328
+ if (definition[cursor]?.value !== '(')
329
+ fail('invalid-ddl', `tables.${entity}.primaryKey`, 'expected a primary-key column list');
330
+ cursor += 1;
331
+ while (cursor < definition.length && definition[cursor]?.value !== ')') {
332
+ const token = definition[cursor];
333
+ if (token?.kind !== 'identifier')
334
+ fail('invalid-ddl', `tables.${entity}.primaryKey`, 'expected a primary-key column');
335
+ keys.push(token.value);
336
+ cursor += 1;
337
+ if (definition[cursor]?.value === ',')
338
+ cursor += 1;
339
+ else
340
+ break;
341
+ }
342
+ if (definition[cursor]?.value !== ')')
343
+ fail('invalid-ddl', `tables.${entity}.primaryKey`, 'malformed primary-key column list');
344
+ }
345
+ parseTableForeignKey(definition, entity) {
346
+ if (!isWord(definition[1], 'KEY'))
347
+ fail('invalid-ddl', `tables.${entity}.foreignKeys`, 'malformed FOREIGN KEY');
348
+ let cursor = 2;
349
+ const columns = this.identifierList(definition, cursor, `tables.${entity}.foreignKeys`);
350
+ cursor = columns.next;
351
+ if (!isWord(definition[cursor], 'REFERENCES'))
352
+ fail('invalid-ddl', `tables.${entity}.foreignKeys`, 'expected REFERENCES');
353
+ cursor += 1;
354
+ const target = definition[cursor]?.value;
355
+ if (!target || definition[cursor]?.kind !== 'identifier')
356
+ fail('invalid-ddl', `tables.${entity}.foreignKeys`, 'expected referenced table');
357
+ cursor += 1;
358
+ let targetColumns = [];
359
+ if (definition[cursor]?.value === '(') {
360
+ const parsed = this.identifierList(definition, cursor, `tables.${entity}.foreignKeys`);
361
+ targetColumns = parsed.values;
362
+ cursor = parsed.next;
363
+ }
364
+ return {
365
+ columns: columns.values,
366
+ target,
367
+ targetColumns,
368
+ onDelete: this.parseDeleteAction(definition, cursor, `tables.${entity}.foreignKeys`),
369
+ };
370
+ }
371
+ identifierList(tokens, start, path) {
372
+ if (tokens[start]?.value !== '(')
373
+ fail('invalid-ddl', path, 'expected a column list');
374
+ const values = [];
375
+ let cursor = start + 1;
376
+ while (cursor < tokens.length && tokens[cursor]?.value !== ')') {
377
+ const token = tokens[cursor];
378
+ if (token?.kind !== 'identifier')
379
+ fail('invalid-ddl', path, 'expected a column name');
380
+ values.push(token.value);
381
+ cursor += 1;
382
+ if (tokens[cursor]?.value === ',')
383
+ cursor += 1;
384
+ else
385
+ break;
386
+ }
387
+ if (tokens[cursor]?.value !== ')' || values.length === 0)
388
+ fail('invalid-ddl', path, 'malformed column list');
389
+ return { values, next: cursor + 1 };
390
+ }
391
+ parseDeleteAction(tokens, start, path) {
392
+ for (let cursor = start; cursor < tokens.length; cursor += 1) {
393
+ if (!isWord(tokens[cursor], 'ON') || !isWord(tokens[cursor + 1], 'DELETE'))
394
+ continue;
395
+ const action = tokens[cursor + 2]?.value.toUpperCase();
396
+ if (action === 'CASCADE')
397
+ return 'cascade';
398
+ if (action === 'SET' && isWord(tokens[cursor + 3], 'NULL'))
399
+ return 'nullify';
400
+ if (action === 'RESTRICT' || action === 'NO')
401
+ return 'restrict';
402
+ fail('unsupported-ddl', path, 'uses an unsupported ON DELETE action');
403
+ }
404
+ return 'restrict';
405
+ }
406
+ parseColumn(definition, entity, columnIndex) {
407
+ const sourceName = definition[0]?.kind === 'identifier' ? definition[0].value : undefined;
408
+ if (!sourceName)
409
+ fail('invalid-ddl', `tables.${entity}.columns[${columnIndex}]`, 'expected a column name');
410
+ const path = `tables.${entity}.columns.${safeName(sourceName, 'field').name}`;
411
+ let cursor = 1;
412
+ const typeTokens = [];
413
+ let parenDepth = 0;
414
+ while (cursor < definition.length) {
415
+ const token = definition[cursor];
416
+ if (parenDepth === 0 &&
417
+ token?.kind === 'identifier' &&
418
+ CONSTRAINT_WORDS.has(token.value.toUpperCase()))
419
+ break;
420
+ if (token?.value === '(')
421
+ parenDepth += 1;
422
+ if (token?.value === ')')
423
+ parenDepth -= 1;
424
+ typeTokens.push(token);
425
+ cursor += 1;
426
+ }
427
+ const type = typeTokens
428
+ .map((token) => token.value)
429
+ .join('')
430
+ .toUpperCase();
431
+ let primaryKey = false;
432
+ let notNull = false;
433
+ let inlineReference;
434
+ while (cursor < definition.length) {
435
+ const token = definition[cursor];
436
+ if (isWord(token, 'CONSTRAINT')) {
437
+ cursor += 2;
438
+ }
439
+ else if (isWord(token, 'PRIMARY') && isWord(definition[cursor + 1], 'KEY')) {
440
+ primaryKey = true;
441
+ cursor += 2;
442
+ }
443
+ else if (isWord(token, 'NOT') && isWord(definition[cursor + 1], 'NULL')) {
444
+ notNull = true;
445
+ cursor += 2;
446
+ }
447
+ else if (isWord(token, 'NULL')) {
448
+ cursor += 1;
449
+ }
450
+ else if (isWord(token, 'UNIQUE')) {
451
+ this.notes.push(`${path}: UNIQUE is omitted because blueprint fields do not express uniqueness.`);
452
+ cursor += 1;
453
+ }
454
+ else if (isWord(token, 'CHECK')) {
455
+ cursor = this.skipExpression(definition, cursor + 1, path);
456
+ this.notes.push(`${path}: CHECK is omitted because blueprint drafts do not model checks.`);
457
+ }
458
+ else if (isWord(token, 'DEFAULT')) {
459
+ cursor = this.skipDefault(definition, cursor + 1, path);
460
+ }
461
+ else if (isWord(token, 'COLLATE')) {
462
+ cursor += 2;
463
+ this.notes.push(`${path}: collation is omitted from the draft.`);
464
+ }
465
+ else if (isWord(token, 'REFERENCES')) {
466
+ const parsed = this.parseInlineReference(definition, cursor + 1, path);
467
+ inlineReference = parsed.reference;
468
+ cursor = parsed.next;
469
+ }
470
+ else if (isWord(token, 'AUTOINCREMENT') || isWord(token, 'ASC') || isWord(token, 'DESC')) {
471
+ cursor += 1;
472
+ }
473
+ else {
474
+ fail('unsupported-ddl', path, 'contains an unsupported column constraint');
475
+ }
476
+ }
477
+ const name = fieldName(sourceName, `field${columnIndex + 1}`, new Set());
478
+ return {
479
+ sourceName,
480
+ name,
481
+ kind: type,
482
+ notNull,
483
+ primaryKey,
484
+ ...(inlineReference ? { inlineReference } : {}),
485
+ };
486
+ }
487
+ parseInlineReference(tokens, start, path) {
488
+ const targetToken = tokens[start];
489
+ if (targetToken?.kind !== 'identifier')
490
+ fail('invalid-ddl', path, 'expected referenced table');
491
+ let cursor = start + 1;
492
+ let targetColumns = [];
493
+ if (tokens[cursor]?.value === '(') {
494
+ const parsed = this.identifierList(tokens, cursor, path);
495
+ targetColumns = parsed.values;
496
+ cursor = parsed.next;
497
+ }
498
+ const onDelete = this.parseDeleteAction(tokens, cursor, path);
499
+ while (cursor < tokens.length) {
500
+ if (isWord(tokens[cursor], 'ON')) {
501
+ cursor += isWord(tokens[cursor + 2], 'SET') ? 4 : 3;
502
+ }
503
+ else if (isWord(tokens[cursor], 'MATCH'))
504
+ cursor += 2;
505
+ else if (isWord(tokens[cursor], 'DEFERRABLE'))
506
+ cursor += 1;
507
+ else if (isWord(tokens[cursor], 'INITIALLY'))
508
+ cursor += 2;
509
+ else if (isWord(tokens[cursor], 'NOT') && isWord(tokens[cursor + 1], 'DEFERRABLE'))
510
+ cursor += 2;
511
+ else
512
+ fail('unsupported-ddl', path, 'contains an unsupported REFERENCES clause');
513
+ }
514
+ return {
515
+ reference: { columns: [], target: targetToken.value, targetColumns, onDelete },
516
+ next: tokens.length,
517
+ };
518
+ }
519
+ skipDefault(tokens, start, path) {
520
+ const first = tokens[start];
521
+ if (!first)
522
+ fail('invalid-ddl', path, 'DEFAULT is missing a value');
523
+ if (first.value === '(')
524
+ return this.skipExpression(tokens, start, path);
525
+ if (first.kind === 'string' || first.kind === 'number' || first.kind === 'identifier')
526
+ return start + 1;
527
+ fail('unsupported-ddl', path, 'uses an unsupported DEFAULT expression');
528
+ }
529
+ skipExpression(tokens, start, path) {
530
+ if (tokens[start]?.value !== '(')
531
+ fail('invalid-ddl', path, 'expected a parenthesized expression');
532
+ let depth = 0;
533
+ for (let cursor = start; cursor < tokens.length; cursor += 1) {
534
+ if (tokens[cursor]?.value === '(')
535
+ depth += 1;
536
+ if (tokens[cursor]?.value === ')')
537
+ depth -= 1;
538
+ if (depth === 0)
539
+ return cursor + 1;
540
+ }
541
+ fail('invalid-ddl', path, 'has an unterminated expression');
542
+ }
543
+ }
544
+ function fieldKind(sqlType, path, notes) {
545
+ const base = sqlType.replace(/\(.*\)$/, '').trim();
546
+ if (!base) {
547
+ notes.push(`${path}: SQLite's implicit BLOB affinity is represented as text for author review.`);
548
+ return 'text';
549
+ }
550
+ if (/INT/.test(base))
551
+ return 'integer';
552
+ if (/CHAR|CLOB|TEXT/.test(base))
553
+ return 'text';
554
+ if (/REAL|FLOA|DOUB|NUMERIC|DECIMAL/.test(base))
555
+ return 'number';
556
+ if (/BOOL/.test(base))
557
+ return 'boolean';
558
+ if (/DATETIME|TIMESTAMP/.test(base))
559
+ return 'datetime';
560
+ if (/^DATE$/.test(base))
561
+ return 'date';
562
+ if (/BLOB/.test(base)) {
563
+ notes.push(`${path}: BLOB is represented as JSON metadata for author review; binary content is never imported.`);
564
+ return 'json';
565
+ }
566
+ fail('unsupported-type', path, 'uses a SQLite type outside the documented mapping');
567
+ }
568
+ function plural(name) {
569
+ if (name.endsWith('s'))
570
+ return name;
571
+ return `${name}s`;
572
+ }
573
+ /** Parse bounded CREATE TABLE text into a validated, value-free blueprint draft. */
574
+ export function importSqliteSchema(sql, options = {}) {
575
+ const limits = parseLimits(options);
576
+ if (new TextEncoder().encode(sql).length > limits.maxBytes) {
577
+ fail('input-too-large', 'sql', `exceeds the ${limits.maxBytes}-byte limit`);
578
+ }
579
+ const notes = [];
580
+ const tables = new Parser(tokenize(sql), limits, notes).parse();
581
+ const sourceToEntity = new Map(tables.map((table) => [table.sourceName.toLowerCase(), table.name]));
582
+ const usedEntityNames = new Set();
583
+ for (const table of tables) {
584
+ if (usedEntityNames.has(table.name))
585
+ fail('invalid-ddl', 'tables', 'table names normalize to a duplicate entity name');
586
+ usedEntityNames.add(table.name);
587
+ }
588
+ const primaryKeys = [];
589
+ const entityInputs = tables.map((table) => {
590
+ const usedFields = new Set();
591
+ const sourceFieldNames = new Map();
592
+ const fields = [];
593
+ const primaryFields = table.primaryKey.map((key) => {
594
+ const column = table.columns.find((candidate) => candidate.sourceName.toLowerCase() === key.toLowerCase());
595
+ if (!column)
596
+ fail('invalid-reference', `tables.${table.name}.primaryKey`, 'names an unknown column');
597
+ return column;
598
+ });
599
+ const platformId = primaryFields.length === 1 &&
600
+ primaryFields[0]?.sourceName.toLowerCase() === 'id' &&
601
+ primaryFields[0]?.kind === 'INTEGER';
602
+ const pkNames = [];
603
+ for (const column of table.columns) {
604
+ if (column.sourceName.toLowerCase() === 'id' && platformId)
605
+ continue;
606
+ let sourceField = column.sourceName;
607
+ let relation;
608
+ if (column.inlineReference)
609
+ relation = { ...column.inlineReference, columns: [column.sourceName] };
610
+ else
611
+ relation = table.foreignKeys.find((key) => key.columns.length === 1 &&
612
+ key.columns[0]?.toLowerCase() === column.sourceName.toLowerCase());
613
+ if (relation) {
614
+ const targetName = sourceToEntity.get(relation.target.toLowerCase());
615
+ if (!targetName)
616
+ fail('invalid-reference', `tables.${table.name}.columns.${column.name}`, 'references an undeclared table');
617
+ const targetTable = tables.find((candidate) => candidate.name === targetName);
618
+ const targetColumn = relation.targetColumns[0];
619
+ if (relation.targetColumns.length > 1 || !targetTable)
620
+ fail('unsupported-ddl', `tables.${table.name}.columns.${column.name}`, 'composite foreign keys are not representable');
621
+ if (targetColumn &&
622
+ !targetTable.columns.some((candidate) => candidate.sourceName.toLowerCase() === targetColumn.toLowerCase())) {
623
+ fail('invalid-reference', `tables.${table.name}.columns.${column.name}`, 'references an unknown target column');
624
+ }
625
+ sourceField = sourceField.replace(/_?id$/i, '') || sourceField;
626
+ const name = fieldName(sourceField, `field${fields.length + 1}`, usedFields);
627
+ sourceFieldNames.set(column.sourceName.toLowerCase(), name);
628
+ fields.push({
629
+ name,
630
+ label: safeName(sourceField, 'Relation').label,
631
+ kind: 'relation',
632
+ required: column.notNull || column.primaryKey,
633
+ to: targetName,
634
+ cardinality: 'one',
635
+ onDelete: relation.onDelete,
636
+ });
637
+ continue;
638
+ }
639
+ const name = fieldName(sourceField, `field${fields.length + 1}`, usedFields);
640
+ sourceFieldNames.set(column.sourceName.toLowerCase(), name);
641
+ const kind = fieldKind(column.kind, `tables.${table.name}.columns.${name}`, notes);
642
+ fields.push({
643
+ name,
644
+ label: safeName(sourceField, 'Field').label,
645
+ kind,
646
+ required: column.notNull || column.primaryKey,
647
+ ...(kind === 'text' ? { maxLength: 200 } : {}),
648
+ ...(kind === 'json' && /BLOB/i.test(column.kind) ? {} : {}),
649
+ });
650
+ if (table.primaryKey.some((key) => key.toLowerCase() === column.sourceName.toLowerCase()))
651
+ pkNames.push(name);
652
+ }
653
+ for (const foreignKey of table.foreignKeys) {
654
+ if (foreignKey.columns.length !== 1)
655
+ fail('unsupported-ddl', `tables.${table.name}.foreignKeys`, 'composite foreign keys are not representable');
656
+ const column = table.columns.find((candidate) => candidate.sourceName.toLowerCase() === foreignKey.columns[0]?.toLowerCase());
657
+ if (!column)
658
+ fail('invalid-reference', `tables.${table.name}.foreignKeys`, 'names an unknown local column');
659
+ if (!sourceFieldNames.has(column.sourceName.toLowerCase())) {
660
+ const targetName = sourceToEntity.get(foreignKey.target.toLowerCase());
661
+ if (!targetName)
662
+ fail('invalid-reference', `tables.${table.name}.foreignKeys`, 'references an undeclared table');
663
+ const relName = fieldName(column.sourceName.replace(/_?id$/i, '') || column.sourceName, `field${fields.length + 1}`, usedFields);
664
+ sourceFieldNames.set(column.sourceName.toLowerCase(), relName);
665
+ fields.push({
666
+ name: relName,
667
+ label: safeName(column.sourceName, 'Relation').label,
668
+ kind: 'relation',
669
+ required: column.notNull || column.primaryKey,
670
+ to: targetName,
671
+ cardinality: 'one',
672
+ onDelete: foreignKey.onDelete,
673
+ });
674
+ }
675
+ }
676
+ if (fields.length === 0) {
677
+ fields.push({ name: 'name', label: 'Name', kind: 'text', maxLength: 200, required: false });
678
+ notes.push(`${table.label}: added a blank Name field because the only declared column is SQLite's platform id.`);
679
+ }
680
+ const title = fields.find((field) => ['text', 'longText', 'integer', 'email', 'url', 'phone'].includes(String(field.kind))) ?? fields[0];
681
+ const mappedKeys = primaryFields
682
+ .map((column) => sourceFieldNames.get(column.sourceName.toLowerCase()))
683
+ .filter((name) => Boolean(name));
684
+ primaryKeys.push({
685
+ entity: table.name,
686
+ fields: mappedKeys,
687
+ representedByPlatformId: platformId,
688
+ });
689
+ if (primaryFields.length > 0 && !platformId)
690
+ notes.push(`${table.label}: primary-key columns are marked required; uniqueness is not represented by blueprint fields.`);
691
+ if (platformId)
692
+ notes.push(`${table.label}: INTEGER PRIMARY KEY maps to Protoyard's generated platform id.`);
693
+ return {
694
+ name: table.name,
695
+ plural: plural(table.name),
696
+ label: table.label,
697
+ fields,
698
+ titleField: title?.name,
699
+ searchable: fields
700
+ .filter((field) => ['text', 'email', 'url', 'phone'].includes(String(field.kind)))
701
+ .slice(0, 3)
702
+ .map((field) => String(field.name)),
703
+ };
704
+ });
705
+ for (const table of tables) {
706
+ for (const foreignKey of table.foreignKeys) {
707
+ if (foreignKey.columns.length !== 1)
708
+ fail('unsupported-ddl', `tables.${table.name}.foreignKeys`, 'composite foreign keys are not representable');
709
+ const local = table.columns.find((column) => column.sourceName.toLowerCase() === foreignKey.columns[0]?.toLowerCase());
710
+ const targetName = sourceToEntity.get(foreignKey.target.toLowerCase());
711
+ const target = tables.find((candidate) => candidate.name === targetName);
712
+ if (!local || !targetName || !target)
713
+ fail('invalid-reference', `tables.${table.name}.foreignKeys`, 'references an unknown table or column');
714
+ if (foreignKey.targetColumns.length > 1)
715
+ fail('unsupported-ddl', `tables.${table.name}.foreignKeys`, 'composite foreign keys are not representable');
716
+ if (foreignKey.targetColumns[0] &&
717
+ !target.columns.some((column) => column.sourceName.toLowerCase() === foreignKey.targetColumns[0]?.toLowerCase())) {
718
+ fail('invalid-reference', `tables.${table.name}.foreignKeys`, 'references an unknown target column');
719
+ }
720
+ }
721
+ }
722
+ const parsed = parseBlueprint({
723
+ formatVersion: 1,
724
+ name: 'Imported SQLite schema',
725
+ entities: entityInputs,
726
+ roles: [{ name: 'owner', permissions: { '*': ['*'] } }],
727
+ });
728
+ return { entities: parsed.entities, primaryKeys, notes, tablesImported: tables.length };
729
+ }