litedbmodel 1.2.9 → 2.0.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 (88) hide show
  1. package/README.md +1 -1
  2. package/dist/DBModel.d.ts.map +1 -1
  3. package/dist/DBModel.js +6 -0
  4. package/dist/DBModel.js.map +1 -1
  5. package/dist/scp/authoring-sql.d.ts +56 -0
  6. package/dist/scp/authoring-sql.d.ts.map +1 -0
  7. package/dist/scp/authoring-sql.js +83 -0
  8. package/dist/scp/authoring-sql.js.map +1 -0
  9. package/dist/scp/authoring.d.ts +155 -0
  10. package/dist/scp/authoring.d.ts.map +1 -0
  11. package/dist/scp/authoring.js +162 -0
  12. package/dist/scp/authoring.js.map +1 -0
  13. package/dist/scp/bridge.d.ts +64 -0
  14. package/dist/scp/bridge.d.ts.map +1 -0
  15. package/dist/scp/bridge.js +329 -0
  16. package/dist/scp/bridge.js.map +1 -0
  17. package/dist/scp/catalog.d.ts +91 -0
  18. package/dist/scp/catalog.d.ts.map +1 -0
  19. package/dist/scp/catalog.js +203 -0
  20. package/dist/scp/catalog.js.map +1 -0
  21. package/dist/scp/codegen.d.ts +131 -0
  22. package/dist/scp/codegen.d.ts.map +1 -0
  23. package/dist/scp/codegen.js +137 -0
  24. package/dist/scp/codegen.js.map +1 -0
  25. package/dist/scp/compile-dialect.d.ts +39 -0
  26. package/dist/scp/compile-dialect.d.ts.map +1 -0
  27. package/dist/scp/compile-dialect.js +62 -0
  28. package/dist/scp/compile-dialect.js.map +1 -0
  29. package/dist/scp/compile-sqlite.d.ts +114 -0
  30. package/dist/scp/compile-sqlite.d.ts.map +1 -0
  31. package/dist/scp/compile-sqlite.js +149 -0
  32. package/dist/scp/compile-sqlite.js.map +1 -0
  33. package/dist/scp/dialect.d.ts +141 -0
  34. package/dist/scp/dialect.d.ts.map +1 -0
  35. package/dist/scp/dialect.js +194 -0
  36. package/dist/scp/dialect.js.map +1 -0
  37. package/dist/scp/errors.d.ts +36 -0
  38. package/dist/scp/errors.d.ts.map +1 -0
  39. package/dist/scp/errors.js +72 -0
  40. package/dist/scp/errors.js.map +1 -0
  41. package/dist/scp/guard.d.ts +47 -0
  42. package/dist/scp/guard.d.ts.map +1 -0
  43. package/dist/scp/guard.js +137 -0
  44. package/dist/scp/guard.js.map +1 -0
  45. package/dist/scp/index.cjs +4744 -0
  46. package/dist/scp/index.d.ts +49 -0
  47. package/dist/scp/index.d.ts.map +1 -0
  48. package/dist/scp/index.js +164 -0
  49. package/dist/scp/index.js.map +1 -0
  50. package/dist/scp/index.mjs +2000 -0
  51. package/dist/scp/ir.d.ts +92 -0
  52. package/dist/scp/ir.d.ts.map +1 -0
  53. package/dist/scp/ir.js +28 -0
  54. package/dist/scp/ir.js.map +1 -0
  55. package/dist/scp/relation.d.ts +136 -0
  56. package/dist/scp/relation.d.ts.map +1 -0
  57. package/dist/scp/relation.js +191 -0
  58. package/dist/scp/relation.js.map +1 -0
  59. package/dist/scp/render.d.ts +40 -0
  60. package/dist/scp/render.d.ts.map +1 -0
  61. package/dist/scp/render.js +170 -0
  62. package/dist/scp/render.js.map +1 -0
  63. package/dist/scp/runtime.d.ts +295 -0
  64. package/dist/scp/runtime.d.ts.map +1 -0
  65. package/dist/scp/runtime.js +665 -0
  66. package/dist/scp/runtime.js.map +1 -0
  67. package/dist/scp/typed-object.d.ts +88 -0
  68. package/dist/scp/typed-object.d.ts.map +1 -0
  69. package/dist/scp/typed-object.js +163 -0
  70. package/dist/scp/typed-object.js.map +1 -0
  71. package/dist/scp/write-plan.d.ts +161 -0
  72. package/dist/scp/write-plan.d.ts.map +1 -0
  73. package/dist/scp/write-plan.js +528 -0
  74. package/dist/scp/write-plan.js.map +1 -0
  75. package/dist/scp/write-runtime.d.ts +80 -0
  76. package/dist/scp/write-runtime.d.ts.map +1 -0
  77. package/dist/scp/write-runtime.js +167 -0
  78. package/dist/scp/write-runtime.js.map +1 -0
  79. package/dist/scp/writes.d.ts +251 -0
  80. package/dist/scp/writes.d.ts.map +1 -0
  81. package/dist/scp/writes.js +176 -0
  82. package/dist/scp/writes.js.map +1 -0
  83. package/docs/proposal/contracts-architecture-feasibility.md +328 -0
  84. package/docs/proposal/contracts-architecture.md +579 -0
  85. package/docs/proposal/litedbmodel-v2-scp-architecture.md +382 -0
  86. package/docs/proposal/sql-dynamic-expansion-spec.md +175 -0
  87. package/package.json +27 -5
  88. package/docs/README_REVIEW.md +0 -23
@@ -0,0 +1,2000 @@
1
+ // src/scp/catalog.ts
2
+ import { classifyBehaviorEffect } from "behavior-contracts";
3
+ var P = (type, required) => required ? { type, required } : { type };
4
+ var SELECT_PORTS = {
5
+ table: P("string", true),
6
+ select: P("string[]", true),
7
+ where: P("fragment"),
8
+ order: P("string"),
9
+ limit: P("expr"),
10
+ offset: P("expr"),
11
+ group: P("string")
12
+ };
13
+ var WRITE_PORTS = {
14
+ table: P("string", true),
15
+ where: P("fragment"),
16
+ returning: P("string"),
17
+ onConflict: P("string"),
18
+ onConflictAction: P("string")
19
+ };
20
+ var entry = (name, inputPorts, shape, portableToIR) => ({
21
+ name,
22
+ inputPorts,
23
+ output: { shape },
24
+ portableToIR,
25
+ // Dynamic port families (where/set/values record ports) are validated by
26
+ // assertComponentsInCatalog, not statically enumerable. Tx overrides back to
27
+ // fail-closed (its one port is static).
28
+ additionalPorts: true
29
+ });
30
+ var LITEDBMODEL_CATALOG = {
31
+ // Read: `items` (a Select root yields a row list; per-row collapse to `item` is the
32
+ // consumer's cardinality concern, mirrored from graphddb Query's `cardinality` port).
33
+ Select: entry("Select", { ...SELECT_PORTS, cardinality: P("string") }, "items", true),
34
+ // Writes yield the RETURNING row list (`items`); a single-row write collapses at the
35
+ // consumer boundary, same as reads.
36
+ Insert: entry("Insert", WRITE_PORTS, "items", true),
37
+ Update: entry("Update", WRITE_PORTS, "items", true),
38
+ Delete: entry("Delete", WRITE_PORTS, "items", true),
39
+ // The dynamic WHERE/SET fragment-tree component (§8). Its ports carry the fragment
40
+ // tree; the compiler resolves SKIP/AND-OR structure to fragment existence rules.
41
+ Fragment: entry("Fragment", { tree: P("fragment", true) }, "item", true),
42
+ // The runtime-side atomic envelope: never referenced by the portable IR (a transaction
43
+ // IS a component over the CRUD primitives) — runtime-only. Its single port is static,
44
+ // so it keeps bc's fail-closed default (no additionalPorts opt-in).
45
+ Tx: {
46
+ ...entry("Tx", { statements: P("map[]", true) }, "item", false),
47
+ additionalPorts: false
48
+ }
49
+ };
50
+ function catalogEntry(name) {
51
+ return LITEDBMODEL_CATALOG[name];
52
+ }
53
+ var CATALOG_NAMES = [
54
+ "Select",
55
+ "Insert",
56
+ "Update",
57
+ "Delete",
58
+ "Fragment",
59
+ "Tx"
60
+ ];
61
+ var WRITE_CATALOG_NAMES = [
62
+ "Insert",
63
+ "Update",
64
+ "Delete",
65
+ "Tx"
66
+ ];
67
+ var WRITE_SET = new Set(WRITE_CATALOG_NAMES);
68
+ var WRITE_PORT_FAMILIES = [
69
+ "values.",
70
+ "set."
71
+ ];
72
+ function assertComponentsInCatalog(components2) {
73
+ const errs = [];
74
+ for (const c of components2) {
75
+ for (const n of c.body) {
76
+ if ("cond" in n) continue;
77
+ const ref = "map" in n ? n.map : n;
78
+ const entryDef = LITEDBMODEL_CATALOG[ref.component];
79
+ if (entryDef === void 0) {
80
+ errs.push(`${c.name}/${n.id}: references '${ref.component}', not a LITEDBMODEL_CATALOG entry (known: ${CATALOG_NAMES.join(", ")})`);
81
+ continue;
82
+ }
83
+ const ports = ref.ports;
84
+ for (const [p, s] of Object.entries(entryDef.inputPorts)) {
85
+ if (s.required === true && !(p in ports)) {
86
+ errs.push(`${c.name}/${n.id} (${ref.component}): required port '${p}' is not wired`);
87
+ }
88
+ }
89
+ const isWriteItem = WRITE_SET.has(ref.component);
90
+ for (const p of Object.keys(ports)) {
91
+ if (p in entryDef.inputPorts) continue;
92
+ if (isWriteItem && WRITE_PORT_FAMILIES.some((f) => p.startsWith(f))) continue;
93
+ errs.push(`${c.name}/${n.id} (${ref.component}): port '${p}' is not declared by the catalog entry (nor a write record-family port)`);
94
+ }
95
+ }
96
+ }
97
+ if (errs.length > 0) {
98
+ throw new Error(`litedbmodel catalog validation failed (${errs.length} violation${errs.length === 1 ? "" : "s"}):
99
+ - ${errs.join("\n - ")}`);
100
+ }
101
+ }
102
+ function deriveContractEffect(component) {
103
+ return classifyBehaviorEffect(component, WRITE_CATALOG_NAMES) === "writing" ? "command" : "query";
104
+ }
105
+
106
+ // src/scp/ir.ts
107
+ var WHERE_SLOT = "{where}";
108
+
109
+ // src/scp/dialect.ts
110
+ function toDollarPlaceholders(sql) {
111
+ let n = 0;
112
+ let out = "";
113
+ for (const ch of sql) {
114
+ if (ch === "?") {
115
+ n += 1;
116
+ out += `$${n}`;
117
+ } else {
118
+ out += ch;
119
+ }
120
+ }
121
+ return out;
122
+ }
123
+ function onConflictClause(conflictColumns, action, excludedRef) {
124
+ const target = conflictColumns.length > 0 ? ` (${conflictColumns.join(", ")})` : "";
125
+ if (action === "ignore") {
126
+ return ` ON CONFLICT${target} DO NOTHING`;
127
+ }
128
+ const sets = action.updateColumns.map((c) => `${c} = ${excludedRef(c)}`);
129
+ return ` ON CONFLICT${target} DO UPDATE SET ${sets.join(", ")}`;
130
+ }
131
+ function guardInsertText(table, columns, placeholders, form) {
132
+ const head = form === "insertIgnore" ? "INSERT IGNORE INTO" : "INSERT INTO";
133
+ const tail = form === "trailingDoNothing" ? " ON CONFLICT DO NOTHING" : "";
134
+ return `${head} ${table} (${columns.join(", ")}) VALUES (${placeholders})${tail}`;
135
+ }
136
+ var SQLITE = Object.freeze({
137
+ name: "sqlite",
138
+ ignoreIsTrailingClause: false,
139
+ insertVerb(action) {
140
+ return action === "ignore" ? "INSERT OR IGNORE INTO" : "INSERT INTO";
141
+ },
142
+ insertConflictClause(conflictColumns, action) {
143
+ if (action === "ignore") return "";
144
+ return onConflictClause(conflictColumns, action, (c) => `excluded.${c}`);
145
+ },
146
+ guardInsert(table, columns, placeholders) {
147
+ return guardInsertText(table, columns, placeholders, "trailingDoNothing");
148
+ },
149
+ finalizePlaceholders(sql) {
150
+ return sql;
151
+ },
152
+ orderByNulls(expr, dir, nulls) {
153
+ return `${expr} ${dir} NULLS ${nulls}`;
154
+ }
155
+ });
156
+ var POSTGRES = Object.freeze({
157
+ name: "postgres",
158
+ ignoreIsTrailingClause: true,
159
+ insertVerb(_action) {
160
+ return "INSERT INTO";
161
+ },
162
+ insertConflictClause(conflictColumns, action) {
163
+ return onConflictClause(conflictColumns, action, (c) => `EXCLUDED.${c}`);
164
+ },
165
+ guardInsert(table, columns, placeholders) {
166
+ return guardInsertText(table, columns, placeholders, "trailingDoNothing");
167
+ },
168
+ finalizePlaceholders(sql) {
169
+ return toDollarPlaceholders(sql);
170
+ },
171
+ orderByNulls(expr, dir, nulls) {
172
+ return `${expr} ${dir} NULLS ${nulls}`;
173
+ }
174
+ });
175
+ var MYSQL = Object.freeze({
176
+ name: "mysql",
177
+ ignoreIsTrailingClause: false,
178
+ insertVerb(action) {
179
+ return action === "ignore" ? "INSERT IGNORE INTO" : "INSERT INTO";
180
+ },
181
+ insertConflictClause(_conflictColumns, action) {
182
+ if (action === "ignore") return "";
183
+ const sets = action.updateColumns.map((c) => `${c} = VALUES(${c})`);
184
+ return ` ON DUPLICATE KEY UPDATE ${sets.join(", ")}`;
185
+ },
186
+ guardInsert(table, columns, placeholders) {
187
+ return guardInsertText(table, columns, placeholders, "insertIgnore");
188
+ },
189
+ finalizePlaceholders(sql) {
190
+ return sql;
191
+ },
192
+ orderByNulls(expr, dir, nulls) {
193
+ const flagDir = nulls === "FIRST" ? "DESC" : "ASC";
194
+ return `${expr} IS NULL ${flagDir}, ${expr} ${dir}`;
195
+ }
196
+ });
197
+ var DIALECTS = Object.freeze({
198
+ sqlite: SQLITE,
199
+ postgres: POSTGRES,
200
+ mysql: MYSQL
201
+ });
202
+ function dialectFor(name) {
203
+ const d = DIALECTS[name];
204
+ if (d === void 0) {
205
+ throw new Error(`scp dialect: unknown dialect '${String(name)}' (known: ${Object.keys(DIALECTS).join(", ")})`);
206
+ }
207
+ return d;
208
+ }
209
+
210
+ // src/scp/compile-dialect.ts
211
+ var assembly = (shape) => ({ shape });
212
+ function compileInsertFor(dialect, desc) {
213
+ const columns = Object.keys(desc.values);
214
+ if (columns.length === 0) throw new Error("INSERT requires at least one value column");
215
+ const placeholders = columns.map(() => "?").join(", ");
216
+ const params = columns.map((c) => desc.values[c]);
217
+ const conflictAction = resolveConflictAction(desc, columns);
218
+ const verb = dialect.insertVerb(conflictAction);
219
+ let sql = `${verb} ${desc.table} (${columns.join(", ")}) VALUES (${placeholders})`;
220
+ if (conflictAction !== void 0) {
221
+ sql += dialect.insertConflictClause(desc.onConflict ?? [], conflictAction);
222
+ }
223
+ if (desc.returning !== void 0) sql += ` RETURNING ${desc.returning.join(", ")}`;
224
+ return { component: "Insert", sql, where: null, params, assembly: assembly("items") };
225
+ }
226
+ function resolveConflictAction(desc, columns) {
227
+ if (desc.onConflict === void 0 || desc.onConflictAction === void 0) return void 0;
228
+ if (desc.onConflictAction === "ignore") return "ignore";
229
+ const cols = desc.onConflictAction.updateColumns === "all" ? columns : desc.onConflictAction.updateColumns;
230
+ return { updateColumns: cols };
231
+ }
232
+
233
+ // src/scp/compile-sqlite.ts
234
+ function lowerCondition(c) {
235
+ if (c.kind === "group") {
236
+ const tree = {
237
+ connector: c.connector,
238
+ fragments: c.conditions.map(lowerCondition)
239
+ };
240
+ if (c.skipWhen !== void 0) {
241
+ throw new Error("a conditional (SKIP) group is not supported in WS1; guard the member conditions individually");
242
+ }
243
+ return tree;
244
+ }
245
+ let f;
246
+ switch (c.kind) {
247
+ case "eq":
248
+ f = { always: true, sql: `${c.column} = ?`, params: [c.value] };
249
+ break;
250
+ case "cmp":
251
+ f = { always: true, sql: `${c.column} ${c.op} ?`, params: [c.value] };
252
+ break;
253
+ case "isNull":
254
+ f = { always: true, sql: `${c.column} IS NULL`, params: [] };
255
+ break;
256
+ case "in":
257
+ f = { always: true, sql: `${c.column} IN (?)`, params: [c.value], expand: 0 };
258
+ break;
259
+ }
260
+ if (c.skipWhen !== void 0) {
261
+ delete f.always;
262
+ f.when = c.skipWhen;
263
+ }
264
+ return f;
265
+ }
266
+ function lowerWhere(conditions) {
267
+ if (conditions === void 0 || conditions.length === 0) return null;
268
+ return { connector: "AND", fragments: conditions.map(lowerCondition) };
269
+ }
270
+ var assembly2 = (shape) => ({ shape });
271
+ function compileSelect(desc) {
272
+ const cols = desc.select.length === 1 && desc.select[0] === "*" ? "*" : desc.select.join(", ");
273
+ let sql = `SELECT ${cols} FROM ${desc.table}`;
274
+ const where = lowerWhere(desc.where);
275
+ if (where !== null) sql += WHERE_SLOT;
276
+ if (desc.group !== void 0) sql += ` GROUP BY ${desc.group}`;
277
+ if (desc.order !== void 0) sql += ` ORDER BY ${desc.order}`;
278
+ const params = [];
279
+ if (desc.limit !== void 0) {
280
+ sql += ` LIMIT ?`;
281
+ params.push(desc.limit);
282
+ }
283
+ if (desc.offset !== void 0) {
284
+ sql += ` OFFSET ?`;
285
+ params.push(desc.offset);
286
+ }
287
+ return { component: "Select", sql, where, params, assembly: assembly2("items") };
288
+ }
289
+ function compileInsert(desc) {
290
+ return compileInsertFor(SQLITE, desc);
291
+ }
292
+ function compileUpdate(desc) {
293
+ const setCols = Object.keys(desc.set);
294
+ if (setCols.length === 0) throw new Error("UPDATE requires at least one SET column");
295
+ const setClauses = setCols.map((c) => `${c} = ?`);
296
+ const params = setCols.map((c) => desc.set[c]);
297
+ const where = lowerWhere(desc.where);
298
+ if (where === null) throw new Error("UPDATE requires conditions");
299
+ let sql = `UPDATE ${desc.table} SET ${setClauses.join(", ")}${WHERE_SLOT}`;
300
+ if (desc.returning !== void 0) sql += ` RETURNING ${desc.returning.join(", ")}`;
301
+ return { component: "Update", sql, where, params, assembly: assembly2("items") };
302
+ }
303
+ function compileDelete(desc) {
304
+ const where = lowerWhere(desc.where);
305
+ if (where === null) throw new Error("DELETE requires conditions");
306
+ let sql = `DELETE FROM ${desc.table}${WHERE_SLOT}`;
307
+ if (desc.returning !== void 0) sql += ` RETURNING ${desc.returning.join(", ")}`;
308
+ return { component: "Delete", sql, where, params: [], assembly: assembly2("items") };
309
+ }
310
+
311
+ // src/scp/render.ts
312
+ import { evaluateExpression } from "behavior-contracts";
313
+ function isTree(node) {
314
+ return "connector" in node;
315
+ }
316
+ function fragmentPresent(f, scope) {
317
+ if (f.always === true) return true;
318
+ if (f.when === void 0) return false;
319
+ const v = evaluateExpression(f.when, scope);
320
+ return v !== null && v !== false;
321
+ }
322
+ function renderFragment(f, scope, params) {
323
+ if (f.expand === void 0) {
324
+ for (const slot of f.params) params.push(evaluateExpression(slot, scope));
325
+ return f.sql;
326
+ }
327
+ let sql = f.sql;
328
+ for (let i = 0; i < f.params.length; i++) {
329
+ const v = evaluateExpression(f.params[i], scope);
330
+ if (i === f.expand) {
331
+ if (!Array.isArray(v)) {
332
+ throw new Error(`IN-list expansion slot ${i} did not bind to an array (got ${v === null ? "null" : typeof v})`);
333
+ }
334
+ if (v.length === 0) {
335
+ sql = "1 = 0";
336
+ } else {
337
+ sql = sql.replace("(?)", `(${v.map(() => "?").join(", ")})`);
338
+ for (const el of v) params.push(el);
339
+ }
340
+ } else {
341
+ params.push(v);
342
+ }
343
+ }
344
+ return sql;
345
+ }
346
+ function renderTree(tree, scope, params) {
347
+ const parts = [];
348
+ for (const node of tree.fragments) {
349
+ if (isTree(node)) {
350
+ const inner = renderTree(node, scope, params);
351
+ if (inner !== "") parts.push(`(${inner})`);
352
+ } else if (fragmentPresent(node, scope)) {
353
+ parts.push(renderFragment(node, scope, params));
354
+ }
355
+ }
356
+ if (parts.length === 0) return "";
357
+ return parts.join(` ${tree.connector} `);
358
+ }
359
+ function renderOperation(op, input, dialect = SQLITE) {
360
+ const params = [];
361
+ const markerIdx = op.sql.indexOf(WHERE_SLOT);
362
+ if (markerIdx === -1) {
363
+ for (const slot of op.params) params.push(evaluateExpression(slot, input));
364
+ return { sql: finalize(op.sql, dialect), params };
365
+ }
366
+ const before = op.sql.slice(0, markerIdx);
367
+ const after = op.sql.slice(markerIdx + WHERE_SLOT.length);
368
+ const beforeQ = countPlaceholders(before);
369
+ const preStatics = op.params.slice(0, beforeQ);
370
+ const postStatics = op.params.slice(beforeQ);
371
+ for (const slot of preStatics) params.push(evaluateExpression(slot, input));
372
+ let whereSql = "";
373
+ if (op.where !== null) {
374
+ const body = renderTree(op.where, input, params);
375
+ if (body !== "") whereSql = ` WHERE ${body}`;
376
+ }
377
+ for (const slot of postStatics) params.push(evaluateExpression(slot, input));
378
+ return { sql: finalize(before + whereSql + after, dialect), params };
379
+ }
380
+ function finalize(sql, dialect) {
381
+ return dialect.finalizePlaceholders(sql);
382
+ }
383
+ function countPlaceholders(sql) {
384
+ let n = 0;
385
+ for (const ch of sql) if (ch === "?") n++;
386
+ return n;
387
+ }
388
+
389
+ // src/scp/guard.ts
390
+ import { assertPortable, FORBIDDEN_OBJECT_KEY, PORTABLE_EXPR_OPERATORS, PortabilityError } from "behavior-contracts";
391
+ function assertExprPortable(node, path = "$") {
392
+ assertPortable(node, path);
393
+ walkExpr(node, path);
394
+ }
395
+ function walkExpr(node, path) {
396
+ if (node === null || typeof node !== "object") return;
397
+ if (Array.isArray(node)) {
398
+ node.forEach((el, i) => walkExpr(el, `${path}[${i}]`));
399
+ return;
400
+ }
401
+ const keys = Object.keys(node);
402
+ if (keys.length === 1) {
403
+ const op = keys[0];
404
+ if (!PORTABLE_EXPR_OPERATORS.has(op)) {
405
+ throw new PortabilityError(path, `Portability Guard: '${op}' is not a portable Expression IR operator (closed set: ${[...PORTABLE_EXPR_OPERATORS].join(", ")})`);
406
+ }
407
+ const arg = node[op];
408
+ if (op === "obj") {
409
+ if (arg === null || typeof arg !== "object" || Array.isArray(arg)) {
410
+ throw new PortabilityError(`${path}.obj`, `Portability Guard: {obj: ...} expects an object (at ${path}.obj)`);
411
+ }
412
+ for (const [k, v] of Object.entries(arg)) {
413
+ if (k === FORBIDDEN_OBJECT_KEY) {
414
+ throw new PortabilityError(`${path}.obj`, `Portability Guard: object key "${FORBIDDEN_OBJECT_KEY}" is forbidden (fail-closed) at ${path}.obj`);
415
+ }
416
+ walkExpr(v, `${path}.obj.${k}`);
417
+ }
418
+ return;
419
+ }
420
+ walkExpr(arg, `${path}.${op}`);
421
+ return;
422
+ }
423
+ for (const k of keys) walkExpr(node[k], `${path}.${k}`);
424
+ }
425
+ function assertFragmentPortable(node, path) {
426
+ if ("connector" in node) {
427
+ node.fragments.forEach((f, i) => assertFragmentPortable(f, `${path}.fragments[${i}]`));
428
+ return;
429
+ }
430
+ if (node.when !== void 0) assertExprPortable(node.when, `${path}.when`);
431
+ node.params.forEach((p, i) => assertExprPortable(p, `${path}.params[${i}]`));
432
+ }
433
+ function assertOperationPortable(op) {
434
+ op.params.forEach((p, i) => assertExprPortable(p, `$.params[${i}]`));
435
+ if (op.where !== null) assertFragmentPortable(op.where, "$.where");
436
+ }
437
+ function assertComponentPortable(component) {
438
+ const at = `component '${component.name}'`;
439
+ for (const n of component.body) {
440
+ if ("map" in n) {
441
+ assertExprPortable(n.map.over, `${at}/${n.id}.map.over`);
442
+ assertExprPortable(n.map.ports, `${at}/${n.id}.map.ports`);
443
+ if (n.map.when !== void 0) assertExprPortable(n.map.when, `${at}/${n.id}.map.when`);
444
+ } else if ("cond" in n) {
445
+ assertExprPortable(n.cond.if, `${at}/${n.id}.cond.if`);
446
+ assertExprPortable(n.cond.then, `${at}/${n.id}.cond.then`);
447
+ assertExprPortable(n.cond.else, `${at}/${n.id}.cond.else`);
448
+ } else {
449
+ assertExprPortable(n.ports, `${at}/${n.id}.ports`);
450
+ }
451
+ }
452
+ assertExprPortable(component.output, `${at}.output`);
453
+ }
454
+ function assertComponentGraphPortable(ir) {
455
+ for (const c of ir.components) assertComponentPortable(c);
456
+ }
457
+
458
+ // src/scp/authoring.ts
459
+ import {
460
+ catalogComponents,
461
+ compileBehaviors,
462
+ SemanticBehavior
463
+ } from "behavior-contracts";
464
+ var boundComponents;
465
+ function components() {
466
+ if (boundComponents === void 0) {
467
+ boundComponents = catalogComponents(LITEDBMODEL_CATALOG);
468
+ }
469
+ return boundComponents;
470
+ }
471
+ function lowerBehaviorClass(cls, options) {
472
+ const ir = compileBehaviors(cls, {
473
+ ...options.inputPorts !== void 0 ? { inputPorts: options.inputPorts } : {},
474
+ ...options.concurrency !== void 0 ? { concurrency: options.concurrency } : {}
475
+ });
476
+ assertComponentsInCatalog(ir.components);
477
+ assertComponentGraphPortable(ir);
478
+ const methods = {};
479
+ for (const component of ir.components) {
480
+ methods[component.name] = {
481
+ name: component.name,
482
+ effect: deriveContractEffect(component),
483
+ component
484
+ };
485
+ }
486
+ return {
487
+ className: cls.name ?? "<anonymous>",
488
+ ir,
489
+ components: ir.components,
490
+ methods
491
+ };
492
+ }
493
+ function publishBehaviors(cls, options = {}) {
494
+ return lowerBehaviorClass(cls, options);
495
+ }
496
+ function compileEager(name, fn, options = {}) {
497
+ const cls = makeEagerClass(name, fn);
498
+ return lowerBehaviorClass(cls, options);
499
+ }
500
+ function makeEagerClass(name, fn) {
501
+ const L = components();
502
+ const cls = class extends SemanticBehavior {
503
+ };
504
+ const method = function($) {
505
+ return fn($, L);
506
+ };
507
+ Object.defineProperty(method, "toString", { value: () => fn.toString(), configurable: true });
508
+ Object.defineProperty(cls.prototype, name, {
509
+ value: method,
510
+ writable: true,
511
+ enumerable: false,
512
+ configurable: true
513
+ });
514
+ Object.defineProperty(cls, "name", { value: name, configurable: true });
515
+ return cls;
516
+ }
517
+
518
+ // src/scp/bridge.ts
519
+ var IN_SENTINEL = "@in";
520
+ function isMap(n) {
521
+ return "map" in n;
522
+ }
523
+ function nodeRef(n) {
524
+ return isMap(n) ? { component: n.map.component, ports: n.map.ports } : { component: n.component, ports: n.ports };
525
+ }
526
+ function stringPort(ports, name) {
527
+ const v = ports[name];
528
+ if (v === void 0) return void 0;
529
+ if (typeof v !== "string") {
530
+ throw new Error(`bridge: port '${name}' must be a literal string in the IR (got ${JSON.stringify(v)})`);
531
+ }
532
+ return v;
533
+ }
534
+ function arrPort(ports, name) {
535
+ const v = ports[name];
536
+ if (v === void 0) return void 0;
537
+ if (typeof v === "object" && v !== null && "arr" in v && Array.isArray(v.arr)) {
538
+ return v.arr;
539
+ }
540
+ throw new Error(`bridge: port '${name}' must be an {arr:[...]} literal in the IR (got ${JSON.stringify(v)})`);
541
+ }
542
+ function stringArrayPort(ports, name) {
543
+ const arr = arrPort(ports, name);
544
+ if (arr === void 0) return void 0;
545
+ return arr.map((e) => {
546
+ if (typeof e !== "string") throw new Error(`bridge: '${name}' entries must be literal strings (got ${JSON.stringify(e)})`);
547
+ return e;
548
+ });
549
+ }
550
+ function opKey(node) {
551
+ if (node === null || typeof node !== "object" || Array.isArray(node)) return void 0;
552
+ const keys = Object.keys(node);
553
+ return keys.length === 1 ? keys[0] : void 0;
554
+ }
555
+ function columnOf(node, ctx) {
556
+ const op = opKey(node);
557
+ if (op !== "ref" && op !== "refOpt") {
558
+ throw new Error(`bridge: ${ctx}: the column operand must be a {ref:[...]} / {refOpt:[...]} path, got ${JSON.stringify(node)}`);
559
+ }
560
+ const path = node[op];
561
+ if (!Array.isArray(path) || path.length === 0 || typeof path[path.length - 1] !== "string") {
562
+ throw new Error(`bridge: ${ctx}: column ref path must be a non-empty string path`);
563
+ }
564
+ return path[path.length - 1];
565
+ }
566
+ var CMP_OPS = {
567
+ lt: "<",
568
+ le: "<=",
569
+ gt: ">",
570
+ ge: ">=",
571
+ ne: "<>"
572
+ };
573
+ function inSentinelColumn(node) {
574
+ const op = opKey(node);
575
+ if (op !== "ref" && op !== "refOpt") return void 0;
576
+ const path = node[op];
577
+ if (Array.isArray(path) && path.length >= 2 && path[0] === IN_SENTINEL && typeof path[path.length - 1] === "string") {
578
+ return path[path.length - 1];
579
+ }
580
+ return void 0;
581
+ }
582
+ function lowerWhereMember(node, at) {
583
+ const op = opKey(node);
584
+ if (op === void 0) {
585
+ throw new Error(`bridge: ${at}: a where member must be a single-operator Expression node, got ${JSON.stringify(node)}`);
586
+ }
587
+ if (op === "cond") {
588
+ const args = node.cond;
589
+ if (!Array.isArray(args) || args.length !== 3 || args[2] !== null) {
590
+ throw new Error(`bridge: ${at}: a SKIP-optional condition must be {cond:[c, <member>, null]} (else = null)`);
591
+ }
592
+ const inner = lowerWhereMember(args[1], `${at}.cond.then`);
593
+ if (inner.skipWhen !== void 0) {
594
+ throw new Error(`bridge: ${at}: nested SKIP guards are not supported (one cond per member)`);
595
+ }
596
+ return { ...inner, skipWhen: args[0] };
597
+ }
598
+ if (op === "and" || op === "or") {
599
+ const args = node[op];
600
+ if (!Array.isArray(args) || args.length < 2) {
601
+ throw new Error(`bridge: ${at}: '${op}' group expects >= 2 members`);
602
+ }
603
+ return {
604
+ kind: "group",
605
+ connector: op === "and" ? "AND" : "OR",
606
+ conditions: args.map((m, i) => lowerWhereMember(m, `${at}.${op}[${i}]`))
607
+ };
608
+ }
609
+ if (op === "eq") {
610
+ const [col, val] = binOperands(node, op, at);
611
+ const inCol = inSentinelColumn(col);
612
+ if (inCol !== void 0) {
613
+ return { kind: "in", column: inCol, value: val };
614
+ }
615
+ if (val === null) {
616
+ return { kind: "isNull", column: columnOf(col, at) };
617
+ }
618
+ return { kind: "eq", column: columnOf(col, at), value: val };
619
+ }
620
+ if (op in CMP_OPS) {
621
+ const [col, val] = binOperands(node, op, at);
622
+ return { kind: "cmp", column: columnOf(col, at), op: CMP_OPS[op], value: val };
623
+ }
624
+ throw new Error(`bridge: ${at}: unsupported where operator '${op}' (supported: eq/ne/lt/le/gt/ge/and/or/cond; IN via ${IN_SENTINEL} column head; IS NULL via eq(col,null))`);
625
+ }
626
+ function binOperands(node, op, at) {
627
+ const args = node[op];
628
+ if (!Array.isArray(args) || args.length !== 2) {
629
+ throw new Error(`bridge: ${at}: '${op}' expects exactly 2 operands`);
630
+ }
631
+ return [args[0], args[1]];
632
+ }
633
+ function lowerWherePort(ports, at) {
634
+ const arr = arrPort(ports, "where");
635
+ if (arr === void 0) return void 0;
636
+ return arr.map((m, i) => lowerWhereMember(m, `${at}.where[${i}]`));
637
+ }
638
+ function collectFamily(ports, prefix) {
639
+ const out = {};
640
+ for (const k of Object.keys(ports)) {
641
+ if (k.startsWith(`${prefix}.`)) out[k.slice(prefix.length + 1)] = ports[k];
642
+ }
643
+ return out;
644
+ }
645
+ function conflictOf(ports) {
646
+ const onConflict = stringPort(ports, "onConflict");
647
+ const action = stringPort(ports, "onConflictAction");
648
+ if (onConflict === void 0) return {};
649
+ const keys = onConflict.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
650
+ if (action === void 0 || action === "ignore") {
651
+ return { onConflict: keys, onConflictAction: "ignore" };
652
+ }
653
+ if (action === "update") {
654
+ return { onConflict: keys, onConflictAction: { updateColumns: "all" } };
655
+ }
656
+ const m = /^update:(.+)$/.exec(action);
657
+ if (m) {
658
+ return { onConflict: keys, onConflictAction: { updateColumns: m[1].split(",").map((s) => s.trim()) } };
659
+ }
660
+ throw new Error(`bridge: unsupported onConflictAction '${action}' (expected 'ignore' / 'update' / 'update:col,col')`);
661
+ }
662
+ function returningOf(ports) {
663
+ const r = stringPort(ports, "returning");
664
+ if (r === void 0) return void 0;
665
+ return r.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
666
+ }
667
+ function compileNode(node, dialect = SQLITE) {
668
+ const { component, ports } = nodeRef(node);
669
+ const table = stringPort(ports, "table");
670
+ if (table === void 0) throw new Error(`bridge: ${component} node requires a literal 'table' port`);
671
+ const at = `${component}`;
672
+ switch (component) {
673
+ case "Select": {
674
+ const select = stringArrayPort(ports, "select");
675
+ if (select === void 0) throw new Error(`bridge: Select requires a 'select' {arr:[...]} port`);
676
+ const desc = { table, select };
677
+ const where = lowerWherePort(ports, at);
678
+ if (where !== void 0) desc.where = where;
679
+ const order = stringPort(ports, "order");
680
+ if (order !== void 0) desc.order = order;
681
+ const group = stringPort(ports, "group");
682
+ if (group !== void 0) desc.group = group;
683
+ if (ports.limit !== void 0) desc.limit = ports.limit;
684
+ if (ports.offset !== void 0) desc.offset = ports.offset;
685
+ return compileSelect(desc);
686
+ }
687
+ case "Insert": {
688
+ const values = collectFamily(ports, "values");
689
+ if (Object.keys(values).length === 0) throw new Error(`bridge: Insert requires at least one 'values.<field>' port`);
690
+ const desc = { table, values, ...conflictOf(ports) };
691
+ const returning = returningOf(ports);
692
+ if (returning !== void 0) desc.returning = returning;
693
+ return compileInsertFor(dialect, desc);
694
+ }
695
+ case "Update": {
696
+ const set = collectFamily(ports, "set");
697
+ if (Object.keys(set).length === 0) throw new Error(`bridge: Update requires at least one 'set.<field>' port`);
698
+ const where = lowerWherePort(ports, at);
699
+ if (where === void 0) throw new Error(`bridge: Update requires a 'where' port`);
700
+ const desc = { table, set, where };
701
+ const returning = returningOf(ports);
702
+ if (returning !== void 0) desc.returning = returning;
703
+ return compileUpdate(desc);
704
+ }
705
+ case "Delete": {
706
+ const where = lowerWherePort(ports, at);
707
+ if (where === void 0) throw new Error(`bridge: Delete requires a 'where' port`);
708
+ const desc = { table, where };
709
+ const returning = returningOf(ports);
710
+ if (returning !== void 0) desc.returning = returning;
711
+ return compileDelete(desc);
712
+ }
713
+ default:
714
+ throw new Error(`bridge: catalog component '${component}' has no Backend-Compile (SQL CRUD only: Select/Insert/Update/Delete)`);
715
+ }
716
+ }
717
+ function compileComponentNodes(component, dialect = SQLITE) {
718
+ const out = /* @__PURE__ */ new Map();
719
+ for (const n of component.body) {
720
+ if ("cond" in n) continue;
721
+ out.set(n.id, compileNode(n, dialect));
722
+ }
723
+ return out;
724
+ }
725
+
726
+ // src/scp/authoring-sql.ts
727
+ import { eq, ne, lt, le, gt, ge } from "behavior-contracts";
728
+ function whereEq(col, value) {
729
+ return eq(col, value);
730
+ }
731
+ function whereNe(col, value) {
732
+ return ne(col, value);
733
+ }
734
+ function whereLt(col, value) {
735
+ return lt(col, value);
736
+ }
737
+ function whereLe(col, value) {
738
+ return le(col, value);
739
+ }
740
+ function whereGt(col, value) {
741
+ return gt(col, value);
742
+ }
743
+ function whereGe(col, value) {
744
+ return ge(col, value);
745
+ }
746
+ function whereIsNull(col) {
747
+ return eq(col, null);
748
+ }
749
+ function whereIn(col, value) {
750
+ return eq(col, value);
751
+ }
752
+ function inColumn($, name) {
753
+ return $[IN_SENTINEL][name];
754
+ }
755
+
756
+ // src/scp/errors.ts
757
+ var SqlFailure = class extends Error {
758
+ constructor(kind, policy, sqliteCode, message) {
759
+ super(message);
760
+ this.kind = kind;
761
+ this.policy = policy;
762
+ this.sqliteCode = sqliteCode;
763
+ this.name = "SqlFailure";
764
+ }
765
+ kind;
766
+ policy;
767
+ sqliteCode;
768
+ };
769
+ function isSqliteError(e) {
770
+ return typeof e === "object" && e !== null && typeof e.code === "string" && e.code.startsWith("SQLITE_");
771
+ }
772
+ function mapSqliteError(e) {
773
+ if (!isSqliteError(e)) {
774
+ const message2 = e instanceof Error ? e.message : String(e);
775
+ return new SqlFailure("driver_error", "fail", void 0, `non-SQLite driver error: ${message2}`);
776
+ }
777
+ const { code, message } = e;
778
+ const tagged = `[${code}] ${message}`;
779
+ if (code === "SQLITE_CONSTRAINT_FOREIGNKEY") {
780
+ return new SqlFailure("foreign_key_violation", "fail", code, tagged);
781
+ }
782
+ if (code.startsWith("SQLITE_CONSTRAINT")) {
783
+ return new SqlFailure("constraint_violation", "fail", code, tagged);
784
+ }
785
+ if (code === "SQLITE_BUSY" || code === "SQLITE_LOCKED") {
786
+ return new SqlFailure("retryable", "retry", code, tagged);
787
+ }
788
+ return new SqlFailure("driver_error", "fail", code, tagged);
789
+ }
790
+
791
+ // src/scp/runtime.ts
792
+ import {
793
+ runBehavior
794
+ } from "behavior-contracts";
795
+
796
+ // src/scp/relation.ts
797
+ var RELATION_KEYS_HEAD = "__keys";
798
+ function compileRelationOp(decl) {
799
+ if (decl.kind !== "hasMany" && decl.limit !== void 0) {
800
+ throw new Error(`relation '${decl.name}': a per-parent 'limit' is only valid for hasMany (got ${decl.kind})`);
801
+ }
802
+ const query = decl.limit !== void 0 ? compileLimitedBatch(decl) : compilePlainBatch(decl);
803
+ return {
804
+ name: decl.name,
805
+ kind: decl.kind,
806
+ parentKey: decl.parentKey,
807
+ targetKey: decl.targetKey,
808
+ query
809
+ };
810
+ }
811
+ function keysRef() {
812
+ return { ref: [RELATION_KEYS_HEAD] };
813
+ }
814
+ function compilePlainBatch(decl) {
815
+ const cols = decl.select.join(", ");
816
+ const inFragment = {
817
+ always: true,
818
+ sql: `${decl.targetKey} IN (?)`,
819
+ params: [keysRef()],
820
+ expand: 0
821
+ };
822
+ const where = { connector: "AND", fragments: [inFragment] };
823
+ let sql = `SELECT ${cols} FROM ${decl.targetTable}${WHERE_SLOT}`;
824
+ if (decl.order !== void 0) sql += ` ORDER BY ${decl.order}`;
825
+ return { component: "Select", sql, where, params: [], assembly: { shape: "items" } };
826
+ }
827
+ function compileLimitedBatch(decl) {
828
+ const cols = decl.select.join(", ");
829
+ const limit = decl.limit;
830
+ if (!Number.isInteger(limit) || limit < 0) {
831
+ throw new Error(`relation '${decl.name}': per-parent limit must be a non-negative integer (got ${String(limit)})`);
832
+ }
833
+ if (decl.order === void 0) {
834
+ throw new Error(
835
+ `relation '${decl.name}': a per-parent 'limit' requires an explicit 'order' (the ROW_NUMBER() window needs a deterministic ordering to decide which ${limit} rows each parent keeps)`
836
+ );
837
+ }
838
+ const order = decl.order;
839
+ const inFragment = {
840
+ always: true,
841
+ sql: `${decl.targetKey} IN (?)`,
842
+ params: [keysRef()],
843
+ expand: 0
844
+ };
845
+ const where = { connector: "AND", fragments: [inFragment] };
846
+ const sql = `WITH ranked AS (SELECT ${cols}, ROW_NUMBER() OVER (PARTITION BY ${decl.targetKey} ORDER BY ${order}) AS _rn FROM ${decl.targetTable}${WHERE_SLOT}) SELECT ${cols} FROM ranked WHERE _rn <= ${limit}`;
847
+ return { component: "Select", sql, where, params: [], assembly: { shape: "items" } };
848
+ }
849
+ function runRelationOp(op, parents, db) {
850
+ const keys = dedupeKeys(parents, op.parentKey);
851
+ const batch = /* @__PURE__ */ new Map();
852
+ if (keys.length === 0) {
853
+ const rendered2 = renderOperation(op.query, { [RELATION_KEYS_HEAD]: [] });
854
+ return { sql: rendered2.sql, keys, batch };
855
+ }
856
+ const rendered = renderOperation(op.query, { [RELATION_KEYS_HEAD]: keys });
857
+ const rows = db.prepare(rendered.sql).all(...rendered.params);
858
+ for (const row of rows) {
859
+ const k = String(row[op.targetKey]);
860
+ const list = batch.get(k);
861
+ if (list === void 0) batch.set(k, [row]);
862
+ else list.push(row);
863
+ }
864
+ return { sql: rendered.sql, keys, batch };
865
+ }
866
+ function dedupeKeys(parents, parentKey) {
867
+ const seen = /* @__PURE__ */ new Set();
868
+ const out = [];
869
+ for (const p of parents) {
870
+ const v = p[parentKey];
871
+ if (v === void 0 || v === null) continue;
872
+ const s = String(v);
873
+ if (seen.has(s)) continue;
874
+ seen.add(s);
875
+ out.push(v);
876
+ }
877
+ return out;
878
+ }
879
+ function distributeToParent(op, parent, batch) {
880
+ const key = parent[op.parentKey];
881
+ const rows = key === void 0 || key === null ? void 0 : batch.get(String(key));
882
+ if (op.kind === "hasMany") return rows ?? [];
883
+ return rows !== void 0 && rows.length > 0 ? rows[0] : null;
884
+ }
885
+
886
+ // src/scp/typed-object.ts
887
+ var RELATION_CONTEXT = /* @__PURE__ */ Symbol("litedbmodel:relation-context");
888
+ var RelationContext = class {
889
+ siblings;
890
+ ops;
891
+ db;
892
+ hydrate;
893
+ /** Per-relation resolved batch (name → grouping), memoized: one batch query per relation. */
894
+ resolved = /* @__PURE__ */ new Map();
895
+ constructor(siblings, ops, db, hydrate) {
896
+ this.siblings = siblings;
897
+ this.ops = ops;
898
+ this.db = db;
899
+ this.hydrate = hydrate;
900
+ }
901
+ /**
902
+ * Resolve `relationName` for `parent` (lazy access). The relation is batch-loaded across
903
+ * ALL siblings on first access and memoized, so N sibling accesses issue ONE query. Returns
904
+ * the hydrated child(ren) per cardinality (`hasMany` → array, else single-or-null).
905
+ */
906
+ resolve(relationName, parent) {
907
+ const op = this.ops[relationName];
908
+ if (op === void 0) {
909
+ throw new Error(`relation '${relationName}' is not declared on this result set`);
910
+ }
911
+ let batch = this.resolved.get(relationName);
912
+ if (batch === void 0) {
913
+ batch = runRelationOp(op, this.siblings, this.db).batch;
914
+ this.resolved.set(relationName, batch);
915
+ }
916
+ return applyHydrate(distributeToParent(op, parent, batch), this.hydrate);
917
+ }
918
+ };
919
+ function readRelationContext(o) {
920
+ const v = o[RELATION_CONTEXT];
921
+ return v instanceof RelationContext ? v : void 0;
922
+ }
923
+ function applyHydrate(value, hydrate) {
924
+ if (hydrate === void 0) return value;
925
+ if (value === null) return null;
926
+ if (Array.isArray(value)) return value.map((v) => v === null ? null : hydrate(v));
927
+ return hydrate(value);
928
+ }
929
+ function buildResultSet(rawRows, ops, db, options = {}) {
930
+ const selected = options.with ?? {};
931
+ const hydrate = options.hydrate;
932
+ const proto = {};
933
+ for (const name of Object.keys(ops)) {
934
+ if (selected[name] === true) continue;
935
+ Object.defineProperty(proto, name, {
936
+ enumerable: false,
937
+ configurable: true,
938
+ get() {
939
+ const ctx2 = readRelationContext(this);
940
+ if (ctx2 === void 0) {
941
+ return Promise.reject(
942
+ new Error(
943
+ `lazy relation '${name}': the batch context was dropped (spread/clone/JSON); re-read through the query API or declaratively select it via { with: { ${name}: true } }`
944
+ )
945
+ );
946
+ }
947
+ return Promise.resolve(ctx2.resolve(name, this));
948
+ }
949
+ });
950
+ }
951
+ const rows = rawRows.map((raw) => {
952
+ const o = Object.create(proto);
953
+ for (const k of Object.keys(raw)) o[k] = raw[k];
954
+ return o;
955
+ });
956
+ const ctx = new RelationContext(rows, ops, db, hydrate);
957
+ for (const o of rows) {
958
+ Object.defineProperty(o, RELATION_CONTEXT, {
959
+ value: ctx,
960
+ enumerable: false,
961
+ writable: false,
962
+ configurable: false
963
+ });
964
+ }
965
+ for (const name of Object.keys(selected)) {
966
+ const op = ops[name];
967
+ if (op === void 0) throw new Error(`declarative select: relation '${name}' is not declared on this model`);
968
+ const { batch } = runRelationOp(op, rows, db);
969
+ for (const o of rows) {
970
+ o[name] = applyHydrate(distributeToParent(op, o, batch), hydrate);
971
+ }
972
+ }
973
+ if (hydrate === void 0) return rows;
974
+ return rows.map((o) => hydrate(o));
975
+ }
976
+
977
+ // src/scp/writes.ts
978
+ var ENTITY_ROOT = "__entity";
979
+ var PATH_RE = /^\$\.(input|entity)\.([A-Za-z_][A-Za-z0-9_]*)$/;
980
+ var REF_PATH_RE = /^\$\.ref\.([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)$/;
981
+ function parseEffectPath(value) {
982
+ const m = PATH_RE.exec(value);
983
+ if (m !== null) return { root: m[1], field: m[2] };
984
+ const r = REF_PATH_RE.exec(value);
985
+ if (r !== null) return { root: "ref", writeName: r[1], field: r[2] };
986
+ throw new Error(
987
+ `write-time relations: '${value}' is not a valid path-rooted value; use '$.input.<field>' (the Command input), '$.entity.<field>' (the sole written row), or '$.ref.<writeName>.<field>' (a named upstream write's RETURNING row).`
988
+ );
989
+ }
990
+ function assertPaths(map) {
991
+ for (const v of Object.values(map)) parseEffectPath(v);
992
+ }
993
+ var recorder = {
994
+ lifecycle(effects = {}) {
995
+ for (const r of effects.requires ?? []) assertPaths(r.keys);
996
+ if (effects.idempotency !== void 0) parseEffectPath(effects.idempotency.token);
997
+ for (const u of effects.unique ?? []) {
998
+ u.scope.forEach((p) => parseEffectPath(p));
999
+ u.fields.forEach((p) => parseEffectPath(p));
1000
+ }
1001
+ for (const d of effects.derive ?? []) assertPaths(d.keys);
1002
+ for (const e of effects.edges ?? []) {
1003
+ assertPaths(e.columns);
1004
+ if (e.where !== void 0) assertPaths(e.where);
1005
+ }
1006
+ for (const em of effects.emits ?? []) assertPaths(em.payload);
1007
+ return { effects: freezeEffects(effects) };
1008
+ },
1009
+ exists(table, keys) {
1010
+ return { kind: "requires", table, keys };
1011
+ },
1012
+ unique(spec) {
1013
+ return { kind: "unique", name: spec.name, guardTable: spec.guardTable, scope: spec.scope, fields: spec.fields };
1014
+ },
1015
+ increment(table, keys, attribute, amount) {
1016
+ if (!Number.isFinite(amount)) {
1017
+ throw new Error(`entityWrites: increment amount for '${table}.${attribute}' must be a finite number (got ${String(amount)})`);
1018
+ }
1019
+ return { kind: "derive", table, keys, attribute, amount };
1020
+ },
1021
+ edge(spec) {
1022
+ if (spec.relation === "fk" && spec.where === void 0) {
1023
+ throw new Error(`entityWrites: a 'fk' edge on '${spec.table}' requires a 'where' key map (the matched row's identity)`);
1024
+ }
1025
+ return {
1026
+ kind: "edge",
1027
+ relation: spec.relation,
1028
+ action: spec.action,
1029
+ table: spec.table,
1030
+ columns: spec.columns,
1031
+ ...spec.where !== void 0 ? { where: spec.where } : {}
1032
+ };
1033
+ },
1034
+ event(name, outboxTable, payload) {
1035
+ return { kind: "emit", name, outboxTable, payload };
1036
+ },
1037
+ idempotentBy(table, column, token) {
1038
+ return { kind: "idempotency", table, column, token };
1039
+ }
1040
+ };
1041
+ function freezeEffects(effects) {
1042
+ const out = {};
1043
+ if (effects.requires !== void 0) out.requires = effects.requires;
1044
+ if (effects.idempotency !== void 0) out.idempotency = effects.idempotency;
1045
+ if (effects.unique !== void 0) out.unique = effects.unique;
1046
+ if (effects.derive !== void 0) out.derive = effects.derive;
1047
+ if (effects.edges !== void 0) out.edges = effects.edges;
1048
+ if (effects.emits !== void 0) out.emits = effects.emits;
1049
+ return out;
1050
+ }
1051
+ function entityWrites(builder) {
1052
+ const shape = builder(recorder);
1053
+ if (shape.create === void 0 && shape.update === void 0 && shape.remove === void 0) {
1054
+ throw new Error(
1055
+ "entityWrites(...) must declare at least one lifecycle (`create` / `update` / `remove`)."
1056
+ );
1057
+ }
1058
+ return {
1059
+ ...shape.create !== void 0 ? { create: shape.create } : {},
1060
+ ...shape.update !== void 0 ? { update: shape.update } : {},
1061
+ ...shape.remove !== void 0 ? { remove: shape.remove } : {}
1062
+ };
1063
+ }
1064
+ function edgeWrites(builder) {
1065
+ const def = entityWrites(builder);
1066
+ for (const phase of ["create", "update", "remove"]) {
1067
+ const c = def[phase];
1068
+ if (c === void 0) continue;
1069
+ const e = c.effects;
1070
+ if (e.requires !== void 0 || e.unique !== void 0 || e.derive !== void 0 || e.emits !== void 0 || e.idempotency !== void 0) {
1071
+ throw new Error(
1072
+ `edgeWrites: the '${phase}' lifecycle may declare ONLY 'edges' effects (an edge entity carries only edge writes; use entityWrites for a full save contract).`
1073
+ );
1074
+ }
1075
+ }
1076
+ return def;
1077
+ }
1078
+ function lifecycleFor(def, phase) {
1079
+ return def[phase];
1080
+ }
1081
+
1082
+ // src/scp/write-plan.ts
1083
+ function pathToRef(value) {
1084
+ const p = parseEffectPath(value);
1085
+ if (p.root === "input") return { ref: [p.field] };
1086
+ if (p.root === "entity") return { ref: [ENTITY_ROOT, p.field] };
1087
+ return { ref: [p.writeName, p.field] };
1088
+ }
1089
+ function makeIdGen() {
1090
+ let n = 0;
1091
+ return (role) => `tx_${role}_${n++}`;
1092
+ }
1093
+ function compileRequires(e, nextId) {
1094
+ const cols = Object.keys(e.keys);
1095
+ if (cols.length === 0) throw new Error(`write-plan: requires on '${e.table}' declares no keys`);
1096
+ const whereSql = cols.map((c) => `${c} = ?`).join(" AND ");
1097
+ const params = cols.map((c) => pathToRef(e.keys[c]));
1098
+ const op = {
1099
+ component: "Select",
1100
+ sql: `SELECT 1 FROM ${e.table} WHERE ${whereSql}`,
1101
+ where: null,
1102
+ params,
1103
+ assembly: { shape: "items" }
1104
+ };
1105
+ return { id: nextId("requires"), role: "gate:requires", op, gate: "existsElseRollback", label: `requires ${e.table}` };
1106
+ }
1107
+ function compileIdempotency(e, nextId, dialect) {
1108
+ const op = {
1109
+ component: "Insert",
1110
+ sql: dialect.guardInsert(e.table, [e.column], "?"),
1111
+ where: null,
1112
+ params: [pathToRef(e.token)],
1113
+ assembly: { shape: "items" }
1114
+ };
1115
+ return { id: nextId("idem"), role: "gate:idempotency", op, gate: "insertedElseNoop", label: `idempotency ${e.table}` };
1116
+ }
1117
+ function compileUnique(e, nextId, dialect) {
1118
+ const scopeCols = e.scope.map((_, i) => `s${i}`);
1119
+ const fieldCols = e.fields.map((_, i) => `f${i}`);
1120
+ const cols = ["name", ...scopeCols, ...fieldCols];
1121
+ const placeholders = cols.map(() => "?").join(", ");
1122
+ const params = [
1123
+ e.name,
1124
+ // literal discriminator (a closed-set string literal)
1125
+ ...e.scope.map(pathToRef),
1126
+ ...e.fields.map(pathToRef)
1127
+ ];
1128
+ const op = {
1129
+ component: "Insert",
1130
+ sql: dialect.guardInsert(e.guardTable, cols, placeholders),
1131
+ where: null,
1132
+ params,
1133
+ assembly: { shape: "items" }
1134
+ };
1135
+ return { id: nextId("unique"), role: "gate:unique", op, gate: "insertedElseRollback", label: `unique ${e.name}` };
1136
+ }
1137
+ function compileDerive(e, nextId) {
1138
+ const keyCols = Object.keys(e.keys);
1139
+ if (keyCols.length === 0) throw new Error(`write-plan: derive on '${e.table}' declares no keys`);
1140
+ const op = {
1141
+ component: "Update",
1142
+ sql: `UPDATE ${e.table} SET ${e.attribute} = ${e.attribute} + ?${WHERE_SLOT}`,
1143
+ where: {
1144
+ connector: "AND",
1145
+ fragments: keyCols.map((c) => ({ always: true, sql: `${c} = ?`, params: [pathToRef(e.keys[c])] }))
1146
+ },
1147
+ params: [e.amount],
1148
+ assembly: { shape: "items" }
1149
+ };
1150
+ return { id: nextId("derive"), role: "derive", op, label: `derive ${e.table}.${e.attribute}` };
1151
+ }
1152
+ function compileEdge(e, nextId) {
1153
+ if (e.relation === "m2m") {
1154
+ const cols = Object.keys(e.columns);
1155
+ if (e.action === "set") {
1156
+ const placeholders = cols.map(() => "?").join(", ");
1157
+ const op3 = {
1158
+ component: "Insert",
1159
+ sql: `INSERT INTO ${e.table} (${cols.join(", ")}) VALUES (${placeholders})`,
1160
+ where: null,
1161
+ params: cols.map((c) => pathToRef(e.columns[c])),
1162
+ assembly: { shape: "items" }
1163
+ };
1164
+ return { id: nextId("edge"), role: "edge", op: op3, label: `edge m2m link ${e.table}` };
1165
+ }
1166
+ const op2 = {
1167
+ component: "Delete",
1168
+ sql: `DELETE FROM ${e.table}${WHERE_SLOT}`,
1169
+ where: { connector: "AND", fragments: cols.map((c) => ({ always: true, sql: `${c} = ?`, params: [pathToRef(e.columns[c])] })) },
1170
+ params: [],
1171
+ assembly: { shape: "items" }
1172
+ };
1173
+ return { id: nextId("edge"), role: "edge", op: op2, label: `edge m2m unlink ${e.table}` };
1174
+ }
1175
+ const setCols = Object.keys(e.columns);
1176
+ const whereCols = Object.keys(e.where);
1177
+ const setClauses = setCols.map((c) => e.action === "set" ? `${c} = ?` : `${c} = NULL`);
1178
+ const setParams = e.action === "set" ? setCols.map((c) => pathToRef(e.columns[c])) : [];
1179
+ const op = {
1180
+ component: "Update",
1181
+ sql: `UPDATE ${e.table} SET ${setClauses.join(", ")}${WHERE_SLOT}`,
1182
+ where: { connector: "AND", fragments: whereCols.map((c) => ({ always: true, sql: `${c} = ?`, params: [pathToRef(e.where[c])] })) },
1183
+ params: setParams,
1184
+ assembly: { shape: "items" }
1185
+ };
1186
+ return { id: nextId("edge"), role: "edge", op, label: `edge fk ${e.action} ${e.table}` };
1187
+ }
1188
+ function compileEmit(e, nextId) {
1189
+ const payloadObj = {};
1190
+ for (const [k, v] of Object.entries(e.payload)) payloadObj[k] = pathToRef(v);
1191
+ const op = {
1192
+ component: "Insert",
1193
+ sql: `INSERT INTO ${e.outboxTable} (type, payload) VALUES (?, ?)`,
1194
+ where: null,
1195
+ params: [e.name, { obj: payloadObj }],
1196
+ assembly: { shape: "items" }
1197
+ };
1198
+ return { id: nextId("emit"), role: "emit", op, label: `emit ${e.name}` };
1199
+ }
1200
+ function compileWriteGroup(base, lifecycle, nextId, dialect, seqRef) {
1201
+ const e = lifecycle.effects;
1202
+ const nodes = [];
1203
+ const mk = (stmt, isGate) => {
1204
+ const consumes = refHeadsOf(stmt).filter((h) => h !== ENTITY_ROOT && !isInputHead(h, stmt));
1205
+ return { stmt, seq: seqRef.n++, consumes, produces: stmt.binds ?? null, isGate };
1206
+ };
1207
+ for (const r of e.requires ?? []) nodes.push(mk(compileRequires(r, nextId), true));
1208
+ if (e.idempotency !== void 0) nodes.push(mk(compileIdempotency(e.idempotency, nextId, dialect), true));
1209
+ for (const u of e.unique ?? []) nodes.push(mk(compileUnique(u, nextId, dialect), true));
1210
+ const bodyId = nextId("body");
1211
+ const bodyStmt = {
1212
+ id: bodyId,
1213
+ role: "body",
1214
+ op: base.op,
1215
+ label: base.label,
1216
+ ...base.name !== void 0 ? { binds: base.name } : {}
1217
+ };
1218
+ nodes.push(mk(bodyStmt, false));
1219
+ for (const d of e.derive ?? []) nodes.push(mk(compileDerive(d, nextId), false));
1220
+ for (const ed of e.edges ?? []) nodes.push(mk(compileEdge(ed, nextId), false));
1221
+ for (const em of e.emits ?? []) nodes.push(mk(compileEmit(em, nextId), false));
1222
+ return { nodes, bodyId };
1223
+ }
1224
+ function topoOrder(nodes) {
1225
+ const byName = /* @__PURE__ */ new Map();
1226
+ for (const n of nodes) {
1227
+ if (n.produces !== null) {
1228
+ if (byName.has(n.produces)) {
1229
+ throw new Error(`write-plan: two writes both bind the name '${n.produces}' \u2014 write names must be unique in a composite Command.`);
1230
+ }
1231
+ byName.set(n.produces, n);
1232
+ }
1233
+ }
1234
+ const idOf = /* @__PURE__ */ new Map();
1235
+ for (const n of nodes) idOf.set(n, n.stmt.id);
1236
+ const succ = /* @__PURE__ */ new Map();
1237
+ const indeg = /* @__PURE__ */ new Map();
1238
+ const nodeById = /* @__PURE__ */ new Map();
1239
+ for (const n of nodes) {
1240
+ nodeById.set(n.stmt.id, n);
1241
+ succ.set(n.stmt.id, /* @__PURE__ */ new Set());
1242
+ indeg.set(n.stmt.id, 0);
1243
+ }
1244
+ const addEdge = (fromId, toId) => {
1245
+ if (fromId === toId) return;
1246
+ const s = succ.get(fromId);
1247
+ if (!s.has(toId)) {
1248
+ s.add(toId);
1249
+ indeg.set(toId, indeg.get(toId) + 1);
1250
+ }
1251
+ };
1252
+ for (const n of nodes) {
1253
+ for (const dep of n.consumes) {
1254
+ const producer = byName.get(dep);
1255
+ if (producer === void 0) {
1256
+ throw new Error(
1257
+ `write-plan: statement '${n.stmt.label}' references '$.ref.${dep}.*' but no write in this Command binds the name '${dep}' \u2014 a dangling write reference (fail-closed; no silent skip).`
1258
+ );
1259
+ }
1260
+ addEdge(producer.stmt.id, n.stmt.id);
1261
+ }
1262
+ }
1263
+ const gates = nodes.filter((n) => n.isGate);
1264
+ const nonGates = nodes.filter((n) => !n.isGate);
1265
+ for (const g of gates) for (const b of nonGates) addEdge(g.stmt.id, b.stmt.id);
1266
+ const ordered = [];
1267
+ const ready = nodes.filter((n) => indeg.get(n.stmt.id) === 0).sort((a, b) => a.seq - b.seq);
1268
+ while (ready.length > 0) {
1269
+ const n = ready.shift();
1270
+ ordered.push(n.stmt);
1271
+ for (const depId of succ.get(n.stmt.id)) {
1272
+ indeg.set(depId, indeg.get(depId) - 1);
1273
+ if (indeg.get(depId) === 0) {
1274
+ const dn = nodeById.get(depId);
1275
+ let lo = 0;
1276
+ while (lo < ready.length && ready[lo].seq < dn.seq) lo++;
1277
+ ready.splice(lo, 0, dn);
1278
+ }
1279
+ }
1280
+ }
1281
+ if (ordered.length !== nodes.length) {
1282
+ const stuck = nodes.filter((n) => !ordered.includes(n.stmt)).map((n) => n.stmt.label);
1283
+ throw new Error(
1284
+ `write-plan: the write-time transaction DAG has a CYCLE (unresolvable ordering among [${stuck.join(", ")}]). This is either a mutual '$.ref' data dependency or a gate that depends on a body row (a gate cannot depend on a not-yet-written row). ESCALATE: the Command's write dependencies are contradictory \u2014 no order satisfies both the data dependencies and gate-first. (Fail-closed; the derivation never guesses an order.)`
1285
+ );
1286
+ }
1287
+ return ordered;
1288
+ }
1289
+ function deriveTransactionPlan(phase, bases, lifecycle, dialect = SQLITE) {
1290
+ if (bases.length === 0) {
1291
+ throw new Error("write-plan: a Command must declare at least one base write (Insert/Update/Delete).");
1292
+ }
1293
+ const nextId = makeIdGen();
1294
+ const seqRef = { n: 0 };
1295
+ const composite = bases.length > 1;
1296
+ const allNodes = [];
1297
+ const bodyIds = [];
1298
+ bases.forEach((base, i) => {
1299
+ const lc = base.effects !== void 0 ? { effects: base.effects } : i === 0 ? lifecycle : { effects: {} };
1300
+ const { nodes, bodyId } = compileWriteGroup(base, lc, nextId, dialect, seqRef);
1301
+ allNodes.push(...nodes);
1302
+ bodyIds.push(bodyId);
1303
+ });
1304
+ const statements = topoOrder(allNodes);
1305
+ const soleBody = bases[0];
1306
+ const soleBodyId = bodyIds[0];
1307
+ const usesEntity = referencesHead(statements, ENTITY_ROOT);
1308
+ const soleBodyReturns = /\breturning\b/i.test(soleBody.op.sql);
1309
+ if (usesEntity && composite) {
1310
+ throw new Error(
1311
+ `write-plan: a composite (multi-write) Command uses '$.entity.*' \u2014 ambiguous which write it denotes. Address each write's row by name via '$.ref.<writeName>.<field>' (fail-closed).`
1312
+ );
1313
+ }
1314
+ if (usesEntity && !soleBodyReturns) {
1315
+ throw new Error(
1316
+ `write-plan: a derive/edges/emits stage references '$.entity.*' but the body write ('${soleBody.label}') has no RETURNING clause \u2014 the written row cannot be exposed to later stages. Add a RETURNING to the base write (fail-closed; no absent-entity default).`
1317
+ );
1318
+ }
1319
+ const entityFrom = usesEntity || !composite && soleBodyReturns ? soleBodyId : null;
1320
+ for (const base of bases) {
1321
+ if (base.name === void 0) continue;
1322
+ if (referencesHead(statements, base.name) && !/\breturning\b/i.test(base.op.sql)) {
1323
+ throw new Error(
1324
+ `write-plan: write '${base.name}' is referenced as '$.ref.${base.name}.*' by a later statement but its op has no RETURNING clause \u2014 its row cannot be bound for downstream writes. Add a RETURNING to write '${base.name}' (fail-closed; no absent-row default).`
1325
+ );
1326
+ }
1327
+ }
1328
+ for (const s of statements) assertOperationPortable(s.op);
1329
+ return { phase, entityFrom, statements, onIdempotentHit: "rollback" };
1330
+ }
1331
+ function refHeadsOf(stmt) {
1332
+ const heads = /* @__PURE__ */ new Set();
1333
+ collectRefHeads(stmt.op.params, heads);
1334
+ if (stmt.op.where !== null) collectWhereHeads(stmt.op.where, heads);
1335
+ return [...heads];
1336
+ }
1337
+ function isInputHead(head, stmt) {
1338
+ return refPathLengthFor(stmt, head) === 1;
1339
+ }
1340
+ function refPathLengthFor(stmt, head) {
1341
+ let maxLen = 0;
1342
+ const walk = (node) => {
1343
+ if (node === null || typeof node !== "object") return;
1344
+ if (Array.isArray(node)) {
1345
+ for (const el of node) walk(el);
1346
+ return;
1347
+ }
1348
+ const keys = Object.keys(node);
1349
+ if (keys.length === 1 && keys[0] === "ref") {
1350
+ const path = node.ref;
1351
+ if (Array.isArray(path) && path[0] === head) maxLen = Math.max(maxLen, path.length);
1352
+ return;
1353
+ }
1354
+ for (const k of keys) walk(node[k]);
1355
+ };
1356
+ walk(stmt.op.params);
1357
+ if (stmt.op.where !== null) {
1358
+ const collectWhere = (node) => {
1359
+ if (node === null || typeof node !== "object") return;
1360
+ const n = node;
1361
+ if ("connector" in n && Array.isArray(n.fragments)) {
1362
+ for (const f of n.fragments) collectWhere(f);
1363
+ return;
1364
+ }
1365
+ if (Array.isArray(n.params)) walk(n.params);
1366
+ };
1367
+ collectWhere(stmt.op.where);
1368
+ }
1369
+ return maxLen;
1370
+ }
1371
+ function referencesHead(statements, head) {
1372
+ const heads = /* @__PURE__ */ new Set();
1373
+ for (const s of statements) {
1374
+ collectRefHeads(s.op.params, heads);
1375
+ if (s.op.where !== null) collectWhereHeads(s.op.where, heads);
1376
+ }
1377
+ return heads.has(head);
1378
+ }
1379
+ function collectRefHeads(node, heads) {
1380
+ if (node === null || typeof node !== "object") return;
1381
+ if (Array.isArray(node)) {
1382
+ for (const el of node) collectRefHeads(el, heads);
1383
+ return;
1384
+ }
1385
+ const keys = Object.keys(node);
1386
+ if (keys.length === 1 && keys[0] === "ref") {
1387
+ const path = node.ref;
1388
+ if (Array.isArray(path) && typeof path[0] === "string") heads.add(path[0]);
1389
+ return;
1390
+ }
1391
+ for (const k of keys) collectRefHeads(node[k], heads);
1392
+ }
1393
+ function collectWhereHeads(node, heads) {
1394
+ if (node === null || typeof node !== "object") return;
1395
+ const n = node;
1396
+ if ("connector" in n && Array.isArray(n.fragments)) {
1397
+ for (const f of n.fragments) collectWhereHeads(f, heads);
1398
+ return;
1399
+ }
1400
+ if (Array.isArray(n.params)) collectRefHeads(n.params, heads);
1401
+ }
1402
+
1403
+ // src/scp/write-runtime.ts
1404
+ function toDriverParam(v) {
1405
+ if (typeof v === "bigint") {
1406
+ if (v >= BigInt(Number.MIN_SAFE_INTEGER) && v <= BigInt(Number.MAX_SAFE_INTEGER)) return Number(v);
1407
+ return v;
1408
+ }
1409
+ if (v !== null && typeof v === "object" && !Array.isArray(v)) return JSON.stringify(v);
1410
+ return v;
1411
+ }
1412
+ function execStatement(db, op, scope, dialect) {
1413
+ const rendered = renderOperation(op, scope, dialect);
1414
+ const params = rendered.params.map(toDriverParam);
1415
+ const stmt = db.prepare(rendered.sql);
1416
+ const hasReturn = op.component === "Select" || /\breturning\b/i.test(rendered.sql);
1417
+ if (hasReturn) {
1418
+ const rows = stmt.all(...params);
1419
+ return { rows, changes: rows.length };
1420
+ }
1421
+ const info = stmt.run(...params);
1422
+ return { rows: [], changes: info.changes };
1423
+ }
1424
+ function gateShortCircuit(gate, result) {
1425
+ switch (gate) {
1426
+ case "existsElseRollback":
1427
+ return result.rows.length === 0 ? "requires_absent" : null;
1428
+ case "insertedElseRollback":
1429
+ return result.changes === 0 ? "unique_collision" : null;
1430
+ case "insertedElseNoop":
1431
+ return result.changes === 0 ? "idempotent_duplicate" : null;
1432
+ }
1433
+ }
1434
+ function executeTransaction(db, plan, input, dialect = SQLITE) {
1435
+ db.prepare("BEGIN").run();
1436
+ const executed = [];
1437
+ const scope = { ...input };
1438
+ let entity = null;
1439
+ try {
1440
+ for (const stmt of plan.statements) {
1441
+ const result = runOne(db, stmt, scope, executed, dialect);
1442
+ if (stmt.gate !== void 0) {
1443
+ const reason = gateShortCircuit(stmt.gate, result);
1444
+ if (reason !== null) {
1445
+ db.prepare("ROLLBACK").run();
1446
+ return { committed: false, shortCircuit: { statementId: stmt.id, reason }, entity: null, executed };
1447
+ }
1448
+ }
1449
+ if (stmt.id === plan.entityFrom) {
1450
+ entity = result.rows.length > 0 ? result.rows[0] : null;
1451
+ if (entity !== null) scope[ENTITY_ROOT] = entity;
1452
+ }
1453
+ if (stmt.binds !== void 0 && result.rows.length > 0) {
1454
+ scope[stmt.binds] = result.rows[0];
1455
+ }
1456
+ }
1457
+ db.prepare("COMMIT").run();
1458
+ return { committed: true, entity, executed };
1459
+ } catch (e) {
1460
+ try {
1461
+ db.prepare("ROLLBACK").run();
1462
+ } catch {
1463
+ }
1464
+ throw mapSqliteError(e);
1465
+ }
1466
+ }
1467
+ function runOne(db, stmt, scope, executed, dialect) {
1468
+ const result = execStatement(db, stmt.op, scope, dialect);
1469
+ executed.push(stmt.id);
1470
+ return result;
1471
+ }
1472
+ function countingDriver(db) {
1473
+ const prepared = [];
1474
+ const wrapped = {
1475
+ prepare(sql) {
1476
+ prepared.push(sql);
1477
+ return db.prepare(sql);
1478
+ }
1479
+ };
1480
+ return { db: wrapped, prepared };
1481
+ }
1482
+
1483
+ // src/scp/runtime.ts
1484
+ var SCOPE_PORT = "__scope";
1485
+ function toDriverParam2(v) {
1486
+ if (typeof v === "bigint") {
1487
+ if (v >= BigInt(Number.MIN_SAFE_INTEGER) && v <= BigInt(Number.MAX_SAFE_INTEGER)) return Number(v);
1488
+ return v;
1489
+ }
1490
+ return v;
1491
+ }
1492
+ function collectHeads(node, heads) {
1493
+ if (node === null || typeof node !== "object") return;
1494
+ if (Array.isArray(node)) {
1495
+ for (const el of node) collectHeads(el, heads);
1496
+ return;
1497
+ }
1498
+ const keys = Object.keys(node);
1499
+ if (keys.length === 1 && (keys[0] === "ref" || keys[0] === "refOpt")) {
1500
+ const path = node[keys[0]];
1501
+ if (Array.isArray(path) && typeof path[0] === "string") heads.add(path[0]);
1502
+ return;
1503
+ }
1504
+ for (const k of keys) collectHeads(node[k], heads);
1505
+ }
1506
+ function collectRefOptHeads(node, heads) {
1507
+ if (node === null || typeof node !== "object") return;
1508
+ if (Array.isArray(node)) {
1509
+ for (const el of node) collectRefOptHeads(el, heads);
1510
+ return;
1511
+ }
1512
+ const keys = Object.keys(node);
1513
+ if (keys.length === 1 && keys[0] === "refOpt") {
1514
+ const path = node.refOpt;
1515
+ if (Array.isArray(path) && typeof path[0] === "string") heads.add(path[0]);
1516
+ return;
1517
+ }
1518
+ for (const k of keys) collectRefOptHeads(node[k], heads);
1519
+ }
1520
+ function collectFragmentHeads(node, heads) {
1521
+ if ("connector" in node) {
1522
+ for (const f of node.fragments) collectFragmentHeads(f, heads);
1523
+ return;
1524
+ }
1525
+ if (node.when !== void 0) collectHeads(node.when, heads);
1526
+ for (const p of node.params) collectHeads(p, heads);
1527
+ }
1528
+ function operationHeads(op) {
1529
+ const heads = /* @__PURE__ */ new Set();
1530
+ for (const p of op.params) collectHeads(p, heads);
1531
+ if (op.where !== null) collectFragmentHeads(op.where, heads);
1532
+ return heads;
1533
+ }
1534
+ function skipGuardedHeads(node, heads) {
1535
+ if ("connector" in node) {
1536
+ for (const f of node.fragments) skipGuardedHeads(f, heads);
1537
+ return;
1538
+ }
1539
+ if (node.when !== void 0) {
1540
+ collectHeads(node.when, heads);
1541
+ for (const p of node.params) collectHeads(p, heads);
1542
+ }
1543
+ }
1544
+ function operationRefOptHeads(op, heads) {
1545
+ for (const p of op.params) collectRefOptHeads(p, heads);
1546
+ if (op.where !== null) collectFragmentRefOptHeads(op.where, heads);
1547
+ }
1548
+ function collectFragmentRefOptHeads(node, heads) {
1549
+ if ("connector" in node) {
1550
+ for (const f of node.fragments) collectFragmentRefOptHeads(f, heads);
1551
+ return;
1552
+ }
1553
+ if (node.when !== void 0) collectRefOptHeads(node.when, heads);
1554
+ for (const p of node.params) collectRefOptHeads(p, heads);
1555
+ }
1556
+ function optionalHeadsOf(compiled) {
1557
+ const heads = /* @__PURE__ */ new Set();
1558
+ for (const op of Object.values(compiled)) {
1559
+ if (op.where !== null) skipGuardedHeads(op.where, heads);
1560
+ operationRefOptHeads(op, heads);
1561
+ }
1562
+ return heads;
1563
+ }
1564
+ function scopePort(op) {
1565
+ const obj = {};
1566
+ for (const head of operationHeads(op)) {
1567
+ if (head === "@in") continue;
1568
+ obj[head] = { ref: [head] };
1569
+ }
1570
+ return { obj };
1571
+ }
1572
+ function surrogateNode(node, compiled) {
1573
+ if ("cond" in node) return node;
1574
+ if ("map" in node) {
1575
+ const op2 = compiled[node.id];
1576
+ if (op2 === void 0) return node;
1577
+ return { ...node, map: { ...node.map, ports: { [SCOPE_PORT]: scopePort(op2) } } };
1578
+ }
1579
+ const op = compiled[node.id];
1580
+ if (op === void 0) return node;
1581
+ return { ...node, ports: { [SCOPE_PORT]: scopePort(op) } };
1582
+ }
1583
+ function surrogateComponent(component, compiled) {
1584
+ return { ...component, body: component.body.map((n) => surrogateNode(n, compiled)) };
1585
+ }
1586
+ function executeRendered(db, op, scope, dialect) {
1587
+ const rendered = renderOperation(op, scope, dialect);
1588
+ const params = rendered.params.map(toDriverParam2);
1589
+ let stmt;
1590
+ try {
1591
+ stmt = db.prepare(rendered.sql);
1592
+ } catch (e) {
1593
+ return { error: mapSqliteError(e).message };
1594
+ }
1595
+ const hasReturn = op.component === "Select" || /\breturning\b/i.test(rendered.sql);
1596
+ try {
1597
+ if (hasReturn) {
1598
+ const rows = stmt.all(...params);
1599
+ return { ok: rows };
1600
+ }
1601
+ const info = stmt.run(...params);
1602
+ return {
1603
+ ok: [{ changes: info.changes, lastInsertRowid: toDriverParam2(BigInt(info.lastInsertRowid)) }]
1604
+ };
1605
+ } catch (e) {
1606
+ return { error: mapSqliteError(e).message };
1607
+ }
1608
+ }
1609
+ function buildHandlers(db, compiled, dialect) {
1610
+ const handle = (ports, ctx) => {
1611
+ const op = compiled[ctx.nodeId];
1612
+ if (op === void 0) {
1613
+ return { error: `scp runtime: no compiled operation for node '${ctx.nodeId}' (${ctx.component})` };
1614
+ }
1615
+ const scope = ports[SCOPE_PORT];
1616
+ if (scope === null || typeof scope !== "object" || Array.isArray(scope)) {
1617
+ return { error: `scp runtime: node '${ctx.nodeId}' surrogate scope did not evaluate to an object` };
1618
+ }
1619
+ return executeRendered(db, op, scope, dialect);
1620
+ };
1621
+ return { Select: handle, Insert: handle, Update: handle, Delete: handle };
1622
+ }
1623
+ function normalizeInput(component, optionalHeads, input) {
1624
+ const out = { ...input };
1625
+ for (const [port, schema] of Object.entries(component.inputPorts)) {
1626
+ if (schema.required !== true && !(port in out)) out[port] = null;
1627
+ }
1628
+ for (const head of optionalHeads) {
1629
+ if (!(head in out)) out[head] = null;
1630
+ }
1631
+ return out;
1632
+ }
1633
+ function executeBehavior(contract, input, options) {
1634
+ return executeBundle(compileBundle(contract, options.entry, options.relations, options.dialect), input, options);
1635
+ }
1636
+ function compileBundle(contract, entry2, relations = [], dialectName = "sqlite") {
1637
+ const dialect = dialectFor(dialectName);
1638
+ const component = entry2 ? contract.components.find((c) => c.name === entry2) : contract.components[0];
1639
+ if (component === void 0) {
1640
+ throw new Error(`scp runtime: entry component '${entry2 ?? "<first>"}' not found in contract`);
1641
+ }
1642
+ const operations = {};
1643
+ for (const [id, op] of compileComponentNodes(component, dialect)) operations[id] = op;
1644
+ const surrogate = surrogateComponent(component, operations);
1645
+ const relationOps = {};
1646
+ for (const decl of relations) {
1647
+ if (relationOps[decl.name] !== void 0) {
1648
+ throw new Error(`scp runtime: duplicate relation declaration '${decl.name}'`);
1649
+ }
1650
+ relationOps[decl.name] = compileRelationOp(decl);
1651
+ }
1652
+ return {
1653
+ irVersion: contract.ir.irVersion,
1654
+ exprVersion: contract.ir.exprVersion,
1655
+ dialect: dialectName,
1656
+ component: surrogate,
1657
+ operations,
1658
+ optionalHeads: [...optionalHeadsOf(operations)],
1659
+ relations: relationOps
1660
+ };
1661
+ }
1662
+ function executeBundle(bundle, input, options) {
1663
+ const surrogate = bundle.component;
1664
+ const ir = {
1665
+ irVersion: bundle.irVersion,
1666
+ exprVersion: bundle.exprVersion,
1667
+ components: [surrogate]
1668
+ };
1669
+ const handlers = buildHandlers(options.db, bundle.operations, dialectFor(bundle.dialect));
1670
+ const optionalHeads = new Set(bundle.optionalHeads);
1671
+ const normalized = normalizeInput(surrogate, optionalHeads, input);
1672
+ try {
1673
+ return runBehavior(ir, handlers, normalized, surrogate.name);
1674
+ } catch (e) {
1675
+ throw reErrorToSqlFailure(e);
1676
+ }
1677
+ }
1678
+ function baseWriteNodeOf(component) {
1679
+ const writes = component.body.filter(
1680
+ (n) => !("cond" in n) && !("map" in n) && (n.component === "Insert" || n.component === "Update" || n.component === "Delete")
1681
+ );
1682
+ if (writes.length === 0) {
1683
+ throw new Error(`scp write: Command '${component.name}' has no base write (Insert/Update/Delete) node`);
1684
+ }
1685
+ if (writes.length > 1) {
1686
+ throw new Error(
1687
+ `scp write: Command '${component.name}' has ${writes.length} base write nodes; WS5 initial scope is a SINGLE-statement Command + write-time relations (multi-write DAG is WS8, spec \xA76/\xA713).`
1688
+ );
1689
+ }
1690
+ return writes[0];
1691
+ }
1692
+ function compileWriteBundle(contract, entry2, writes, phase, dialectName = "sqlite") {
1693
+ const dialect = dialectFor(dialectName);
1694
+ const component = contract.components.find((c) => c.name === entry2);
1695
+ if (component === void 0) {
1696
+ throw new Error(`scp write: entry component '${entry2}' not found in contract`);
1697
+ }
1698
+ const lifecycle = lifecycleFor(writes, phase);
1699
+ if (lifecycle === void 0) {
1700
+ throw new Error(`scp write: the '${phase}' lifecycle is not declared in the entityWrites save contract`);
1701
+ }
1702
+ const writeNode = baseWriteNodeOf(component);
1703
+ const baseOp = compileNode(writeNode, dialect);
1704
+ const base = { op: baseOp, label: `${baseOp.component} ${writeNode.component}` };
1705
+ const plan = deriveTransactionPlan(phase, [base], lifecycle, dialect);
1706
+ const operations = {};
1707
+ for (const [id, op] of compileComponentNodes(component, dialect)) operations[id] = op;
1708
+ const surrogate = surrogateComponent(component, operations);
1709
+ return {
1710
+ irVersion: contract.ir.irVersion,
1711
+ exprVersion: contract.ir.exprVersion,
1712
+ dialect: dialectName,
1713
+ component: surrogate,
1714
+ operations,
1715
+ optionalHeads: [...optionalHeadsOf(operations)],
1716
+ relations: {},
1717
+ transaction: plan
1718
+ };
1719
+ }
1720
+ function compileCompositeWriteBundle(contract, entries, phase, dialectName = "sqlite") {
1721
+ if (entries.length < 2) {
1722
+ throw new Error("scp write: a composite write bundle needs at least 2 named write members (use compileWriteBundle for a single write).");
1723
+ }
1724
+ const dialect = dialectFor(dialectName);
1725
+ const bases = [];
1726
+ const operations = {};
1727
+ let firstComponent;
1728
+ for (const e of entries) {
1729
+ const component = contract.components.find((c) => c.name === e.entry);
1730
+ if (component === void 0) throw new Error(`scp write: entry component '${e.entry}' not found in contract`);
1731
+ if (firstComponent === void 0) firstComponent = component;
1732
+ const writeNode = baseWriteNodeOf(component);
1733
+ const baseOp = compileNode(writeNode, dialect);
1734
+ bases.push({ op: baseOp, label: `${writeNode.component} ${e.name}`, name: e.name, effects: e.lifecycle.effects });
1735
+ for (const [id, op] of compileComponentNodes(component, dialect)) operations[`${e.name}.${id}`] = op;
1736
+ }
1737
+ const plan = deriveTransactionPlan(phase, bases, { effects: {} }, dialect);
1738
+ const surrogate = surrogateComponent(firstComponent, operations);
1739
+ return {
1740
+ irVersion: contract.ir.irVersion,
1741
+ exprVersion: contract.ir.exprVersion,
1742
+ dialect: dialectName,
1743
+ component: surrogate,
1744
+ operations,
1745
+ optionalHeads: [...optionalHeadsOf(operations)],
1746
+ relations: {},
1747
+ transaction: plan
1748
+ };
1749
+ }
1750
+ function executeCompositeCommand(contract, entries, phase, input, options) {
1751
+ const bundle = compileCompositeWriteBundle(contract, entries, phase, options.dialect);
1752
+ return executeTransactionBundle(bundle, input, options);
1753
+ }
1754
+ function executeCommand(contract, writes, phase, input, options) {
1755
+ const bundle = compileWriteBundle(contract, options.entry, writes, phase, options.dialect);
1756
+ return executeTransactionBundle(bundle, input, options);
1757
+ }
1758
+ function executeTransactionBundle(bundle, input, options) {
1759
+ if (bundle.transaction === void 0) {
1760
+ throw new Error("scp write: this bundle carries no transaction plan (not a write-time-relations Command bundle)");
1761
+ }
1762
+ return executeTransaction(options.db, bundle.transaction, input, dialectFor(bundle.dialect));
1763
+ }
1764
+ function read(contract, input, options) {
1765
+ return readBundle(compileBundle(contract, options.entry, options.relations), input, options);
1766
+ }
1767
+ function readBundle(bundle, input, options) {
1768
+ const out = executeBundle(bundle, input, options);
1769
+ if (!Array.isArray(out)) {
1770
+ throw new Error(
1771
+ `scp read: the read behavior output is not a row list (got ${out === null ? "null" : typeof out}); the typed-object read surface expects a Select-shaped output`
1772
+ );
1773
+ }
1774
+ const rawRows = out;
1775
+ const readOpts = {
1776
+ ...options.with !== void 0 ? { with: options.with } : {},
1777
+ ...options.hydrate !== void 0 ? { hydrate: options.hydrate } : {}
1778
+ };
1779
+ return buildResultSet(rawRows, bundle.relations, options.db, readOpts);
1780
+ }
1781
+ var RELATION_BATCH_COMPONENT = "__RelationBatch";
1782
+ var RELATION_KEY_PORT = "__key";
1783
+ function resolveRelationViaPlan(op, parents, db) {
1784
+ const elemAs = "$p";
1785
+ const node = {
1786
+ id: "rel",
1787
+ map: {
1788
+ over: { ref: ["__parents"] },
1789
+ as: elemAs,
1790
+ component: RELATION_BATCH_COMPONENT,
1791
+ ports: { [RELATION_KEY_PORT]: { ref: [elemAs, op.parentKey] } },
1792
+ into: op.name,
1793
+ batched: true
1794
+ }
1795
+ };
1796
+ const component = {
1797
+ name: "__resolveRelation",
1798
+ inputPorts: { __parents: { required: true } },
1799
+ body: [node],
1800
+ output: { ref: ["rel"] }
1801
+ };
1802
+ const ir = {
1803
+ irVersion: 1,
1804
+ exprVersion: 1,
1805
+ components: [component]
1806
+ };
1807
+ const handlers = {
1808
+ [RELATION_BATCH_COMPONENT]: (portsOrItems) => {
1809
+ const items = portsOrItems.items;
1810
+ if (!Array.isArray(items)) {
1811
+ return { error: `${RELATION_BATCH_COMPONENT}: expected a batched {items:[\u2026]} invocation` };
1812
+ }
1813
+ const keyRows = items.map((it) => ({ [op.parentKey]: it[RELATION_KEY_PORT] }));
1814
+ const { batch } = runRelationOp(op, keyRows, db);
1815
+ const aligned = keyRows.map((r) => distributeToParent(op, r, batch));
1816
+ return { ok: aligned };
1817
+ }
1818
+ };
1819
+ const out = runBehavior(ir, handlers, { __parents: parents }, "__resolveRelation");
1820
+ return out;
1821
+ }
1822
+ function reErrorToSqlFailure(e) {
1823
+ const message = e instanceof Error ? e.message : String(e);
1824
+ const m = /(SQLITE_[A-Z_]+)/.exec(message);
1825
+ if (m) {
1826
+ return mapSqliteError({ code: m[1], message });
1827
+ }
1828
+ return e;
1829
+ }
1830
+
1831
+ // src/scp/codegen.ts
1832
+ import { generateModule } from "behavior-contracts";
1833
+ import { runBehavior as runBehavior2 } from "behavior-contracts";
1834
+ var CODEGEN_LANGUAGES = ["typescript", "python", "go", "rust", "php"];
1835
+ function bundleToPortableIR(bundle) {
1836
+ return {
1837
+ irVersion: bundle.irVersion,
1838
+ exprVersion: bundle.exprVersion,
1839
+ components: [bundle.component]
1840
+ };
1841
+ }
1842
+ function companionOf(bundle) {
1843
+ const base = {
1844
+ operations: bundle.operations,
1845
+ dialect: bundle.dialect,
1846
+ optionalHeads: bundle.optionalHeads,
1847
+ relations: bundle.relations
1848
+ };
1849
+ return bundle.transaction === void 0 ? base : { ...base, transaction: bundle.transaction };
1850
+ }
1851
+ function assertLanguageSupported(language, registered) {
1852
+ if (!CODEGEN_LANGUAGES.includes(language)) {
1853
+ throw new Error(
1854
+ `litedbmodel codegen: language '${language}' is not a supported target (supported: ${CODEGEN_LANGUAGES.join(", ")})`
1855
+ );
1856
+ }
1857
+ if (!registered.includes(language)) {
1858
+ throw new Error(
1859
+ `litedbmodel codegen: bc's shared generator does not register an emitter for '${language}' (bc registered: ${[...registered].sort().join(", ")}). This is a bc#13 capability limit \u2014 ESCALATE to bc (bc#22 pattern), never fork a litedbmodel-local generator (codegen \u306F\u4E0A\u6D41\u6240\u6709, spec \xA79).`
1860
+ );
1861
+ }
1862
+ }
1863
+ function generateCodegenArtifact(bundle, language, registeredLanguages, runtimeImport) {
1864
+ assertLanguageSupported(language, registeredLanguages);
1865
+ const ir = bundleToPortableIR(bundle);
1866
+ const module = generateModule(ir, runtimeImport === void 0 ? { language } : { language, runtimeImport });
1867
+ return { language, module, companion: companionOf(bundle), ir };
1868
+ }
1869
+ function codegenExecuteBundleForTest(artifact, input, db) {
1870
+ const { ir } = artifact;
1871
+ const component = ir.components[0];
1872
+ const { operations, dialect, optionalHeads } = artifact.companion;
1873
+ const handlers = buildHandlers(db, operations, dialectFor(dialect));
1874
+ const normalized = normalizeInput(component, new Set(optionalHeads), input);
1875
+ return runBehavior2(ir, handlers, normalized, component.name);
1876
+ }
1877
+
1878
+ // src/scp/index.ts
1879
+ import {
1880
+ SemanticBehavior as SemanticBehavior2,
1881
+ behavior,
1882
+ when,
1883
+ concat,
1884
+ add,
1885
+ sub,
1886
+ mul,
1887
+ div,
1888
+ mod,
1889
+ neg,
1890
+ eq as eq2,
1891
+ ne as ne2,
1892
+ lt as lt2,
1893
+ le as le2,
1894
+ gt as gt2,
1895
+ ge as ge2,
1896
+ and,
1897
+ or,
1898
+ not,
1899
+ coalesce,
1900
+ len,
1901
+ opt
1902
+ } from "behavior-contracts";
1903
+ export {
1904
+ CATALOG_NAMES,
1905
+ CODEGEN_LANGUAGES,
1906
+ ENTITY_ROOT,
1907
+ IN_SENTINEL,
1908
+ LITEDBMODEL_CATALOG,
1909
+ MYSQL,
1910
+ POSTGRES,
1911
+ RELATION_CONTEXT,
1912
+ RELATION_KEYS_HEAD,
1913
+ RelationContext,
1914
+ SQLITE,
1915
+ SemanticBehavior2 as SemanticBehavior,
1916
+ SqlFailure,
1917
+ WHERE_SLOT,
1918
+ WRITE_CATALOG_NAMES,
1919
+ WRITE_PORT_FAMILIES,
1920
+ add,
1921
+ and,
1922
+ assertComponentGraphPortable,
1923
+ assertComponentPortable,
1924
+ assertComponentsInCatalog,
1925
+ assertExprPortable,
1926
+ assertLanguageSupported,
1927
+ assertOperationPortable,
1928
+ behavior,
1929
+ buildHandlers,
1930
+ buildResultSet,
1931
+ bundleToPortableIR,
1932
+ catalogEntry,
1933
+ coalesce,
1934
+ codegenExecuteBundleForTest,
1935
+ compileBundle,
1936
+ compileComponentNodes,
1937
+ compileCompositeWriteBundle,
1938
+ compileDelete,
1939
+ compileEager,
1940
+ compileInsert,
1941
+ compileInsertFor,
1942
+ compileNode,
1943
+ compileRelationOp,
1944
+ compileSelect,
1945
+ compileUpdate,
1946
+ compileWriteBundle,
1947
+ components,
1948
+ concat,
1949
+ countingDriver,
1950
+ deriveContractEffect,
1951
+ deriveTransactionPlan,
1952
+ dialectFor,
1953
+ distributeToParent,
1954
+ div,
1955
+ edgeWrites,
1956
+ entityWrites,
1957
+ eq2 as eq,
1958
+ executeBehavior,
1959
+ executeBundle,
1960
+ executeCommand,
1961
+ executeCompositeCommand,
1962
+ executeTransaction,
1963
+ executeTransactionBundle,
1964
+ ge2 as ge,
1965
+ generateCodegenArtifact,
1966
+ gt2 as gt,
1967
+ inColumn,
1968
+ le2 as le,
1969
+ len,
1970
+ lifecycleFor,
1971
+ lt2 as lt,
1972
+ mapSqliteError,
1973
+ mod,
1974
+ mul,
1975
+ ne2 as ne,
1976
+ neg,
1977
+ normalizeInput,
1978
+ not,
1979
+ opt,
1980
+ or,
1981
+ parseEffectPath,
1982
+ publishBehaviors,
1983
+ read,
1984
+ readBundle,
1985
+ readRelationContext,
1986
+ renderOperation,
1987
+ resolveRelationViaPlan,
1988
+ runRelationOp,
1989
+ sub,
1990
+ toDollarPlaceholders,
1991
+ when,
1992
+ whereEq,
1993
+ whereGe,
1994
+ whereGt,
1995
+ whereIn,
1996
+ whereIsNull,
1997
+ whereLe,
1998
+ whereLt,
1999
+ whereNe
2000
+ };