@happyvertical/smrt-core 0.40.69 → 0.40.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +29 -4
- package/README.md +20 -1
- package/agents/query-bounds.md +45 -0
- package/agents/schema-paths.md +328 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +2 -1
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +28 -4
- package/dist/class.js.map +1 -1
- package/dist/collection.d.ts +101 -2
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +224 -43
- package/dist/collection.js.map +1 -1
- package/dist/database.d.ts +8 -0
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +16 -8
- package/dist/database.js.map +1 -1
- package/dist/db-errors.d.ts +105 -0
- package/dist/db-errors.d.ts.map +1 -0
- package/dist/db-errors.js +382 -0
- package/dist/db-errors.js.map +1 -0
- package/dist/decorators/index.d.ts +17 -1
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +65 -7
- package/dist/decorators/index.js.map +1 -1
- package/dist/dispatch/bus.d.ts.map +1 -1
- package/dist/dispatch/bus.js +4 -3
- package/dist/dispatch/bus.js.map +1 -1
- package/dist/errors.d.ts +30 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +34 -2
- package/dist/errors.js.map +1 -1
- package/dist/generators/changes-route.d.ts.map +1 -1
- package/dist/generators/changes-route.js +6 -3
- package/dist/generators/changes-route.js.map +1 -1
- package/dist/generators/mcp-runtime-template.d.ts +8 -0
- package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
- package/dist/generators/mcp-runtime-template.js +38 -4
- package/dist/generators/mcp-runtime-template.js.map +1 -1
- package/dist/generators/mcp.d.ts +16 -0
- package/dist/generators/mcp.d.ts.map +1 -1
- package/dist/generators/mcp.js +41 -3
- package/dist/generators/mcp.js.map +1 -1
- package/dist/generators/rest.d.ts +22 -0
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +34 -3
- package/dist/generators/rest.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/manifest/static-manifest.js +1 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/migrations/differ.d.ts +202 -8
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +604 -46
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/generator.d.ts +31 -4
- package/dist/migrations/generator.d.ts.map +1 -1
- package/dist/migrations/generator.js +49 -5
- package/dist/migrations/generator.js.map +1 -1
- package/dist/migrations/index.d.ts +3 -2
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +5 -3
- package/dist/migrations/orchestrate.js +35 -6
- package/dist/migrations/orchestrate.js.map +1 -1
- package/dist/migrations/sqlite-rebuild.d.ts +142 -0
- package/dist/migrations/sqlite-rebuild.d.ts.map +1 -0
- package/dist/migrations/sqlite-rebuild.js +514 -0
- package/dist/migrations/sqlite-rebuild.js.map +1 -0
- package/dist/migrations/tracker.d.ts +114 -1
- package/dist/migrations/tracker.d.ts.map +1 -1
- package/dist/migrations/tracker.js +331 -16
- package/dist/migrations/tracker.js.map +1 -1
- package/dist/migrations/types.d.ts +19 -4
- package/dist/migrations/types.d.ts.map +1 -1
- package/dist/migrations.js +5 -3
- package/dist/object.d.ts +45 -6
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +60 -13
- package/dist/object.js.map +1 -1
- package/dist/postgres-timeouts.d.ts +240 -0
- package/dist/postgres-timeouts.d.ts.map +1 -0
- package/dist/postgres-timeouts.js +204 -0
- package/dist/postgres-timeouts.js.map +1 -0
- package/dist/query-bounds.d.ts +97 -0
- package/dist/query-bounds.d.ts.map +1 -0
- package/dist/query-bounds.js +173 -0
- package/dist/query-bounds.js.map +1 -0
- package/dist/registry/types.d.ts +32 -1
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry.js +1 -1
- package/dist/scanner/types.d.ts +5 -0
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/scanner/types.js.map +1 -1
- package/dist/schema/ddl/base-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/base-strategy.js +2 -2
- package/dist/schema/ddl/base-strategy.js.map +1 -1
- package/dist/schema/generator.d.ts +70 -5
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +124 -6
- package/dist/schema/generator.js.map +1 -1
- package/dist/schema/index.d.ts +3 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +4 -1
- package/dist/schema/live-parity.d.ts +90 -0
- package/dist/schema/live-parity.d.ts.map +1 -0
- package/dist/schema/live-parity.js +600 -0
- package/dist/schema/live-parity.js.map +1 -0
- package/dist/schema/manifest-schema.d.ts +121 -0
- package/dist/schema/manifest-schema.d.ts.map +1 -0
- package/dist/schema/manifest-schema.js +267 -0
- package/dist/schema/manifest-schema.js.map +1 -0
- package/dist/schema/schema-aggregator.d.ts +24 -10
- package/dist/schema/schema-aggregator.d.ts.map +1 -1
- package/dist/schema/schema-aggregator.js +35 -90
- package/dist/schema/schema-aggregator.js.map +1 -1
- package/dist/schema/system-table-shapes.d.ts +52 -0
- package/dist/schema/system-table-shapes.d.ts.map +1 -0
- package/dist/schema/system-table-shapes.js +174 -0
- package/dist/schema/system-table-shapes.js.map +1 -0
- package/dist/schema/types.d.ts +103 -4
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/utils.d.ts +1 -0
- package/dist/schema/utils.d.ts.map +1 -1
- package/dist/schema/utils.js +3 -1
- package/dist/schema/utils.js.map +1 -1
- package/dist/schema.js +4 -1
- package/dist/smrt-knowledge.json +18 -6
- package/dist/sync/apply.d.ts.map +1 -1
- package/dist/sync/apply.js +9 -16
- package/dist/sync/apply.js.map +1 -1
- package/dist/testing/database.d.ts.map +1 -1
- package/dist/testing/database.js +1 -0
- package/dist/testing/database.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +71 -5
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +6 -4
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +5 -5
package/dist/schema/types.d.ts
CHANGED
|
@@ -42,6 +42,44 @@ export interface IndexDefinition {
|
|
|
42
42
|
path: string;
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* A multi-column index declared on an object through `@smrt({ indexes: [...] })`
|
|
47
|
+
* (issue #2357).
|
|
48
|
+
*
|
|
49
|
+
* Generated schemas otherwise only index foreign keys, unique/conflict columns,
|
|
50
|
+
* `updated_at`, the STI discriminator, `tenant_id`, and columns opted in one at
|
|
51
|
+
* a time with `@field({ indexed: true })`. None of those can express a list
|
|
52
|
+
* workload's real access path, which is composite: filter column(s) first, sort
|
|
53
|
+
* column last. `WHERE tenant_id = ? ... ORDER BY publish_date DESC LIMIT 10`
|
|
54
|
+
* wants `(tenant_id, publish_date)`, and there was no way to ask for it.
|
|
55
|
+
*
|
|
56
|
+
* Declare the columns, not a direction: PostgreSQL scans a btree in either
|
|
57
|
+
* direction, so a plain ascending index also serves the matching
|
|
58
|
+
* `ORDER BY ... DESC` as an ordered scan with no sort step.
|
|
59
|
+
*/
|
|
60
|
+
export interface DeclaredIndexDefinition {
|
|
61
|
+
/**
|
|
62
|
+
* Index name. Must be unique within the database, and must not collide with
|
|
63
|
+
* a generated index of the same name unless it describes the same target —
|
|
64
|
+
* schema generation fails rather than silently dropping either one.
|
|
65
|
+
*/
|
|
66
|
+
name: string;
|
|
67
|
+
/**
|
|
68
|
+
* SMRT field names or column names, in index order. Field names are mapped to
|
|
69
|
+
* their column names; a name that resolves to no column on the table is a
|
|
70
|
+
* hard error.
|
|
71
|
+
*/
|
|
72
|
+
columns: string[];
|
|
73
|
+
/** Emit as a UNIQUE index. */
|
|
74
|
+
unique?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Partial-index predicate, rendered verbatim into `CREATE INDEX ... WHERE`.
|
|
77
|
+
*
|
|
78
|
+
* This is authored SQL from the object's own source, exactly like the
|
|
79
|
+
* predicates the STI path emits; never build it from request input.
|
|
80
|
+
*/
|
|
81
|
+
where?: string;
|
|
82
|
+
}
|
|
45
83
|
export interface TriggerDefinition {
|
|
46
84
|
name: string;
|
|
47
85
|
when: 'BEFORE' | 'AFTER' | 'INSTEAD OF';
|
|
@@ -59,7 +97,16 @@ export interface ForeignKeyDefinition {
|
|
|
59
97
|
}
|
|
60
98
|
export interface SchemaDefinition {
|
|
61
99
|
tableName: string;
|
|
62
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* Engine-neutral CREATE TABLE preview (optional; may be generated lazily).
|
|
102
|
+
*
|
|
103
|
+
* Non-authoritative (#2358): it carries no indexes, no triggers and no
|
|
104
|
+
* per-engine type mapping (`REAL`/`JSON`/`UUID`/`TIMESTAMP` stay abstract).
|
|
105
|
+
* Kept for backward compatibility with published manifests and third-party
|
|
106
|
+
* tooling; framework consumers render `columns` and `indexes` through
|
|
107
|
+
* `getDDLStrategy(engine)` and only merge `ddl` in for a table whose
|
|
108
|
+
* contributors expose no `columns` (hand-authored manifests).
|
|
109
|
+
*/
|
|
63
110
|
ddl?: string;
|
|
64
111
|
columns: Record<string, ColumnDefinition>;
|
|
65
112
|
indexes: IndexDefinition[];
|
|
@@ -198,12 +245,39 @@ export interface DriftReport {
|
|
|
198
245
|
/** Suggested action to resolve the drift */
|
|
199
246
|
recommendation: string;
|
|
200
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Which column property an `alter_column` change repairs (#2369).
|
|
250
|
+
*
|
|
251
|
+
* - `set_not_null` / `set_default`: the manifest is stricter than the live
|
|
252
|
+
* column (strengthening). The differ emits executable SQL for these on
|
|
253
|
+
* engines that support `ALTER COLUMN`.
|
|
254
|
+
* - `drop_not_null` / `drop_default`: the live column is stricter than the
|
|
255
|
+
* manifest (relaxing). Reported as an advisory by default and only made
|
|
256
|
+
* executable when the caller opts in (`DiffOptions.relaxColumns`), because
|
|
257
|
+
* weakening a production column on the strength of a manifest that may
|
|
258
|
+
* itself be under-specified is not something to do silently.
|
|
259
|
+
*/
|
|
260
|
+
export type ColumnAlteration = 'set_not_null' | 'drop_not_null' | 'set_default' | 'drop_default';
|
|
261
|
+
/**
|
|
262
|
+
* Advisory attached to a change the differ reports but does not execute
|
|
263
|
+
* (orphan columns/indexes, or relaxations the caller has not opted into).
|
|
264
|
+
* `suggestedSql` is remediation the operator can run by hand; it is never
|
|
265
|
+
* part of the executable statement stream.
|
|
266
|
+
*/
|
|
267
|
+
export interface SchemaChangeAdvisory {
|
|
268
|
+
/** `warning` when the state will break application writes; `info` otherwise. */
|
|
269
|
+
severity: 'warning' | 'info';
|
|
270
|
+
/** Human-readable explanation of the drift and how to resolve it. */
|
|
271
|
+
message: string;
|
|
272
|
+
/** Remediation SQL the differ suggests but does not run. */
|
|
273
|
+
suggestedSql?: string[];
|
|
274
|
+
}
|
|
201
275
|
/**
|
|
202
276
|
* Schema difference for a single change
|
|
203
277
|
*/
|
|
204
278
|
export interface SchemaChange {
|
|
205
279
|
/** Type of change */
|
|
206
|
-
type: 'add_table' | 'drop_table' | 'add_column' | 'drop_column' | 'add_index' | 'drop_index' | 'type_mismatch' | 'type_upgrade';
|
|
280
|
+
type: 'add_table' | 'drop_table' | 'add_column' | 'drop_column' | 'alter_column' | 'orphan_column' | 'add_index' | 'drop_index' | 'orphan_index' | 'type_mismatch' | 'type_upgrade';
|
|
207
281
|
/** Affected table name */
|
|
208
282
|
table: string;
|
|
209
283
|
/** Column or index name (if applicable) */
|
|
@@ -212,11 +286,25 @@ export interface SchemaChange {
|
|
|
212
286
|
column?: ColumnDefinition;
|
|
213
287
|
/** Index definition (for add_index) */
|
|
214
288
|
index?: IndexDefinition;
|
|
215
|
-
/**
|
|
289
|
+
/**
|
|
290
|
+
* For type mismatches/upgrades: expected vs actual type. For
|
|
291
|
+
* `alter_column`: the expected vs actual constraint rendering (e.g.
|
|
292
|
+
* `NOT NULL` vs `NULL`, `DEFAULT ''` vs `(no default)`). For
|
|
293
|
+
* `orphan_column`: `expected` is `(not in manifest)` and `actual` is the
|
|
294
|
+
* live column shape.
|
|
295
|
+
*/
|
|
216
296
|
mismatch?: {
|
|
217
297
|
expected: string;
|
|
218
298
|
actual: string;
|
|
219
299
|
};
|
|
300
|
+
/** Which property an `alter_column` change repairs. */
|
|
301
|
+
alteration?: ColumnAlteration;
|
|
302
|
+
/**
|
|
303
|
+
* Present on report-only changes (`orphan_column`, `orphan_index`, and
|
|
304
|
+
* `alter_column` relaxations the caller has not opted into). A change
|
|
305
|
+
* carrying an advisory and no `sql`/`sqlStatements` is never executed.
|
|
306
|
+
*/
|
|
307
|
+
advisory?: SchemaChangeAdvisory;
|
|
220
308
|
/** Generated SQL statement */
|
|
221
309
|
sql?: string;
|
|
222
310
|
/**
|
|
@@ -233,9 +321,20 @@ export interface SchemaDiff {
|
|
|
233
321
|
added_tables: SchemaDefinition[];
|
|
234
322
|
/** Tables to drop (only if explicitly requested) */
|
|
235
323
|
dropped_tables: string[];
|
|
324
|
+
/**
|
|
325
|
+
* Application tables present in the database but absent from the manifest
|
|
326
|
+
* (system `_smrt_*` / `sqlite_*` tables excluded). Always reported so a
|
|
327
|
+
* stale table is never silently retained; dropping stays opt-in via
|
|
328
|
+
* `dropped_tables`. Informational — does not affect `has_changes`.
|
|
329
|
+
*/
|
|
330
|
+
orphan_tables?: string[];
|
|
236
331
|
/** Changes to existing tables */
|
|
237
332
|
changes: SchemaChange[];
|
|
238
|
-
/**
|
|
333
|
+
/**
|
|
334
|
+
* Whether any executable, manual, or warning-level difference was found.
|
|
335
|
+
* Info-level report-only notes in `changes` (a harmless orphan column, a
|
|
336
|
+
* stale default) and `orphan_tables` do not set it.
|
|
337
|
+
*/
|
|
239
338
|
has_changes: boolean;
|
|
240
339
|
}
|
|
241
340
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/schema/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,WAAW,GACnB,MAAM,GACN,SAAS,GACT,MAAM,GACN,MAAM,GACN,SAAS,GACT,MAAM,GACN,WAAW,GAKX,MAAM,CAAC;AAEX,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,WAAW,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,aAAa,CAAC,EAAE,IAAI,GAAG,YAAY,GAAG,iBAAiB,GAAG,UAAU,CAAC;IACrE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE;QACX,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;QAC/C,QAAQ,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;KAChD,CAAC;IACF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE;QACT,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,YAAY,CAAC;IACxC,KAAK,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;IAC/C,QAAQ,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;CAChD;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/schema/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,WAAW,GACnB,MAAM,GACN,SAAS,GACT,MAAM,GACN,MAAM,GACN,SAAS,GACT,MAAM,GACN,WAAW,GAKX,MAAM,CAAC;AAEX,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,WAAW,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,aAAa,CAAC,EAAE,IAAI,GAAG,YAAY,GAAG,iBAAiB,GAAG,UAAU,CAAC;IACrE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE;QACX,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;QAC/C,QAAQ,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;KAChD,CAAC;IACF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE;QACT,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,8BAA8B;IAC9B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,YAAY,CAAC;IACxC,KAAK,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;IAC/C,QAAQ,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;CAChD;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;;;OASG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC1C,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,WAAW,EAAE,oBAAoB,EAAE,CAAC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC1C,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC9C,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,UAAU,CAAC,EAAE,eAAe,EAAE,CAAC;IAC/B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,WAAW,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAClC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,EAAE,EAAE,MAAM,EAAE,CAAC;IACb,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,MAAM,eAAe,GACvB,SAAS,GACT,SAAS,GACT,WAAW,GACX,QAAQ,GACR,aAAa,CAAC;AAElB;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,uBAAuB;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,wDAAwD;IACxD,QAAQ,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,qCAAqC;IACrC,UAAU,EAAE,IAAI,CAAC;IACjB,yCAAyC;IACzC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,sDAAsD;IACtD,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,mCAAmC;IACnC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,+BAA+B;IAC/B,MAAM,EAAE,eAAe,CAAC;IACxB,0CAA0C;IAC1C,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,aAAa,EAAE,OAAO,CAAC;IACvB,yDAAyD;IACzD,cAAc,EAAE,IAAI,GAAG,IAAI,CAAC;IAC5B,+DAA+D;IAC/D,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,mDAAmD;IACnD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,EAAE,EAAE,MAAM,CAAC;IACX,iCAAiC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,EAAE,EAAE,MAAM,EAAE,CAAC;IACb,+CAA+C;IAC/C,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,uDAAuD;IACvD,OAAO,EAAE,OAAO,CAAC;IACjB,qBAAqB;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,yCAAyC;IACzC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,4EAA4E;IAC5E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,6EAA6E;IAC7E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gCAAgC;IAChC,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACvB,4CAA4C;IAC5C,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,qBAAqB;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,kDAAkD;IAClD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,wCAAwC;IACxC,eAAe,EAAE,MAAM,CAAC;IACxB,6BAA6B;IAC7B,UAAU,EACN,eAAe,GACf,aAAa,GACb,eAAe,GACf,eAAe,CAAC;IACpB,4CAA4C;IAC5C,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,gBAAgB,GACxB,cAAc,GACd,eAAe,GACf,aAAa,GACb,cAAc,CAAC;AAEnB;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,gFAAgF;IAChF,QAAQ,EAAE,SAAS,GAAG,MAAM,CAAC;IAC7B,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,qBAAqB;IACrB,IAAI,EACA,WAAW,GACX,YAAY,GACZ,YAAY,GACZ,aAAa,GACb,cAAc,GACd,eAAe,GACf,WAAW,GACX,YAAY,GACZ,cAAc,GACd,eAAe,GACf,cAAc,CAAC;IACnB,0BAA0B;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,uCAAuC;IACvC,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE;QACT,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,uDAAuD;IACvD,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IAChC,8BAA8B;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,2BAA2B;IAC3B,YAAY,EAAE,gBAAgB,EAAE,CAAC;IACjC,oDAAoD;IACpD,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,iCAAiC;IACjC,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB;;;;OAIG;IACH,WAAW,EAAE,OAAO,CAAC;CACtB"}
|
package/dist/schema/utils.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { FieldDefinition } from '../scanner/types.js';
|
|
|
4
4
|
import { DatabaseEngine } from './ddl/types.js';
|
|
5
5
|
export { materializeManifestDDLForEngine, tokenizeSQLDDLBody, } from './ddl/materialize-manifest.js';
|
|
6
6
|
export { isJsonPathIndex, renderIndexTarget } from './index-utils.js';
|
|
7
|
+
export { type CollectedManifestTable, collectManifestTables, type ManifestColumnLike, type ManifestIndexLike, type ManifestSchemaLike, manifestColumnsToDefinitions, manifestIndexesToDefinitions, manifestSchemaToDefinition, mergeSchemaDefinitionInto, renderCollectedManifestTable, } from './manifest-schema.js';
|
|
7
8
|
/**
|
|
8
9
|
* Generates a complete database schema SQL statement for a class
|
|
9
10
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/schema/utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAGrD,OAAO,EACL,+BAA+B,EAC/B,kBAAkB,GACnB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/schema/utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAGrD,OAAO,EACL,+BAA+B,EAC/B,kBAAkB,GACnB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAGtE,OAAO,EACL,KAAK,sBAAsB,EAC3B,qBAAqB,EACrB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,4BAA4B,EAC5B,4BAA4B,EAC5B,0BAA0B,EAC1B,yBAAyB,EACzB,4BAA4B,GAC7B,MAAM,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;GAaG;AACH,wBAAsB,cAAc,CAMlC,SAAS,EAAE;IAAE,KAAK,GAAG,IAAI,EAAE,KAAK,EAAE,GAAG,UAAU,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACxE,cAAc,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,EAC7C,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,cAAc,CAAA;CAAO,mBA8I1C;AAED;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAChC,EAAE,EAAE,iBAAiB,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAyEf"}
|
package/dist/schema/utils.js
CHANGED
|
@@ -3,6 +3,7 @@ import { tableNameFromClass } from "../utils.js";
|
|
|
3
3
|
import { ObjectRegistry } from "../registry.js";
|
|
4
4
|
import { SchemaManager } from "./schema-manager.js";
|
|
5
5
|
import { materializeManifestDDLForEngine, tokenizeSQLDDLBody } from "./ddl/materialize-manifest.js";
|
|
6
|
+
import { collectManifestTables, manifestColumnsToDefinitions, manifestIndexesToDefinitions, manifestSchemaToDefinition, mergeSchemaDefinitionInto, renderCollectedManifestTable } from "./manifest-schema.js";
|
|
6
7
|
//#region src/schema/utils.ts
|
|
7
8
|
/**
|
|
8
9
|
* Generates a complete database schema SQL statement for a class
|
|
@@ -36,6 +37,7 @@ async function generateSchema(ClassType, providedFields, options = {}) {
|
|
|
36
37
|
const runtimeSchemaConfig = registeredClass?.config ? {
|
|
37
38
|
conflictColumns: registeredClass.config.conflictColumns,
|
|
38
39
|
idType: registeredClass.config.idType,
|
|
40
|
+
indexes: registeredClass.config.indexes,
|
|
39
41
|
registry: ObjectRegistry
|
|
40
42
|
} : void 0;
|
|
41
43
|
let schemaDefinition;
|
|
@@ -98,6 +100,6 @@ async function ensureSchema(db, className) {
|
|
|
98
100
|
await new SchemaManager(db, { skipTriggers: typeof db.exportTable === "function" }).ensureTable(effectiveSchemaDefinition);
|
|
99
101
|
}
|
|
100
102
|
//#endregion
|
|
101
|
-
export { ensureSchema, generateSchema, isJsonPathIndex, materializeManifestDDLForEngine, renderIndexTarget, tokenizeSQLDDLBody };
|
|
103
|
+
export { collectManifestTables, ensureSchema, generateSchema, isJsonPathIndex, manifestColumnsToDefinitions, manifestIndexesToDefinitions, manifestSchemaToDefinition, materializeManifestDDLForEngine, mergeSchemaDefinitionInto, renderCollectedManifestTable, renderIndexTarget, tokenizeSQLDDLBody };
|
|
102
104
|
|
|
103
105
|
//# sourceMappingURL=utils.js.map
|
package/dist/schema/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","names":[],"sources":["../../src/schema/utils.ts"],"sourcesContent":["/**\n * Schema generation utilities - Node.js only\n *\n * These functions use SchemaGenerator which depends on node:crypto.\n * Separated from main utils.ts to prevent bundling in browser builds.\n *\n * SMRT handles ALL database maintenance directly.\n * SDK SQL remains a pure query/CRUD layer.\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport type { SmrtObject } from '../object.js';\nimport { ObjectRegistry } from '../registry.js';\nimport type { FieldDefinition } from '../scanner/types.js';\nimport { tableNameFromClass } from '../utils.js';\nimport type { DatabaseEngine } from './ddl/types.js';\nimport { SchemaManager } from './schema-manager.js';\n\nexport {\n materializeManifestDDLForEngine,\n tokenizeSQLDDLBody,\n} from './ddl/materialize-manifest.js';\n// Index-rendering helpers live in a separate file to avoid pulling the\n// heavyweight registry/collection module graph into the DDL strategies.\nexport { isJsonPathIndex, renderIndexTarget } from './index-utils.js';\n\n/**\n * Generates a complete database schema SQL statement for a class\n *\n * This is a thin wrapper around SchemaGenerator that provides the\n * single source of truth for schema generation. Uses ObjectRegistry\n * cached fields from AST manifest for consistent schema generation.\n *\n * **Note**: Uses dynamic import for SchemaGenerator to avoid bundling\n * Node.js-only code (node:crypto) in browser builds.\n *\n * @param ClassType - Class constructor to generate schema for\n * @param providedFields - Optional fields map (used during registration)\n * @returns SQL schema creation statement with CREATE TABLE and CREATE INDEX statements\n */\nexport async function generateSchema(\n // Accepts any SmrtObject constructor: the registry-stored `typeof SmrtObject`\n // as well as collection item classes whose construct signature is narrower\n // than the full static surface. Only `.name` and the construct identity are\n // used here. `never[]` keeps the construct signature contravariantly\n // compatible with constructors that declare their own argument shapes.\n ClassType: { new (...args: never[]): SmrtObject; readonly name: string },\n providedFields?: Map<string, FieldDefinition>,\n options: { engine?: DatabaseEngine } = {},\n) {\n const className = ClassType.name;\n const tableName = tableNameFromClass(ClassType);\n\n // For external packages, ensure manifest is loaded before proceeding\n if (!providedFields || providedFields.size === 0) {\n await ObjectRegistry.ensureManifestLoaded(className);\n }\n\n // Use provided fields if available AND non-empty (during registration), otherwise get from registry\n // NEW: Use getAllFields() to include inherited fields from parent classes\n const cachedFields =\n providedFields && providedFields.size > 0\n ? providedFields\n : await ObjectRegistry.getAllFields(className);\n\n // Throw error if no fields found\n if (cachedFields.size === 0) {\n // Detect if running in test environment\n const testGlobals = globalThis as {\n describe?: unknown;\n it?: unknown;\n };\n const isTestEnv =\n process.env.NODE_ENV === 'test' ||\n process.env.VITEST === 'true' ||\n typeof testGlobals.describe !== 'undefined' ||\n typeof testGlobals.it !== 'undefined';\n\n const testHint = isTestEnv\n ? `\\n\\n⚠️ Are you using 'smrt test'? ` +\n `Tests require manifest generation.\\n` +\n ` ✅ Use: smrt test\\n` +\n ` ❌ NOT: npx vitest\\n`\n : '';\n\n // Check if class is actually registered (decorator ran but no fields loaded)\n const isRegistered = ObjectRegistry.hasClass(className);\n\n if (isRegistered) {\n // Class registered but no fields - manifest problem\n throw new Error(\n `No field metadata found for class '${className}'. ` +\n `The class is registered (decorator ran) but has no field definitions. ` +\n `This usually means the manifest file is missing or stale.` +\n testHint,\n );\n } else {\n // Class not registered - decorator never ran\n throw new Error(\n `Cannot generate schema for unregistered class '${className}'. ` +\n `Ensure the class is decorated with @smrt() for schema generation to work. ` +\n `Runtime introspection has been removed per issue #131.` +\n testHint,\n );\n }\n }\n\n // Check if class uses STI strategy\n const tableStrategy = ObjectRegistry.getTableStrategy(className);\n\n // Dynamic import SchemaGenerator (Node.js-only, uses node:crypto)\n // This prevents bundling it into browser builds\n const { SchemaGenerator } = await import('./generator.js');\n const generator = new SchemaGenerator();\n const registeredClass = ObjectRegistry.getClass(className);\n const runtimeSchemaConfig = registeredClass?.config\n ? {\n conflictColumns: registeredClass.config.conflictColumns,\n idType: registeredClass.config.idType,\n registry: ObjectRegistry,\n }\n : undefined;\n\n let schemaDefinition: Awaited<\n ReturnType<\n InstanceType<typeof SchemaGenerator>['generateSchemaFromRegistry']\n >\n >;\n\n if (tableStrategy === 'sti') {\n // STI: Generate shared table for base class\n const stiBase = ObjectRegistry.getSTIBase(className);\n\n if (!stiBase) {\n throw new Error(\n `STI strategy detected for '${className}' but no STI base class found. ` +\n `This should not happen - please report this bug.`,\n );\n }\n\n // Only generate schema for the base class (not for children).\n // R5-canon: `getSTIBase` returns the qualified name; compare against\n // the qualified form of this class so an STI base isn't\n // mis-classified as a subclass and skipped.\n const qualifiedClassName =\n registeredClass?.qualifiedName ?? registeredClass?.name ?? className;\n if (qualifiedClassName === stiBase || className === stiBase) {\n // This is the base class - generate STI schema\n schemaDefinition = await generator.generateSTISchemaFromRegistry(\n className,\n tableName,\n cachedFields,\n runtimeSchemaConfig,\n );\n } else {\n // This is a child class - return null or empty schema\n // The base class schema already includes all fields\n // Child classes don't need their own tables\n return ''; // Empty SQL - table already created by base class\n }\n } else {\n // CTI: Generate separate table for each class\n schemaDefinition = generator.generateSchemaFromRegistry(\n className,\n tableName,\n cachedFields,\n runtimeSchemaConfig,\n );\n }\n\n // Store the full schema definition in the registry\n // This is critical for:\n // - STI tables where descendants have additional columns (issue #427)\n // - Per-engine DDL generation via SchemaManager\n if (registeredClass) {\n // Store the full SchemaDefinition for SchemaManager to use\n registeredClass.schema = schemaDefinition;\n // Also store generated DDL for backward compatibility\n registeredClass.schema.ddl = generator.generateSQL(\n schemaDefinition,\n 'sqlite',\n );\n }\n\n return generator.generateSQL(schemaDefinition, options.engine);\n}\n\n/**\n * Ensure schema exists for a registered class.\n *\n * This compatibility helper is kept for tooling flows such as CLI commands that\n * explicitly prepare schema ahead of runtime. Core runtime no longer calls this\n * automatically.\n *\n * @deprecated Prefer explicit migration/bootstrap tooling. Runtime verifies\n * schema and fails fast when tables are missing.\n */\nexport async function ensureSchema(\n db: DatabaseInterface,\n className: string,\n): Promise<void> {\n const registered = ObjectRegistry.getClass(className);\n if (!registered) {\n throw new Error(\n `Cannot ensure schema for unregistered class '${className}'. ` +\n `Ensure the class is decorated with @smrt() and registered in the ObjectRegistry.`,\n );\n }\n\n const tableStrategy = ObjectRegistry.getTableStrategy(className);\n if (tableStrategy === 'sti') {\n const stiBase = ObjectRegistry.getSTIBase(className);\n // R5-canon: `getSTIBase` returns the qualified name. Compare\n // against the qualified form of `className` so an STI base isn't\n // mis-classified as a child and recursed on. Falls back to a\n // simple-name compare for classes without a package context.\n const qualifiedClassName =\n registered.qualifiedName ?? registered.name ?? className;\n if (stiBase && stiBase !== qualifiedClassName && stiBase !== className) {\n await ensureSchema(db, stiBase);\n return;\n }\n }\n\n let schemaDefinition = ObjectRegistry.getSchema(className);\n if (tableStrategy === 'sti') {\n const tableName = ObjectRegistry.getTableName(className);\n if (tableName) {\n const mergedSchema =\n ObjectRegistry.getAllSchemasAsDefinitions()[tableName];\n if (mergedSchema) {\n schemaDefinition = mergedSchema;\n }\n }\n }\n\n if (!schemaDefinition?.tableName) {\n const providedFields =\n registered.fields.size > 0 ? registered.fields : undefined;\n await generateSchema(registered.constructor, providedFields);\n schemaDefinition = ObjectRegistry.getSchema(className);\n\n if (tableStrategy === 'sti') {\n const tableName = ObjectRegistry.getTableName(className);\n if (tableName) {\n const mergedSchema =\n ObjectRegistry.getAllSchemasAsDefinitions()[tableName];\n if (mergedSchema) {\n schemaDefinition = mergedSchema;\n }\n }\n }\n }\n\n if (!schemaDefinition?.tableName) {\n throw new Error(\n `No schema definition found for class '${className}'. ` +\n `Run the manifest generation/build step before preparing schema.`,\n );\n }\n\n // Use the merged table definition for shared tables (especially STI).\n // Per-class schema metadata can reflect only the current class, while\n // the database table must include columns from all classes sharing it.\n const mergedSchemaDefinition =\n ObjectRegistry.getAllSchemasAsDefinitions()[schemaDefinition.tableName];\n const effectiveSchemaDefinition = mergedSchemaDefinition ?? schemaDefinition;\n\n const schemaManager = new SchemaManager(db, {\n skipTriggers:\n typeof (db as { exportTable?: unknown }).exportTable === 'function',\n });\n await schemaManager.ensureTable(effectiveSchemaDefinition);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"utils.js","names":[],"sources":["../../src/schema/utils.ts"],"sourcesContent":["/**\n * Schema generation utilities - Node.js only\n *\n * These functions use SchemaGenerator which depends on node:crypto.\n * Separated from main utils.ts to prevent bundling in browser builds.\n *\n * SMRT handles ALL database maintenance directly.\n * SDK SQL remains a pure query/CRUD layer.\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport type { SmrtObject } from '../object.js';\nimport { ObjectRegistry } from '../registry.js';\nimport type { FieldDefinition } from '../scanner/types.js';\nimport { tableNameFromClass } from '../utils.js';\nimport type { DatabaseEngine } from './ddl/types.js';\nimport { SchemaManager } from './schema-manager.js';\n\nexport {\n materializeManifestDDLForEngine,\n tokenizeSQLDDLBody,\n} from './ddl/materialize-manifest.js';\n// Index-rendering helpers live in a separate file to avoid pulling the\n// heavyweight registry/collection module graph into the DDL strategies.\nexport { isJsonPathIndex, renderIndexTarget } from './index-utils.js';\n// Structured manifest → executable DDL helpers (#2358). Re-exported here for\n// `@happyvertical/smrt-vitest`, which already imports this subpath.\nexport {\n type CollectedManifestTable,\n collectManifestTables,\n type ManifestColumnLike,\n type ManifestIndexLike,\n type ManifestSchemaLike,\n manifestColumnsToDefinitions,\n manifestIndexesToDefinitions,\n manifestSchemaToDefinition,\n mergeSchemaDefinitionInto,\n renderCollectedManifestTable,\n} from './manifest-schema.js';\n\n/**\n * Generates a complete database schema SQL statement for a class\n *\n * This is a thin wrapper around SchemaGenerator that provides the\n * single source of truth for schema generation. Uses ObjectRegistry\n * cached fields from AST manifest for consistent schema generation.\n *\n * **Note**: Uses dynamic import for SchemaGenerator to avoid bundling\n * Node.js-only code (node:crypto) in browser builds.\n *\n * @param ClassType - Class constructor to generate schema for\n * @param providedFields - Optional fields map (used during registration)\n * @returns SQL schema creation statement with CREATE TABLE and CREATE INDEX statements\n */\nexport async function generateSchema(\n // Accepts any SmrtObject constructor: the registry-stored `typeof SmrtObject`\n // as well as collection item classes whose construct signature is narrower\n // than the full static surface. Only `.name` and the construct identity are\n // used here. `never[]` keeps the construct signature contravariantly\n // compatible with constructors that declare their own argument shapes.\n ClassType: { new (...args: never[]): SmrtObject; readonly name: string },\n providedFields?: Map<string, FieldDefinition>,\n options: { engine?: DatabaseEngine } = {},\n) {\n const className = ClassType.name;\n const tableName = tableNameFromClass(ClassType);\n\n // For external packages, ensure manifest is loaded before proceeding\n if (!providedFields || providedFields.size === 0) {\n await ObjectRegistry.ensureManifestLoaded(className);\n }\n\n // Use provided fields if available AND non-empty (during registration), otherwise get from registry\n // NEW: Use getAllFields() to include inherited fields from parent classes\n const cachedFields =\n providedFields && providedFields.size > 0\n ? providedFields\n : await ObjectRegistry.getAllFields(className);\n\n // Throw error if no fields found\n if (cachedFields.size === 0) {\n // Detect if running in test environment\n const testGlobals = globalThis as {\n describe?: unknown;\n it?: unknown;\n };\n const isTestEnv =\n process.env.NODE_ENV === 'test' ||\n process.env.VITEST === 'true' ||\n typeof testGlobals.describe !== 'undefined' ||\n typeof testGlobals.it !== 'undefined';\n\n const testHint = isTestEnv\n ? `\\n\\n⚠️ Are you using 'smrt test'? ` +\n `Tests require manifest generation.\\n` +\n ` ✅ Use: smrt test\\n` +\n ` ❌ NOT: npx vitest\\n`\n : '';\n\n // Check if class is actually registered (decorator ran but no fields loaded)\n const isRegistered = ObjectRegistry.hasClass(className);\n\n if (isRegistered) {\n // Class registered but no fields - manifest problem\n throw new Error(\n `No field metadata found for class '${className}'. ` +\n `The class is registered (decorator ran) but has no field definitions. ` +\n `This usually means the manifest file is missing or stale.` +\n testHint,\n );\n } else {\n // Class not registered - decorator never ran\n throw new Error(\n `Cannot generate schema for unregistered class '${className}'. ` +\n `Ensure the class is decorated with @smrt() for schema generation to work. ` +\n `Runtime introspection has been removed per issue #131.` +\n testHint,\n );\n }\n }\n\n // Check if class uses STI strategy\n const tableStrategy = ObjectRegistry.getTableStrategy(className);\n\n // Dynamic import SchemaGenerator (Node.js-only, uses node:crypto)\n // This prevents bundling it into browser builds\n const { SchemaGenerator } = await import('./generator.js');\n const generator = new SchemaGenerator();\n const registeredClass = ObjectRegistry.getClass(className);\n // Every key the generator reads from `@smrt()` config must be listed here:\n // this bag is rebuilt by hand rather than passed through, so an unlisted\n // option is silently unreachable at runtime while still appearing in the\n // manifest. `indexes` (#2357) was exactly that.\n const runtimeSchemaConfig = registeredClass?.config\n ? {\n conflictColumns: registeredClass.config.conflictColumns,\n idType: registeredClass.config.idType,\n indexes: registeredClass.config.indexes,\n registry: ObjectRegistry,\n }\n : undefined;\n\n let schemaDefinition: Awaited<\n ReturnType<\n InstanceType<typeof SchemaGenerator>['generateSchemaFromRegistry']\n >\n >;\n\n if (tableStrategy === 'sti') {\n // STI: Generate shared table for base class\n const stiBase = ObjectRegistry.getSTIBase(className);\n\n if (!stiBase) {\n throw new Error(\n `STI strategy detected for '${className}' but no STI base class found. ` +\n `This should not happen - please report this bug.`,\n );\n }\n\n // Only generate schema for the base class (not for children).\n // R5-canon: `getSTIBase` returns the qualified name; compare against\n // the qualified form of this class so an STI base isn't\n // mis-classified as a subclass and skipped.\n const qualifiedClassName =\n registeredClass?.qualifiedName ?? registeredClass?.name ?? className;\n if (qualifiedClassName === stiBase || className === stiBase) {\n // This is the base class - generate STI schema\n schemaDefinition = await generator.generateSTISchemaFromRegistry(\n className,\n tableName,\n cachedFields,\n runtimeSchemaConfig,\n );\n } else {\n // This is a child class - return null or empty schema\n // The base class schema already includes all fields\n // Child classes don't need their own tables\n return ''; // Empty SQL - table already created by base class\n }\n } else {\n // CTI: Generate separate table for each class\n schemaDefinition = generator.generateSchemaFromRegistry(\n className,\n tableName,\n cachedFields,\n runtimeSchemaConfig,\n );\n }\n\n // Store the full schema definition in the registry\n // This is critical for:\n // - STI tables where descendants have additional columns (issue #427)\n // - Per-engine DDL generation via SchemaManager\n if (registeredClass) {\n // Store the full SchemaDefinition for SchemaManager to use\n registeredClass.schema = schemaDefinition;\n // Also store generated DDL for backward compatibility\n registeredClass.schema.ddl = generator.generateSQL(\n schemaDefinition,\n 'sqlite',\n );\n }\n\n return generator.generateSQL(schemaDefinition, options.engine);\n}\n\n/**\n * Ensure schema exists for a registered class.\n *\n * This compatibility helper is kept for tooling flows such as CLI commands that\n * explicitly prepare schema ahead of runtime. Core runtime no longer calls this\n * automatically.\n *\n * @deprecated Prefer explicit migration/bootstrap tooling. Runtime verifies\n * schema and fails fast when tables are missing.\n */\nexport async function ensureSchema(\n db: DatabaseInterface,\n className: string,\n): Promise<void> {\n const registered = ObjectRegistry.getClass(className);\n if (!registered) {\n throw new Error(\n `Cannot ensure schema for unregistered class '${className}'. ` +\n `Ensure the class is decorated with @smrt() and registered in the ObjectRegistry.`,\n );\n }\n\n const tableStrategy = ObjectRegistry.getTableStrategy(className);\n if (tableStrategy === 'sti') {\n const stiBase = ObjectRegistry.getSTIBase(className);\n // R5-canon: `getSTIBase` returns the qualified name. Compare\n // against the qualified form of `className` so an STI base isn't\n // mis-classified as a child and recursed on. Falls back to a\n // simple-name compare for classes without a package context.\n const qualifiedClassName =\n registered.qualifiedName ?? registered.name ?? className;\n if (stiBase && stiBase !== qualifiedClassName && stiBase !== className) {\n await ensureSchema(db, stiBase);\n return;\n }\n }\n\n let schemaDefinition = ObjectRegistry.getSchema(className);\n if (tableStrategy === 'sti') {\n const tableName = ObjectRegistry.getTableName(className);\n if (tableName) {\n const mergedSchema =\n ObjectRegistry.getAllSchemasAsDefinitions()[tableName];\n if (mergedSchema) {\n schemaDefinition = mergedSchema;\n }\n }\n }\n\n if (!schemaDefinition?.tableName) {\n const providedFields =\n registered.fields.size > 0 ? registered.fields : undefined;\n await generateSchema(registered.constructor, providedFields);\n schemaDefinition = ObjectRegistry.getSchema(className);\n\n if (tableStrategy === 'sti') {\n const tableName = ObjectRegistry.getTableName(className);\n if (tableName) {\n const mergedSchema =\n ObjectRegistry.getAllSchemasAsDefinitions()[tableName];\n if (mergedSchema) {\n schemaDefinition = mergedSchema;\n }\n }\n }\n }\n\n if (!schemaDefinition?.tableName) {\n throw new Error(\n `No schema definition found for class '${className}'. ` +\n `Run the manifest generation/build step before preparing schema.`,\n );\n }\n\n // Use the merged table definition for shared tables (especially STI).\n // Per-class schema metadata can reflect only the current class, while\n // the database table must include columns from all classes sharing it.\n const mergedSchemaDefinition =\n ObjectRegistry.getAllSchemasAsDefinitions()[schemaDefinition.tableName];\n const effectiveSchemaDefinition = mergedSchemaDefinition ?? schemaDefinition;\n\n const schemaManager = new SchemaManager(db, {\n skipTriggers:\n typeof (db as { exportTable?: unknown }).exportTable === 'function',\n });\n await schemaManager.ensureTable(effectiveSchemaDefinition);\n}\n"],"mappings":"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"}
|
package/dist/schema.js
CHANGED
|
@@ -6,8 +6,11 @@ import { SQLiteStrategy } from "./schema/ddl/sqlite-strategy.js";
|
|
|
6
6
|
import { detectEngine, generateDDLForEngine, generateMultiEngineDDL, getAllStrategies, getDDLStrategy } from "./schema/ddl/index.js";
|
|
7
7
|
import { SchemaGenerator } from "./schema/generator.js";
|
|
8
8
|
import { SchemaManager, createSchemaManager } from "./schema/schema-manager.js";
|
|
9
|
+
import { collectManifestTables, manifestColumnsToDefinitions, manifestIndexesToDefinitions, manifestSchemaToDefinition, mergeSchemaDefinitionInto, renderCollectedManifestTable } from "./schema/manifest-schema.js";
|
|
10
|
+
import { getSystemTableShapes, parseTableShapes } from "./schema/system-table-shapes.js";
|
|
11
|
+
import { LiveSchemaParityError, checkLiveSchemaParity, normalizeSqlType, parseIndexDefColumns } from "./schema/live-parity.js";
|
|
9
12
|
import { SchemaCodeGenerator } from "./schema/code-generator.js";
|
|
10
13
|
import { SchemaOverrideSystem } from "./schema/override-system.js";
|
|
11
14
|
import { SchemaAggregator } from "./schema/schema-aggregator.js";
|
|
12
15
|
import "./schema/index.js";
|
|
13
|
-
export { BaseDDLStrategy, DuckDBStrategy, JsonDuckDBStrategy, PostgresStrategy, SQLiteStrategy, SchemaAggregator, SchemaCodeGenerator, SchemaGenerator, SchemaManager, SchemaOverrideSystem, createSchemaManager, detectEngine, generateDDLForEngine, generateMultiEngineDDL, getAllStrategies, getDDLStrategy };
|
|
16
|
+
export { BaseDDLStrategy, DuckDBStrategy, JsonDuckDBStrategy, LiveSchemaParityError, PostgresStrategy, SQLiteStrategy, SchemaAggregator, SchemaCodeGenerator, SchemaGenerator, SchemaManager, SchemaOverrideSystem, checkLiveSchemaParity, collectManifestTables, createSchemaManager, detectEngine, generateDDLForEngine, generateMultiEngineDDL, getAllStrategies, getDDLStrategy, getSystemTableShapes, manifestColumnsToDefinitions, manifestIndexesToDefinitions, manifestSchemaToDefinition, mergeSchemaDefinitionInto, normalizeSqlType, parseIndexDefColumns, parseTableShapes, renderCollectedManifestTable };
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,16 +3,18 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-core",
|
|
6
|
-
"packageVersion": "0.40.
|
|
6
|
+
"packageVersion": "0.40.70",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
12
|
-
"agents": "
|
|
10
|
+
"manifest": "314596dcbe22e4d189c0771339f4884dc57491b67628170633e49cc48497fa5b",
|
|
11
|
+
"packageJson": "629a1ab5419e67ef36a95b91044ad102ca12c39f2046015899971be5366df31b",
|
|
12
|
+
"agents": "8bb5be8f5b8e6b8a817bcc756010753fbb93513480182c03c09db6330f29c046",
|
|
13
13
|
"moduleDoc:agents/change-feed.md": "5278797d6c049ea21071b011182a9fcd79d17f9d74e0f90a93caa4c05c25294f",
|
|
14
14
|
"moduleDoc:agents/change-signals.md": "d9cb6a5541728ffea46607a6b1d4fa61d4621849f2b4ea86a0645fbb0af892e9",
|
|
15
|
-
"moduleDoc:agents/generators.md": "2b6ccd5ff557293f2ce84d25cc254ceb518f4c274d93b7e3175985b18f36f564"
|
|
15
|
+
"moduleDoc:agents/generators.md": "2b6ccd5ff557293f2ce84d25cc254ceb518f4c274d93b7e3175985b18f36f564",
|
|
16
|
+
"moduleDoc:agents/schema-paths.md": "750d35be156b1c99d1cda5be0942d5831b4dfc5b44097a203353f6e9ce1338f3",
|
|
17
|
+
"moduleDoc:agents/query-bounds.md": "3a0601ddaf2bea4e90e3f22e16a2eb3508a6fb8c019e3fb2c1f930c90a377cd5"
|
|
16
18
|
},
|
|
17
19
|
"exports": [
|
|
18
20
|
".",
|
|
@@ -911,7 +913,7 @@
|
|
|
911
913
|
"polymorphicAssociations": 1,
|
|
912
914
|
"uuidColumns": 3
|
|
913
915
|
},
|
|
914
|
-
"agentDoc": "# @happyvertical/smrt-core\n\nORM, code generation, AI integration, and the DispatchBus. Everything else builds on this.\n\nKey surfaces are `SmrtObject`, `SmrtCollection`, `ObjectRegistry`,\n`DispatchBus`, `GlobalInterceptors`, and `LearningMemory`; this file documents\ntheir invariants and source locations, and the module docs below cover the\nper-subsystem semantics.\n\n## Modules\n\nSubsystem semantics live in sibling module docs — read the one for the\nsubsystem you are editing. This file keeps what holds across all of them.\n\n| Module | Scope | Module doc |\n|---|---|---|\n| `src/change-feed.ts` | the adapter-agnostic change-observation spine — `_smrt_changes`, cursors, table versions, generated `_changes` routes, retention | [agents/change-feed.md](agents/change-feed.md) |\n| `src/change-signals.ts` + the generated `_events` SSE route | the push companion to the change feed — the signal bus, cross-replica fan-out, the SSE route, and its documented gaps | [agents/change-signals.md](agents/change-signals.md) |\n| `src/generators/` + `src/vite-plugin/web-collections.ts` | REST/CLI/MCP/web-collection generation, the `manifestHash` emission sites, and generated conditional-GET / ETag v2 semantics | [agents/generators.md](agents/generators.md) |\n\n## SmrtObject Lifecycle\n\n`constructor(options)` → `initialize()` → ready for `save()`/`delete()`/`loadFromId()`\n\n- `initialize()`: loads field initializers, applies option values (options override initializers), loads from DB if id/slug provided\n- `save()`: upsert with STI validation, interceptor execution, auto-embeddings. Persisted objects (`isPersisted` — set by DB hydration and successful saves) upsert on `['id']` so natural-key edits (e.g. slug renames) update in place; new objects upsert on the natural-key conflict columns for ingestion-style dedup (#1472)\n- `is(criteria)` / `do(instructions)` / `describe()`: AI operations via function calling. They inject the object's own `toPublicJSON()` (sensitive fields stripped) as a \"content body\" so the model reasons over the instance. Options: `includeData: false` skips injection (for callers that already curate the relevant fields into the instruction); `maxDataLength` overrides the truncation budget. Neither key is forwarded to `ai.message()`. (#1567)\n- `getSlug()`: auto-generates from name → title → label → id\n- `loadRelated(fieldName)`: lazy-loads relationships (cached in `_loadedRelationships` Map)\n\n## LearningMemory (#1886)\n\n`LearningMemory` provides tenant-isolated, confidence-scored recall over\n`_smrt_contexts` plus optional injected semantic search. `capture()` reinforces\nsuccesses and decays failures while updating outcome counters; `recall()`\napplies confidence, expiry, time-decay, and hierarchical-scope filters and\nrefreshes `last_used_at`. Keep semantic search behind the\n`SmrtCollection.semanticSearch`-compatible injection boundary.\n\n## SmrtCollection Query\n\n```typescript\nawait collection.list({\n where: { status: 'active', 'price >': 10 },\n limit: 50, offset: 0, orderBy: 'created_at DESC'\n});\n```\n\nProjection primitive (#1902): pass `select: ['id', 'title', 'tenantId']` to\n`list()` when an admin/list workflow needs compact rows. `select` uses SMRT\nfield names, maps them to DB columns internally, and returns plain objects keyed\nby the same SMRT field names without hydrating `SmrtObject` instances. It\ncomposes with `where`, `orderBy`, `limit`, and `offset`; `beforeList`\ninterceptors still run. It is for column-backed fields only and cannot combine\nwith `include`/relationship eager loading.\n\n`list()` and `query()` hydrate model instances serially in result order because\nan `initialize()` hook may query through the same transaction-bound PostgreSQL\nclient. Keep this serialization invariant; use `select` when callers need plain\nrows without model hydration.\n\n**WHERE operators**: `=`, `>`, `<`, `>=`, `<=`, `!=`, `in`, `not in`, `like`.\nArrays auto-detect `IN`. NULL is a value, not an operator: `{ deletedAt: null }`\nrenders `IS NULL` and `{ 'deletedAt !=': null }` renders `IS NOT NULL`.\n\nThis list is the set `@happyvertical/sql`'s `buildWhere` can execute, and\n`convertWhereKeys` accepts nothing outside it — an operator accepted here but\nunknown there fails inside the query builder, after the API said the query was\nvalid (#2276). Two entries were removed for that reason and now reject at the\nAPI boundary: `contains` (never existed in the SQL layer; use `like` with\nexplicit wildcards) and dot-notation JSON paths such as `metadata.userId` (never\nrewritten into an extraction expression, so they reached SQL as qualified column\nreferences). Re-adding either requires the query builder to support it first;\n`src/__tests__/issue-2276-where-contract.test.ts` executes every accepted\noperator against a database to keep the two in step.\n\nSTI child collections auto-filter by `_meta_type`.\n\n## Bounded Collection Read Plans\n\nUse `executeCollectionReadPlan()` when one operation needs several independent\ncollections. It bounds top-level `collection.list()` concurrency while keeping\nall reads on the normal registry/collection path. Callers must choose an\nexplicit positive `maxConcurrency` and pass their normal shared\n`collectionOptions` when database or tenant context matters.\n\nThe executor deliberately does not compose SQL, cache the plan, or change pool\ndefaults. On failure it stops starting queued entries, drains operations already\nin flight, and rethrows the first error.\n\n## Object Memory & Semantic Search\n\nTwo persistence primitives every `SmrtObject`/`SmrtCollection` inherits — load-bearing for learning agents, usable by any object. Full guide: `docs/content/core.md` → \"Context Memory System\".\n\n- **Context memory** (`remember`/`recall`/`recallAll`/`forget`/`forgetScope`, table `_smrt_contexts`): stores any JSON value keyed by `(owner_class, owner_id, scope, key, version)` with a `confidence` score (0–1) and a stored `expiresAt` (metadata — `recall()` does **not** filter expired rows; expiry is caller-managed). `recall()` returns the highest-confidence match with an optional `minConfidence` floor and **opt-in** hierarchical scope fallback (`includeAncestors: true` → `'a/b/c' → 'a/b' → 'a' → 'global'`; default off); `recallAll()` returns a `Map`. Typical use: cache a learned strategy (e.g. a working selector per host) and reuse it across sessions. `success_count`/`failure_count` columns exist for outcome-weighting: `SmrtObject.remember()` leaves them untouched, `SmrtCollection.remember()` resets them to zero, and neither recall path updates them. `LearningMemory` is the layer that maintains them (and that does filter expired rows).\n- **Semantic search** (on `SmrtCollection`, table `_smrt_embeddings`): `semanticSearch(query)`, `findSimilar(object)`, `findSimilarToEmbedding(vector)` — cosine ranking over embeddings of the fields declared in `@smrt({ embeddings })`. Native pgvector/HNSW when configured, in-memory `CosineSimilarity` fallback otherwise; default local model `Xenova/bge-base-en-v1.5` (768-dim) or AI `text-embedding-3-small`. Hits hydrate via `list({ 'id in': … })`, so `@TenantScoped` isolation applies to results.\n\n## @smrt() Decorator Options\n\nKey options: `tableName`, `tableStrategy` ('cti'|'sti'), `conflictColumns`, `api`/`mcp`/`cli` (generation config), `ai` (callable methods), `hooks` (beforeSave/afterSave/beforeDelete/afterDelete), `embeddings` (auto-generate), `tenantScoped`, `agent`, `ui` (`{ icon, label, description }` — nav/help hints round-tripped through the manifest as plain data; `description` is the object-level seed for form-level help, #2046).\n\nRegistration sets `SMRT_TABLE_NAME` static property (survives minification).\n\n## @field() UI hints (#2046)\n\n`@field({ ui: { basic, group, order, locked } })` — a static, presentation-only\nseed for the field-policy rail (epic #2045). Carried in the manifest under the\nfield's `_meta.ui` (never a top-level `FieldDefinition` key), readable at\nruntime via `getAllFields()` at `field._meta.ui`, and emitted (sanitized) with\n`description` into generated web-collection definitions and browser MCP tool\nschemas. No schema/persistence/security effect — `sensitive`/`readPermission`\nstay the security rail, and `sensitive`/`transient` fields never emit to the\nclient at all.\n\n## Domain Knowledge Artifacts\n\n`smrtPlugin()` writes runtime manifests and agent/developer knowledge artifacts:\n\n- local dev/build: `.smrt/manifest.json` and `.smrt/smrt-knowledge.json`\n- package build: `dist/manifest.json` and `dist/smrt-knowledge.json`\n\nKeep `manifest.json` runtime-focused. `smrt-knowledge.json` is the deterministic\nagent contract for downstream review and architecture tools.\n\nThe schema-version-1 object projection is additive and high-signal: it retains\nnormalized tenant mode/field, explicit `cti`/`sti` strategy, conflict columns,\nmethod signatures, and field defaults/constraints/readonly/transient flags.\nSensitive fields are removed before both `fields` and `relationships` are\nderived, including legacy flags stored under `_meta`; matching field and\nsnake-case column names are also removed from projected conflict columns, and a\nsensitive custom tenant field is omitted while retaining scope and mode.\nGenerated artifacts assert this boundary with `sensitiveFieldsExcluded: true`;\nthe optional marker keeps schema version 1 additive while letting readers\nidentify older artifacts that require raw-manifest corroboration.\n\nConfig precedence for knowledge is defaults → top-level `knowledge` in\n`smrt.config.ts` → `packages[packageName].knowledge` → plugin option →\nobject-level `@smrt({ knowledge })`.\n\nObject-level `knowledge: false` excludes an object from authored context only;\nit must not change runtime manifest registration. Use\n`knowledge: { tags, summary, risks }` for review-sensitive domain objects.\n\nHTTP knowledge routes are disabled by default. If `knowledge.api.enabled` is\ntrue, generated SvelteKit routes must stay GET-only and guarded by dev mode or\nadmin auth.\n\n## DispatchBus\n\n- `emit(signalType, payload, metadata)` → creates persistent Dispatch record\n- `on(pattern, handler)` → in-memory handler (immediate)\n- `subscribe({ signalType, subscriber })` → persistent subscription (survives restarts)\n- `process(subscriberName, handler)` → process pending dispatches\n- Wildcards: `campaign.*` matches `campaign.completed` (single segment only)\n- Tables: `_smrt_dispatch`, `_smrt_dispatch_subscriptions`\n- Status: `pending → processing → completed` (or `failed`)\n\n## Single Table Inheritance (STI)\n\n- Base: `@smrt({ tableStrategy: 'sti' })` — children inherit, share one table\n- Discriminator: `_meta_type` column with qualified names (`@happyvertical/smrt-content:Article`)\n- Child fields: `@meta()` decorator → stored in `_meta_data` JSONB (not as columns)\n- Polymorphic queries: collection loads `_meta_type`, creates correct subclass dynamically\n- Validation: fail-fast on save if `_meta_type` missing or mismatched\n\n## Child Accessors (R10)\n\n`src/child-accessors.ts` installs a consistent `get<FieldName>()` instance method for every `@oneToMany` field at `@smrt()` registration time (e.g. `@oneToMany('OrderItem') items` → `order.getItems()`), delegating to `loadRelatedMany`. Two invariants:\n\n- **Additive** — never overwrites a hand-rolled method of the same name (checks the whole prototype chain). `Profile.getMetadata()` (key-value) and `ProfileRelationship.getTerms()` are preserved.\n- **Runtime-only** — attached to the prototype, invisible to the build-time manifest, so it never leaks into the REST/CLI/MCP surface.\n\nWhen the target declares multiple FKs back to the parent, annotate `@oneToMany(Target, { foreignKey: '<inverseField>' })`; `loadRelatedMany` and the eager `include:` loader both honor it (else first-match).\n\n## Vite Plugin\n\n```typescript\n// vite.config.ts — required for @smrt() decorators (Vite 8+, oxc transform)\nexport default defineConfig({\n oxc: {\n decorator: {\n legacy: true,\n emitDecoratorMetadata: true,\n },\n },\n});\n```\n\nUnder Vite 8 the oxc transform does not honor the pre-Vite-8 `esbuild.tsconfigRaw`\nrecipe (or tsconfig `experimentalDecorators` reached through SvelteKit's\n`extends \"./.svelte-kit/tsconfig.json\"` chain), so that recipe throws\n`SyntaxError: Invalid or unexpected token` on the first SSR request. Configure\ndecorators through `oxc.decorator` instead. Consumers still pinned on vite<8 need\nthe legacy `esbuild.tsconfigRaw` form with `experimentalDecorators: true,\nemitDecoratorMetadata: true`.\n\nFor independent CI invocations, both `smrtPlugin()` and `smrtConsumer()` accept\nthe same `generationSnapshot: { path, sha256, provenance, sourceRoot }`. The\nschema-v1 snapshot produced by `serializeSmrtGenerationSnapshot()` contains the\nmerged project/dependency manifest, portable source paths, and source-file\ndigests; each plugin selects its own view. Reuse mode fails closed on\nbyte/provenance/path/content drift, skips scans and manifest writes, and still\ngenerates routes, types, registration, and virtual modules. Omit it for normal\nlocal development and watch mode.\n\n## Gotchas\n\n- **Filesystem support is a lazy boundary (#1979)**: `SmrtClass` acquires `options.fs` adapters via `createFilesystemAdapter()` (`src/filesystem-loader.ts`), never a static `@happyvertical/files` import — the files SDK statically pulls @aws-sdk/client-s3 and reaches googleapis, and a static edge here would land it in every downstream SSR bundle. Node/tsx/vite-dev runtimes resolve it on first use; fully-bundled deployments import `@happyvertical/smrt-core/filesystem` at startup. Use `importOptionalDependency()` (`src/lazy-external.ts`) for any similar optional heavyweight dependency.\n- **Never override toJSON()** — handles STI discriminator + meta field extraction. Use `transformJSON()`\n- **Property init order**: TypeScript initializers run first, then `initialize()` applies option values (options win)\n- **No runtime schema creation**: application tables must be prepared explicitly via migrations/tooling; runtime only verifies and fails clearly\n- **Retry logic**: `db.get()` (3 retries, 250ms) and `db.upsert()` (3 retries, 500ms) have built-in retry\n- **Field caching**: `_cachedFields` populated during `Collection.create()` — eliminates async `getFields()` per query\n- **Smart cloning**: arrays/objects shallow-cloned in property init to prevent aliasing (Issue #22)\n- **Table verification cache**: `isTableVerified(dbUrl, tableName)` avoids redundant `tableExists()` calls\n- **Manifest required**: build-time AST scanning creates manifest. Without vitest plugin → \"No field metadata\"\n- **ManifestBuilder fails on scanner errors**: every production manifest path\n must abort before adapting partial scan results. A syntax error or unresolved\n `@smrt()` config spread cannot be allowed to emit a default-open manifest.\n- **Vite plugin loads scanner from `dist/` first**: `src/vite-plugin/import-build-aware.ts` prefers `dist/` when it exists on disk; it only falls back to `src/` on fresh clones. So if you edit `src/scanner/*.ts` or `src/schema/generator.ts` and want those edits reflected in consumer manifest generation, you must rebuild (`pnpm build` or have `pnpm dev` / `pnpm build:watch` running in core). This is intentional — sniffing `.ts` vs `.js` via `import.meta.url` was non-deterministic under tsx and broke 12–13 publishes (#1139).\n- **Bundled registry ownership**: flattened production bundles can rewrite constructor names and make decorator-time stack inference attribute provider code to the consumer. Generated registration repairs identity only from the exact imported constructor plus an explicit package and isolated one-object manifest; never infer ownership from output paths, simple names, or table names. Distinct packages may export the same simple name under qualified keys. The production-consumer gate lives in `packages/bundle-gate/src/__tests__/registry-identity.spec.ts` (#2308).\n",
|
|
916
|
+
"agentDoc": "# @happyvertical/smrt-core\n\nORM, code generation, AI integration, and the DispatchBus. Everything else builds on this.\n\nKey surfaces are `SmrtObject`, `SmrtCollection`, `ObjectRegistry`,\n`DispatchBus`, `GlobalInterceptors`, and `LearningMemory`; this file documents\ntheir invariants and source locations, and the module docs below cover the\nper-subsystem semantics.\n\n## Modules\n\nSubsystem semantics live in sibling module docs — read the one for the\nsubsystem you are editing. This file keeps what holds across all of them.\n\n| Module | Scope | Module doc |\n|---|---|---|\n| `src/change-feed.ts` | the adapter-agnostic change-observation spine — `_smrt_changes`, cursors, table versions, generated `_changes` routes, retention | [agents/change-feed.md](agents/change-feed.md) |\n| `src/change-signals.ts` + the generated `_events` SSE route | the push companion to the change feed — the signal bus, cross-replica fan-out, the SSE route, and its documented gaps | [agents/change-signals.md](agents/change-signals.md) |\n| `src/generators/` + `src/vite-plugin/web-collections.ts` | REST/CLI/MCP/web-collection generation, the `manifestHash` emission sites, and generated conditional-GET / ETag v2 semantics | [agents/generators.md](agents/generators.md) |\n| `src/schema/` | the five `SchemaGenerator` entry points, which two reach production, why schema drift stayed invisible, and the #2382 index/tenancy rules | [agents/schema-paths.md](agents/schema-paths.md) |\n\n## SmrtObject Lifecycle\n\n`constructor(options)` → `initialize()` → ready for `save()`/`delete()`/`loadFromId()`\n\n- `initialize()`: loads field initializers, applies option values (options override initializers), loads from DB if id/slug provided\n- `save()`: upsert with STI validation, interceptor execution, auto-embeddings. Persisted objects (`isPersisted` — set by DB hydration and successful saves) upsert on `['id']` so natural-key edits (e.g. slug renames) update in place; new objects upsert on the natural-key conflict columns for ingestion-style dedup (#1472)\n- `is(criteria)` / `do(instructions)` / `describe()`: AI operations via function calling. They inject the object's own `toPublicJSON()` (sensitive fields stripped) as a \"content body\" so the model reasons over the instance. Options: `includeData: false` skips injection (for callers that already curate the relevant fields into the instruction); `maxDataLength` overrides the truncation budget. Neither key is forwarded to `ai.message()`. (#1567)\n- `save()` error contract (#2366): unique/PK violation → `ValidationError` `VALIDATION_UNIQUE_CONSTRAINT`, NOT NULL → `VALIDATION_REQUIRED_FIELD`, both on the first attempt on every adapter; any other database failure → `DatabaseError` with the driver error on `cause`\n- `getSlug()`: auto-generates from name → title → label → id\n- `loadRelated(fieldName)`: lazy-loads relationships (cached in `_loadedRelationships` Map)\n\n## LearningMemory (#1886)\n\n`LearningMemory` provides tenant-isolated, confidence-scored recall over\n`_smrt_contexts` plus optional injected semantic search. `capture()` reinforces\nsuccesses and decays failures while updating outcome counters; `recall()`\napplies confidence, expiry, time-decay, and hierarchical-scope filters and\nrefreshes `last_used_at`. Keep semantic search behind the\n`SmrtCollection.semanticSearch`-compatible injection boundary.\n\n## SmrtCollection Query\n\n```typescript\nawait collection.list({\n where: { status: 'active', 'price >': 10 },\n limit: 50, offset: 0, orderBy: 'created_at DESC'\n});\n```\n\nProjection primitive (#1902): pass `select: ['id', 'title', 'tenantId']` to\n`list()` when an admin/list workflow needs compact rows. `select` uses SMRT\nfield names, maps them to DB columns internally, and returns plain objects keyed\nby the same SMRT field names without hydrating `SmrtObject` instances. It\ncomposes with `where`, `orderBy`, `limit`, and `offset`; `beforeList`\ninterceptors still run. It is for column-backed fields only and cannot combine\nwith `include`/relationship eager loading.\n\n`list()` and `query()` hydrate model instances serially in result order because\nan `initialize()` hook may query through the same transaction-bound PostgreSQL\nclient. Keep this serialization invariant; use `select` when callers need plain\nrows without model hydration.\n\n**WHERE operators**: `=`, `>`, `<`, `>=`, `<=`, `!=`, `in`, `not in`, `like`.\nArrays auto-detect `IN`. NULL is a value, not an operator: `{ deletedAt: null }`\nrenders `IS NULL` and `{ 'deletedAt !=': null }` renders `IS NOT NULL`.\n\nThis list is the set `@happyvertical/sql`'s `buildWhere` can execute, and\n`convertWhereKeys` accepts nothing outside it — an operator accepted here but\nunknown there fails inside the query builder, after the API said the query was\nvalid (#2276). Two entries were removed for that reason and now reject at the\nAPI boundary: `contains` (never existed in the SQL layer; use `like` with\nexplicit wildcards) and dot-notation JSON paths such as `metadata.userId` (never\nrewritten into an extraction expression, so they reached SQL as qualified column\nreferences). Re-adding either requires the query builder to support it first;\n`src/__tests__/issue-2276-where-contract.test.ts` executes every accepted\noperator against a database to keep the two in step.\n\nSTI child collections auto-filter by `_meta_type`. Query bounds — `LIMIT 1` on `get()`, the `limit`/`offset` parser, the `orderBy` whitelist and sensitive/permission refusals, and the deterministic generated-list ordering (#2367) — are in [agents/query-bounds.md](agents/query-bounds.md).\n\n## Bounded Collection Read Plans\n\nUse `executeCollectionReadPlan()` when one operation needs several independent\ncollections. It bounds top-level `collection.list()` concurrency while keeping\nall reads on the normal registry/collection path. Callers must choose an\nexplicit positive `maxConcurrency` and pass their normal shared\n`collectionOptions` when database or tenant context matters.\n\nThe executor deliberately does not compose SQL, cache the plan, or change pool\ndefaults. On failure it stops starting queued entries, drains operations already\nin flight, and rethrows the first error.\n\n## Object Memory & Semantic Search\n\nTwo persistence primitives every `SmrtObject`/`SmrtCollection` inherits — load-bearing for learning agents, usable by any object. Full guide: `docs/content/core.md` → \"Context Memory System\".\n\n- **Context memory** (`remember`/`recall`/`recallAll`/`forget`/`forgetScope`, table `_smrt_contexts`): stores any JSON value keyed by `(owner_class, owner_id, scope, key, version)` with a `confidence` score (0–1) and a stored `expiresAt` (metadata — `recall()` does **not** filter expired rows; expiry is caller-managed). `recall()` returns the highest-confidence match with an optional `minConfidence` floor and **opt-in** hierarchical scope fallback (`includeAncestors: true` → `'a/b/c' → 'a/b' → 'a' → 'global'`; default off); `recallAll()` returns a `Map`. Typical use: cache a learned strategy (e.g. a working selector per host) and reuse it across sessions. `success_count`/`failure_count` columns exist for outcome-weighting: `SmrtObject.remember()` leaves them untouched, `SmrtCollection.remember()` resets them to zero, and neither recall path updates them. `LearningMemory` is the layer that maintains them (and that does filter expired rows).\n- **Semantic search** (on `SmrtCollection`, table `_smrt_embeddings`): `semanticSearch(query)`, `findSimilar(object)`, `findSimilarToEmbedding(vector)` — cosine ranking over embeddings of the fields declared in `@smrt({ embeddings })`. Native pgvector/HNSW when configured, in-memory `CosineSimilarity` fallback otherwise; default local model `Xenova/bge-base-en-v1.5` (768-dim) or AI `text-embedding-3-small`. Hits hydrate via `list({ 'id in': … })`, so `@TenantScoped` isolation applies to results.\n\n## @smrt() Decorator Options\n\nKey options: `tableName`, `tableStrategy` ('cti'|'sti'), `conflictColumns`, `indexes` (declared multi-column indexes, #2357 — see \"Schema paths\"), `api`/`mcp`/`cli` (generation config), `ai` (callable methods), `hooks` (beforeSave/afterSave/beforeDelete/afterDelete), `embeddings` (auto-generate), `tenantScoped`, `agent`, `ui` (`{ icon, label, description }` — nav/help hints round-tripped through the manifest as plain data; `description` is the object-level seed for form-level help, #2046).\n\nRegistration sets `SMRT_TABLE_NAME` static property (survives minification).\n\n## @field() UI hints (#2046)\n\n`@field({ ui: { basic, group, order, locked } })` — a static, presentation-only\nseed for the field-policy rail (epic #2045). Carried in the manifest under the\nfield's `_meta.ui` (never a top-level `FieldDefinition` key), readable at\nruntime via `getAllFields()` at `field._meta.ui`, and emitted (sanitized) with\n`description` into generated web-collection definitions and browser MCP tool\nschemas. No schema/persistence/security effect — `sensitive`/`readPermission`\nstay the security rail, and `sensitive`/`transient` fields never emit to the\nclient at all.\n\n## Domain Knowledge Artifacts\n\n`smrtPlugin()` writes runtime manifests and agent/developer knowledge artifacts:\n\n- local dev/build: `.smrt/manifest.json` and `.smrt/smrt-knowledge.json`\n- package build: `dist/manifest.json` and `dist/smrt-knowledge.json`\n\nKeep `manifest.json` runtime-focused. `smrt-knowledge.json` is the deterministic\nagent contract for downstream review and architecture tools.\n\nThe schema-version-1 object projection is additive and high-signal: it retains\nnormalized tenant mode/field, explicit `cti`/`sti` strategy, conflict columns,\nmethod signatures, and field defaults/constraints/readonly/transient flags.\nSensitive fields are removed before both `fields` and `relationships` are\nderived, including legacy flags stored under `_meta`; matching field and\nsnake-case column names are also removed from projected conflict columns, and a\nsensitive custom tenant field is omitted while retaining scope and mode.\nGenerated artifacts assert this boundary with `sensitiveFieldsExcluded: true`;\nthe optional marker keeps schema version 1 additive while letting readers\nidentify older artifacts that require raw-manifest corroboration.\n\nConfig precedence for knowledge is defaults → top-level `knowledge` in\n`smrt.config.ts` → `packages[packageName].knowledge` → plugin option →\nobject-level `@smrt({ knowledge })`.\n\nObject-level `knowledge: false` excludes an object from authored context only;\nit must not change runtime manifest registration. Use\n`knowledge: { tags, summary, risks }` for review-sensitive domain objects.\n\nHTTP knowledge routes are disabled by default. If `knowledge.api.enabled` is\ntrue, generated SvelteKit routes must stay GET-only and guarded by dev mode or\nadmin auth.\n\n## DispatchBus\n\n- `emit(signalType, payload, metadata)` → creates persistent Dispatch record\n- `on(pattern, handler)` → in-memory handler (immediate)\n- `subscribe({ signalType, subscriber })` → persistent subscription (survives restarts)\n- `process(subscriberName, handler)` → process pending dispatches\n- Wildcards: `campaign.*` matches `campaign.completed` (single segment only)\n- Tables: `_smrt_dispatch`, `_smrt_dispatch_subscriptions`\n- Status: `pending → processing → completed` (or `failed`)\n\n## Single Table Inheritance (STI)\n\n- Base: `@smrt({ tableStrategy: 'sti' })` — children inherit, share one table\n- Discriminator: `_meta_type` column with qualified names (`@happyvertical/smrt-content:Article`)\n- Child fields: `@meta()` decorator → stored in `_meta_data` JSONB (not as columns)\n- Polymorphic queries: collection loads `_meta_type`, creates correct subclass dynamically\n- Validation: fail-fast on save if `_meta_type` missing or mismatched\n\n## Child Accessors (R10)\n\n`src/child-accessors.ts` installs a consistent `get<FieldName>()` instance method for every `@oneToMany` field at `@smrt()` registration time (e.g. `@oneToMany('OrderItem') items` → `order.getItems()`), delegating to `loadRelatedMany`. Two invariants:\n\n- **Additive** — never overwrites a hand-rolled method of the same name (checks the whole prototype chain). `Profile.getMetadata()` (key-value) and `ProfileRelationship.getTerms()` are preserved.\n- **Runtime-only** — attached to the prototype, invisible to the build-time manifest, so it never leaks into the REST/CLI/MCP surface.\n\nWhen the target declares multiple FKs back to the parent, annotate `@oneToMany(Target, { foreignKey: '<inverseField>' })`; `loadRelatedMany` and the eager `include:` loader both honor it (else first-match).\n\n## Vite Plugin\n\n```typescript\n// vite.config.ts — required for @smrt() decorators (Vite 8+, oxc transform)\nexport default defineConfig({\n oxc: {\n decorator: {\n legacy: true,\n emitDecoratorMetadata: true,\n },\n },\n});\n```\n\nUnder Vite 8 the oxc transform does not honor the pre-Vite-8 `esbuild.tsconfigRaw`\nrecipe (or tsconfig `experimentalDecorators` reached through SvelteKit's\n`extends \"./.svelte-kit/tsconfig.json\"` chain), so that recipe throws\n`SyntaxError: Invalid or unexpected token` on the first SSR request. Configure\ndecorators through `oxc.decorator` instead. Consumers still pinned on vite<8 need\nthe legacy `esbuild.tsconfigRaw` form with `experimentalDecorators: true,\nemitDecoratorMetadata: true`.\n\nFor independent CI invocations, both `smrtPlugin()` and `smrtConsumer()` accept\nthe same `generationSnapshot: { path, sha256, provenance, sourceRoot }`. The\nschema-v1 snapshot produced by `serializeSmrtGenerationSnapshot()` contains the\nmerged project/dependency manifest, portable source paths, and source-file\ndigests; each plugin selects its own view. Reuse mode fails closed on\nbyte/provenance/path/content drift, skips scans and manifest writes, and still\ngenerates routes, types, registration, and virtual modules. Omit it for normal\nlocal development and watch mode.\n\n## Schema paths (#2382)\n\nProduction DDL comes from the **manifest** paths\n(`generateSTISchemaFromManifest`/`generateCTISchemaFromManifest`, selected in\n`src/scanner/manifest-generator.ts` → registered `schema` → `db:migrate`). The\n**registry** paths feed `getTestDatabase()` and emit foreign-key indexes\nproduction never gets: the suite runs on a richer schema than it ships.\n\n- Change column/index emission on every shipping path, proven by a path-parity\n test (#2359 adds one). A \"same as migrations\" comment is a claim to check.\n- Every new query predicate ships with its index, or a reason it doesn't.\n- Numeric types, uuid casts, conflict targets, timestamps, migrations: run the\n `test:postgres` lane — SQLite affinity accepts what PostgreSQL rejects.\n- Read `dist/manifest.json`/regenerated schemas for what a decorator produced;\n count across all packages instead of sampling.\n- Tenant scoping is whole-path: every unique constraint and conflict target on a\n tenant-scoped table carries the tenant column, and every read path — not only\n `list()` — is interceptor-aware.\n- Rolling indexes out is part of the change: a bulk `CREATE INDEX` batch needs\n the bounded, concurrent migrate path (#2362, Gotchas), or it takes production\n down on deploy.\n\n## Gotchas\n\n- **Filesystem support is a lazy boundary (#1979)**: `SmrtClass` acquires `options.fs` adapters via `createFilesystemAdapter()` (`src/filesystem-loader.ts`), never a static `@happyvertical/files` import — the files SDK statically pulls @aws-sdk/client-s3 and reaches googleapis, and a static edge here would land it in every downstream SSR bundle. Node/tsx/vite-dev runtimes resolve it on first use; fully-bundled deployments import `@happyvertical/smrt-core/filesystem` at startup. Use `importOptionalDependency()` (`src/lazy-external.ts`) for any similar optional heavyweight dependency.\n- **Never override toJSON()** — handles STI discriminator + meta field extraction. Use `transformJSON()`\n- **Property init order**: TypeScript initializers run first, then `initialize()` applies option values (options win)\n- **No runtime schema creation**: application tables must be prepared explicitly via migrations/tooling; runtime verification is `tableExists()` only (`src/schema/table-verifier.ts`) — no column, type, or index check\n- **PostgreSQL migrate batches are always time-bounded (#2362)**: `MigrationTracker.applyAll({ atomic: true })` emits `SET LOCAL lock_timeout`/`statement_timeout` before any DDL, so a batch blocked on one table cannot hold its earlier locks indefinitely. `postgresSafe: true` adds concurrent-index mode — non-index DDL commits atomically, then index DDL runs `CONCURRENTLY` on a session pinned via `db.acquireSession()` (a pooled `db.query` would not keep the `SET` and the DDL on one connection). That mode is deliberately **not atomic**: unfinished index migrations are recorded `failed`, not `running`, and their `error_message` carries a `[smrt: concurrent-index phase 1 committed]` marker so a reconciling re-run resumes at the index build instead of replaying committed DDL. INVALID indexes are found via `pg_index.indisvalid` (`pg_indexes` reports them as present) and dropped before rebuild. Operational detail: `packages/cli/AGENTS.md`.\n- **Retry logic is transient-only (#2366)**: `db.get()`/`db.upsert()` retry 4× total (initial + 3), but `ErrorUtils.withRetry` classifies via the cause chain (`src/db-errors.ts`) and rethrows deterministic failures immediately — constraint violations, bad input syntax, missing tables, aborted PG tx (`25P02`). `@happyvertical/sql` stringifies the driver text into `context.originalError`, so **never match `error.message`**; use `classifyDatabaseError()` / `isUniqueViolationError()` / `isAbortedTransactionError()`.\n- **Field caching**: `_cachedFields` populated during `Collection.create()` — eliminates async `getFields()` per query\n- **Smart cloning**: arrays/objects shallow-cloned in property init to prevent aliasing (Issue #22)\n- **Table verification cache**: `isTableVerified(dbUrl, tableName)` avoids redundant `tableExists()` calls\n- **Manifest required**: build-time AST scanning creates manifest. Without vitest plugin → \"No field metadata\"\n- **ManifestBuilder fails on scanner errors**: every production manifest path\n must abort before adapting partial scan results. A syntax error or unresolved\n `@smrt()` config spread cannot be allowed to emit a default-open manifest.\n- **Vite plugin loads scanner from `dist/` first**: `src/vite-plugin/import-build-aware.ts` prefers `dist/` when it exists on disk; it only falls back to `src/` on fresh clones. So if you edit `src/scanner/*.ts` or `src/schema/generator.ts` and want those edits reflected in consumer manifest generation, you must rebuild (`pnpm build` or have `pnpm dev` / `pnpm build:watch` running in core). This is intentional — sniffing `.ts` vs `.js` via `import.meta.url` was non-deterministic under tsx and broke 12–13 publishes (#1139).\n- **Bundled registry ownership**: flattened production bundles can rewrite constructor names and make decorator-time stack inference attribute provider code to the consumer. Generated registration repairs identity only from the exact imported constructor plus an explicit package and isolated one-object manifest; never infer ownership from output paths, simple names, or table names. Distinct packages may export the same simple name under qualified keys. The production-consumer gate lives in `packages/bundle-gate/src/__tests__/registry-identity.spec.ts` (#2308).\n",
|
|
915
917
|
"moduleDocs": [
|
|
916
918
|
{
|
|
917
919
|
"path": "agents/change-feed.md",
|
|
@@ -927,6 +929,16 @@
|
|
|
927
929
|
"path": "agents/generators.md",
|
|
928
930
|
"module": "generators",
|
|
929
931
|
"content": "# smrt-core/code generators\n\nModule semantics for `src/generators/` + `src/vite-plugin/`. Package orientation, the cross-module\ninvariants, and the traps that apply before editing anything live in\n[../AGENTS.md](../AGENTS.md) — read that first.\n\n## Code Generators\n\n| Generator | Location | Output |\n|-----------|----------|--------|\n| REST API | `src/generators/rest.ts` | OpenAPI-compliant CRUD endpoints |\n| CLI | `src/generators/cli.ts` | `objectname:action` admin commands — writable allowlist, exhaustive-include, `--from-file`, fail-closed tenant context |\n| MCP Server | `src/generators/mcp.ts` | Model Context Protocol tools |\n| Web collections | `src/vite-plugin/web-collections.ts` (selectors) + `generateWebModule` | `@happyvertical/smrt-virt-web` — one typed collection definition per API-exposed REST collection (#1761), consumed by `@happyvertical/smrt-web` |\n\nGenerated API clients share `selectApiClientEntries()` across the runtime Vite\nmodule, its ambient declaration, and physical prebuild declarations. When a\ncollection class and its populated model share an endpoint, the model owns the\ncanonical collection key and row payload schema; the collection class remains\navailable under a deterministic class-derived secondary key. Selection and\ncollision suffixes must not depend on manifest insertion order (#2027).\nFor aggregated manifests, inheritance and item-type references resolve exact\nqualified names first, then package-local simple names, then a stable identity\nfallback so duplicate class names across packages cannot reintroduce ordering.\n\nThe web module also emits a build-time **`manifestHash`** constant (#1764): `computeWebManifestHash(manifest)` is a deterministic, replica-stable digest of the emitted web-collection SHAPE (name/className/endpoint/idField/actions/fields/relationships), canonicalized (recursive key sort) before `sha256 → base64url`, truncated to 16 chars — so the same schema always hashes the same, and a field add/remove/type-change/edge-change changes it. A change means old persisted client rows may mis-hydrate, so smrt-web keys its durable persistence namespace on it and its `updateAvailable` contract signal compares against it. Four co-managed emission sites must not drift: the runtime value (`generateWebModule`), the `@happyvertical/smrt-virt-web` ambient d.ts (`vite-plugin/index.ts`), the physical `@smrt/web` d.ts (`prebuild/index.ts`), and the hand-written type mirror in `@happyvertical/smrt-web` (`packages/smrt-web/src/index.ts` — dependency-free, so textual sync only).\n\nPer-field web emission (#2046): `buildWebFieldDefinitions` carries `description` (from `@field({ description })`) and sanitized `ui` hints (from `@field({ ui: { basic, group, order, locked } })`, read off the manifest `_meta.ui` bag through per-key type guards) into each emitted field definition, and `buildWebToolDescriptors` threads the same `description` into browser MCP tool schemas. `sensitive`/`transient` fields are excluded from emission entirely, so their descriptions never ship. Both keys are conditional, so hint-less schemas emit byte-identical definitions (and hashes) as before; adding a description/ui hint changes the manifest hash — deliberate over-invalidation, harmless per the #1764 contract.\n\n## Generated MCP server output language\n\n`MCPGenerator` builds every file as TypeScript, so the requested `outputPath`\nextension decides what is written (#2279). `.ts`/`.mts` targets keep the source\nverbatim for `tsx` or Node type stripping — which is why the generated source\nmust stay erasable-syntax-only (no parameter properties, enums, or namespaces).\nEvery other target (`.smrt/mcp-server/index.js` by default) is transpiled to\nJavaScript with the `typescript` dependency before writing, because the printed\nrun script and the generated `claude-config.example.json` both invoke it with\nplain `node`. A `.cjs`/`.cts` target is rejected outright: generated servers are\nES modules. `src/generators/mcp-emit.ts` owns those decisions — do not\nreintroduce a bare `writeFile` of generated source.\n\nModular output writes `config`, `tools/index`, and `handlers/index` with the\nentry point's own extension, and emits the entry's relative import specifiers\nwith that same extension, so the files it imports both exist and load with the\nsame module semantics — an `.mjs` entry gets `.mjs` siblings, not `.js` ones a\nCommonJS package would then parse as CommonJS. The entry is written at the\nrequested path rather than a hardcoded `index.js`.\nGenerated code also has to be valid in an ES module: `arguments` is not a legal\nbinding name there, however convenient it reads.\n\n## Custom-action contract\n\n`resolveCustomActionMetadata()` is the common discovery and invocation contract\nfor generated REST routes and API clients, MCP, CLI, WebMCP, and simple\nREST-resource discovery. Receiver scope comes from the executable method, never a\nconfiguration-only `api.routes[name].scope` override: instance model methods\nare item-scoped and require `id`; static model methods and recognized\n`SmrtCollection` methods are collection-scoped and do not accept `id`. Route\nconfiguration may still choose its path and HTTP verb, but it cannot turn an\ninstance call into `ClassRef.action` or vice versa.\n\nWhen scanner method metadata exists, discovery projects each named parameter\nand its JSON-schema type, and invokers pass the values positionally in declared\norder. The legacy single `options` bag remains compatible when metadata is\nabsent (or the declared method takes `options`). Do not infer this from runtime\nfunction arity. An omitted typed `options` parameter remains `undefined`, so a\nmethod's JavaScript default initializer continues to apply; an explicit `null`\nremains `null`. Flat tool and CLI inputs reserve `id` for receiver parsing. If\nan action declares an `id` parameter, its flat MCP/WebMCP field is `actionId`\n(and CLI uses `--action-id`); REST keeps its independent path/body\nnamespaces. Typed CLI actions may use standard flag names such as `limit`,\n`offset`, `where`, and `format` without those values being stripped as CRUD\nflags.\n\nCustom actions may return an explicit, domain-neutral failure object with\n`ok: false`, `code`, and `message` plus optional `status`, `details`,\n`retryable`, and `correlationId`. `normalizeCustomActionFailure()` redacts it;\ngenerated REST returns `{ error: failure }` with the non-2xx status, while MCP\nreturns `isError: true` and `_meta['io.happyvertical/smrt']`. Opaque successful\nobjects (including `{ code, message }`) remain untouched; thrown exceptions are\nnot reclassified as domain failures.\n\nGenerated reads (`list`/`get`) on the REST and SvelteKit generators support conditional GET (helpers in `src/generators/conditional-get.ts`). ETag v2 (#1765): the validator is the table's change-feed version (`getTableVersion`) keyed by the request representation, so a **concrete** `If-None-Match` short-circuits into a 304 with an empty body **before** the collection query runs — an unchanged table revalidates with zero table scan. A wildcard `If-None-Match: *` is deferred until the payload builds (existence confirmed), so a missing item still returns 404, not a false 304. Tenant-scoped reads fold the active tenant into the representation (`resolveTenantEtagDiscriminator`) so one tenant's cached validator never satisfies another's read of the same URL. Routes whose GET renders via a **custom serializer** (which can load related tables the base-table version can't observe) keep the v1 body-hash ETag (`#1757`, query-first but correct); the default `toPublicJSON` path — all REST reads and non-serializer SvelteKit reads — uses v2. v2 is weakly consistent by design (the cost of not reading the data): a revalidation in the sub-statement window between a committed write and its feed append can return a stale 304 that self-heals on the next revalidation. The other v2 window — a deploy that changes the response shape WITHOUT a table write — is closed by the **#1764 ETag salt**: `computeTableVersionEtag(version, representation, manifestHash?)` folds the build's web-collection shape digest into the digest, so a shape-only redeploy busts every read validator (`undefined` reproduces the pre-#1764 unsalted value byte-for-byte for direct helper callers). The generated SvelteKit route bakes the digest in as a `MANIFEST_HASH` constant (via `generateConditionalGetRouteHelper`'s `manifestHash` option, sourced from `computeWebManifestHash(manifest)`) — automatic for the SvelteKit transport. The runtime `APIGenerator` auto-populates the same salt from the runtime registry with `computeRuntimeWebManifestHash()` when `APIConfig.manifestHash` is omitted; explicit `APIConfig.manifestHash` still wins for custom setups. The digest scope is get-OR-list (`selectWebEtagSaltEntries`), so **get-only** routes are salted too. Strong consistency still requires the v1 body-hash path. Cache-Control policy (unchanged from #1757): `private, no-cache` by default; public models may opt into shared caching via `@smrt({ api: { public: true | 'read', cache: { sMaxage } } })` → `public, max-age=0, s-maxage=<n>`; non-public models never emit shared-cache headers. Tenant-scoped models (any mode) never emit them either — bodies vary with session-cookie tenant context that URL-keyed shared caches cannot see; `sMaxage` is neutralized to `private, no-cache` with a one-time warning.\n"
|
|
932
|
+
},
|
|
933
|
+
{
|
|
934
|
+
"path": "agents/schema-paths.md",
|
|
935
|
+
"module": "schema-paths",
|
|
936
|
+
"content": "# smrt-core/schema paths\n\nModule semantics for `src/schema/` — which `SchemaGenerator` entry point reaches\na real database, what each one emits, and the rules that keep them in step.\nPackage orientation, the cross-module invariants, and the traps that apply\nbefore editing anything live in [../AGENTS.md](../AGENTS.md) — read that first;\nits \"Schema paths\" section is the short form of everything below.\n\nWritten from the 2026-08-17 database-layer gap assessment (epic #2382). Symbol\nnames here are stable; the line numbers the assessment quotes are not, so trust\nthis call graph and re-grep before citing a location.\n\n## Five entry points, two of which ship\n\n`src/schema/generator.ts` exposes five index-emitting entry points. They do not\nproduce the same schema for the same class.\n\n| Entry point | Selected by | Status |\n|---|---|---|\n| `generateSTISchemaFromManifest` | `src/scanner/manifest-generator.ts` | **production** |\n| `generateCTISchemaFromManifest` | `src/scanner/manifest-generator.ts` | **production** |\n| `generateSTISchemaFromRegistry` | `src/testing/database.ts` (`getTestDatabase()`), `src/schema/utils.ts` (`generateSchema`; `ensureSchema` only as a fallback) | tests + runtime helpers |\n| `generateSchemaFromRegistry` | the same two callers | tests + runtime helpers |\n| `generateSchema` (AST) | the `smrt:schema` virtual module, which has no consumer | dead (#2380) |\n\nProduction DDL takes the manifest route:\n\n```\n@smrt() class ─▶ scanner ─▶ manifest.json ─▶ generate{STI,CTI}SchemaFromManifest\n ─▶ registered `schema` ─▶ ObjectRegistry.getAllSchemasAsDefinitions()\n ├─▶ smrt db:migrate | db:diff | db:status\n │ (the CLI drives SchemaComparer + MigrationTracker directly)\n └─▶ migrateSmrtSchemas() / getPendingSchemaStatements()\n (src/migrations/orchestrate.ts — exported for programmatic\n use; no in-repo caller outside its own tests)\n```\n\nThe suite takes the registry route, and the registry route emits indexes the\nmanifest route does not — per-column foreign-key indexes, and STI partial FK\nindexes filtered by `_meta_type`. Tests therefore run against a richer schema\nthan any deployment receives. `src/testing/database.ts`'s \"Generate schema using\nSchemaGenerator (same as migrations)\" comment describes an intent, not the code.\n\nThe manifest STI path even populates a `fkColumnsByClass` map and never reads it\n— only its registry counterpart iterates one — and the manifest CTI path has no\nFK loop at all. Both manifest paths then skip the explicit\n`@foreignKey(X, { indexed: true })` opt-in — the CTI one under a comment claiming\n\"FK columns and unique columns get their own indexes\", which holds on the\nregistry paths and not on this one.\n\n`src/schema/utils.ts` sits in between, and the two exports differ:\n\n- `generateSchema()` (reached from `SmrtCollection.generateSchema()`) always\n rebuilds from the registry and writes the result back into the registry,\n replacing whatever the manifest registered for that class.\n- `ensureSchema()` (reached from the deprecated `smrt db:setup`) is\n manifest-first: it takes `ObjectRegistry.getSchema()` plus the merged\n `getAllSchemasAsDefinitions()` table definition, and only falls back to\n `generateSchema()` when no schema is registered at all.\n\nSo a normal build keeps the manifest schema through `db:setup`, and a\nregistry-derived schema is a dev/test artifact. `smrt-content` shows what one\nlooks like: `packages/content/src/hooks.server.ts` `bootstrapSchema()` calls\n`generateSchema()` for every registered class and then `ensureSchema()` from the\nSvelteKit `handle` hook on any `/api/*` request, so that process holds\nregistry-derived schemas rather than the manifest ones. It reaches only that\npackage's own `vite dev` app — the library build excludes the file and the\npackage never exports it — but it is the shape to recognize. Check which route a\nprocess actually took before trusting a reproduction.\n\n## Why the drift stayed invisible\n\nEvery drift oracle compares a database with the same artifact that dropped the\nindex:\n\n- `verifyPersistenceTable()` (`src/schema/table-verifier.ts`) calls\n `db.tableExists()` and nothing else. \"Runtime verifies schema\" has always meant\n existence-only — no column, type, constraint, or index comparison.\n- `smrt doctor` never opens a database connection.\n- `db:status` and `db:diff` diff the live database against\n `getAllSchemasAsDefinitions()`, i.e. the manifest projection.\n\nAn index the manifest never emitted is \"in sync\" by construction. That is how a\nproduction database reached 164 unindexed `tenant_id` columns while `db:status`\nreported no drift (#2356 → #2359). The assessment's other counts — 196/231\n`@foreignKey` and 91/92 `@crossPackageRef` columns with no production index,\n238/238 tables carrying a redundant index on the primary key, zero DB-level\nforeign-key constraints on any engine — come from regenerating every package's\nschema against a live database, so re-measure rather than quote them once the\nepic's fixes land.\n\n## Rules\n\n### 1. Verify against the production path, not the test path\n\nAny change to column or index emission goes on **all** paths that ship and is\nproven by a path-parity test. #2359 adds that test under `src/schema/`; until it\nlands, assert the parity yourself in the nearest generator test — a green suite\notherwise proves the registry paths only. Read the call graph before believing a\ncomment: \"same as migrations\" was wrong for years.\n\n### 2. Every new query predicate ships with its index\n\nCollection methods, poll loops, auth lookups, junction right-side filters, and\npolymorphic owner lookups all count — or write down why the predicate does not\nneed one. For list workloads, EXPLAIN on a PostgreSQL snapshot; the measured\nspread on the assessed workload was 21 ms → 0.1 ms.\n\n### 3. Run the PostgreSQL lane\n\nAnything touching numeric types, uuid casts, upsert conflict targets, timestamps,\nor migrations runs the package's `test:postgres` script:\n\n```bash\npnpm --filter @happyvertical/smrt-<pkg> test:postgres\n```\n\ncore, cli, users, sales, marketing, analytics, and vitest carry the lane.\nSQLite's type affinity accepts values PostgreSQL rejects — a money field declared\n`number = 0` compiles to INTEGER and only fails on PG (#2361).\n\n### 4. Read the built artifact, not the source\n\nWhat a decorator produced is in `dist/manifest.json` and in regenerated schemas:\n`integer` vs `decimal`, the actual index list, the actual conflict columns. When\nthe question is \"how many tables/columns/indexes\", regenerate and count across\nevery package; do not sample a few and extrapolate.\n\n### 5. Index intent belongs on both the constraint and the read path\n\nA conflict target is not automatically a unique index, and a unique index is not\nautomatically the index a read path uses. Custom `conflictColumns` replace the\n`(slug, context)` index while `loadFromSlug`/`getId` still query slug+context;\nSTI drops `@field({ unique: true })`. Check the pair, not the declaration.\n\n### 6. Multi-tenancy is a whole-path property\n\nEvery unique constraint and every conflict target on a tenant-scoped table\nincludes the tenant column — otherwise a second tenant's `save()` of the same\nnatural key updates the first tenant's row through `DO UPDATE SET` (#2360). And\nevery read path is interceptor-aware: hydration (`loadFromId`/`loadFromSlug`),\nget-by-slug, vector search, and collection memory, not only `list()` (#2365).\n\n### 7. Retry only transient errors\n\nClassify through the cause chain (SQLSTATE), never on a message substring, and\nnever retry inside an aborted PostgreSQL transaction (`25P02`). Test the\ncontract end to end against a real database, not only the classifier (#2366).\n\n### 8. Thread new decorator options through every config-rebuild site\n\nA new `@smrt()` or `@field()` option that affects schema must reach the\n`SchemaGeneratorConfig` type in `src/schema/generator.ts` and every site that\nrebuilds that config — `src/schema/utils.ts` and `src/testing/database.ts` — or\nit is silently dropped on the paths that rebuild it (#2357).\n\n### 9. Delete or wire dead paths, and write docs to what the code does\n\nDead code that looks canonical misleads the next agent: the AST `generateSchema`\npath, `SchemaOverrideSystem`, and the never-emitted `triggers: []` all read as\nsupported surfaces (#2380). Documentation follows the implementation, not the\nintent — say \"verifies the table exists\" when that is what runs.\n\n### 10. Untracked \"known limitation\" comments are bugs nobody will read\n\nFile the issue and link it from the comment. A `products` comment explaining why\na conflict-column change was refrained from sat there for months — and\nmisdescribed the failure mode the whole time.\n\n### 11. Consumer repair scripts are signals\n\nDownstream repair tooling (anytown's `db-repair-plan.ts` carried column-type\nrepairs, missing STI columns and indexes, and `tenant_id` backfills since April)\nis the consumer-side record of framework gaps. Mine it during triage.\n\n### 12. Try to falsify before filing, and treat operations as correctness\n\nRe-verify a finding at source before it becomes an issue — one assessment\ncandidate claimed conflict indexes past two columns were narrowed to two\ncolumns, when only the index *name* is shortened. And an index fix that ships\nwithout a bounded-timeout, `CONCURRENTLY`-capable migrate path can take\nproduction down on rollout (#2362).\n\n### 13. Composite indexes are declared, not inferred (#2357)\n\nThe generated set only covers foreign keys, unique/conflict columns,\n`updated_at`, the STI discriminator, `tenant_id`, and single columns opted in\nwith `@field({ indexed: true })`. A list workload's access path is composite,\nso declare it:\n\n```ts\n@smrt({\n indexes: [\n { name: 'contents_tenant_id_publish_date_idx',\n columns: ['tenantId', 'publish_date'] },\n ],\n})\n```\n\n`columns` takes field names or column names in access-path order — filter\ncolumns first, sort column last. Declare columns, not a direction: PostgreSQL\nscans a btree either way, so an ascending index also serves the matching\n`ORDER BY ... DESC` as an ordered scan with no Sort node. `unique` and `where`\n(partial index) are honoured.\n\n`appendDeclaredIndexes()` runs on all five entry points, before\n`ensureTenantIdIndex()`, so a declared composite leading with the tenant column\nreplaces the automatic standalone `tenant_id` index rather than duplicating it.\nUnknown columns, malformed entries, and a name collision with a different index\nall fail generation — a silently dropped index only surfaces later as a\nproduction slowdown. Rule 8 above is why this works at runtime at all.\n\n### 14. Relationship targets resolve to a class name on both paths\n\n`@foreignKey`/`@oneToMany`/`@manyToMany` accept a class, a name string, or a\n`() => Target` thunk. The decorator invokes the thunk and throws when the target\ncannot be resolved (never `related: ''`); the scanner unwraps the same thunk\nfrom raw source (never `related: '() => Target'`). An unresolved target silently\ncosts the relationship edge, `loadRelated()`, and the FK-derived index (#2379).\nA thunk resolves at decoration time, so a target declared later in the same\nmodule is still in its temporal dead zone — use the string form there.\n\n### 15. A SQLite type change is a table rebuild (#2370)\n\nSQLite has no `ALTER TABLE ... ALTER COLUMN ... TYPE`, so\n`src/migrations/sqlite-rebuild.ts` answers a `type_upgrade` on SQLite with the\nstatement list SQLite's own docs prescribe: stage a new table under\n`_smrt_rebuild_<table>`, copy, drop, rename, replay the indexes and triggers.\n`SchemaComparer.compareTable` swaps that plan in for the differ's\n\"requires table recreation\" placeholder, so `db:migrate` applies it inside the\nnormal atomic batch instead of exiting 1 forever.\n\nFour properties of that module are load-bearing; keep them if you touch it:\n\n- **The target shape comes from the live `sqlite_master` DDL**, retyping only\n the drifted columns. It is not regenerated from the manifest, so the rebuild\n never becomes an implicit `DROP COLUMN`, and it preserves table constraints,\n `CHECK`s, and `WITHOUT ROWID`/`STRICT`.\n- **The rebuild is hoisted ahead of the table's other column changes.** Its\n staging DDL and copy list are captured at diff time, and the differ emits\n changes in manifest field order, so a new field declared above the retyped\n one would otherwise run `ALTER TABLE ... ADD COLUMN` first and have the\n rebuild silently drop it — both statements succeed and the batch commits.\n Rebuild first, then add columns to the rebuilt table.\n- **The copy carries no `CAST`.** SQLite applies the destination column's\n affinity on insert — the same conversion a fresh table performs. An explicit\n cast is worse: non-numeric TEXT cast to REAL/INTEGER silently becomes `0`,\n and an ISO timestamp cast to NUMERIC-affinity `DATETIME` becomes its year.\n- **It refuses when any table has a foreign key onto the target and\n `PRAGMA foreign_keys` is ON** (the SMRT adapter's default). `DROP TABLE`\n performs an implicit `DELETE FROM` that fires `ON DELETE CASCADE` on\n children, and `defer_foreign_keys` defers constraint *checks*, not FK\n *actions* — verified: the child rows go. The target's own self-reference\n counts, because the staging table copies that clause and becomes a child of\n the table being dropped (verified: a two-row self-referencing table finishes\n the rebuild holding one row). Such a column stays manual drift.\n- **`PRAGMA legacy_alter_table` brackets the rename**, because SQLite ≥ 3.25\n re-parses the schema on `ALTER TABLE ... RENAME` and a view still pointing at\n the just-dropped table makes it fail outright. It is restored immediately\n after; a rolled-back batch leaves it set on that connection, which is inert\n here only because nothing else in SMRT renames a table.\n\nAll the drifted columns of one table share a single rebuild: the first change\ncarries the plan and the rest become `no change needed` comments that the CLI\nclassifies as no-ops.\n\n## What the differ compares (#2369)\n\n`SchemaComparer` (`src/migrations/differ.ts`) compares each manifest column's\ntype, then — unless the type itself is drifting — its nullability and default,\nand always reports what it will not touch:\n\n- **Strengthening** (`SET NOT NULL`, `SET DEFAULT`) is executable on\n PostgreSQL/DuckDB. `SET NOT NULL` is preceded by an `UPDATE … WHERE c IS NULL`\n backfill of the manifest default; without a default the live data is probed\n and, if NULLs exist, the change is reported (comment SQL + `advisory`) instead\n of emitting an ALTER that would abort the atomic batch.\n- **Relaxing** (`DROP NOT NULL`, `DROP DEFAULT`) is a report-only advisory until\n the caller passes `relaxColumns` (`db:migrate --relax-columns`). The manifest\n can be under-specified (#2372 registration-order weakness), so a live column\n that is stricter than the manifest is never weakened silently.\n- **Orphans** — DB columns absent from the manifest, DB tables no manifest\n declares (`SchemaDiff.orphan_tables`), and unclaimed `*_key` unique constraint\n indexes — are always reported. A NOT NULL orphan without a default is a\n `warning` advisory (every ORM insert fails on it); `includeDroppedColumns`\n (`--drop-columns`) drops it, `relaxColumns` relaxes it. Advisory-only changes\n carry no SQL, never reach the tracker, and do not fail `db:migrate`.\n- **ADD COLUMN** is planned per engine: DuckDB rejects every inline constraint\n (add with `DEFAULT`, then `SET NOT NULL`, `CREATE UNIQUE INDEX`); SQLite\n rejects inline `UNIQUE` (separate `CREATE UNIQUE INDEX <table>_<col>_key`, the\n PostgreSQL constraint-index name, so the orphan sweep leaves it alone) and\n `NOT NULL` without a default on a populated table; PostgreSQL keeps constraints\n inline. DuckDB has no `ADD CONSTRAINT`, so the separate index is the only\n way to add uniqueness there; the bundled DuckDB 1.4.x resolves\n `ON CONFLICT (col)` through that index (the old #12684 limitation the DuckDB\n strategy's `requiresInlineUnique()` note describes no longer reproduces —\n the #2369 DuckDB test pins the upsert), older DuckDB builds may not. A required column with no default is enforced only on an empty table;\n on a populated one it is added nullable and the `NOT NULL` is reported as a\n manual follow-up on every engine.\n- **SQLite** has no `ALTER COLUMN`: nullability/default alterations are manual\n (comment SQL → `db:migrate` exit 1). The #2370 rebuild (rule 15) consumes\n only `type_upgrade` placeholders today; extending it to rewrite constraints\n would lift this.\n- Defaults compare through `canonicalizeDefault()`, which folds engine\n renderings (`'x'::text`, `CAST('t' AS BOOLEAN)`, `CURRENT_TIMESTAMP` vs\n `now()`) by manifest type; an unclassifiable rendering skips the comparison\n rather than risking a false positive that would churn every run. The\n round-trip test (create from each DDL strategy → compare → zero changes) in\n `src/migrations/__tests__/issue-2369-*.test.ts` guards this.\n\n### 16. `schema.ddl` is a preview, not the table\n\n`SchemaDefinition.ddl` / `manifest.json` `schema.ddl` is the engine-neutral\nCREATE TABLE string from `SchemaGenerator.generateSQL()` with no engine: no\nindexes, no triggers, abstract `REAL`/`JSON`/`UUID`/`TIMESTAMP`. It is kept for\nbackward compatibility only. Everything that needs an executable table renders\n`columns` + `indexes` through `getDDLStrategy(engine)` — `db:migrate`\n(`migrations/orchestrate.ts`), `MigrationGenerator` (default\n`materializeStructuredSchema: true`; `false` is a deprecated opt-out),\n`SchemaAggregator`, and `createIsolatedTestDbFromManifest` in smrt-vitest, the\nlast two via `src/schema/manifest-schema.ts` (`collectManifestTables` /\n`renderCollectedManifestTable`). The cached string is merged in only for a\ntable whose contributors expose no structured columns (hand-authored\nmanifests); table constraints that exist only in the string are dropped with a\nwarning, as `db:migrate` drops them. Do not add a new consumer of the\nstring, and do not write a private CREATE INDEX renderer — the retired ones\ndropped `where` and `jsonPath` (#2358). Every DDL strategy also spells out\n`PRIMARY KEY NOT NULL`: SQLite lets a bare non-INTEGER PRIMARY KEY hold NULL.\n"
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
"path": "agents/query-bounds.md",
|
|
940
|
+
"module": "query-bounds",
|
|
941
|
+
"content": "<!-- Module doc for packages/core/AGENTS.md. Linked from the Modules table there. -->\n\n# Query bounds (#2367)\n\n`src/query-bounds.ts` is the single parser every generated read surface uses for\n`limit`/`offset`. A bound is a non-negative integer or it is a client error:\nmalformed input raises a 400-typed `QueryBoundsError` (a `ValidationError`, so\n`withRetry` never retries it and `normalizeTypedHttpError` renders it as a\nstructured 400) instead of reaching the driver as `LIMIT NaN`. Oversized pages\nare **clamped** to `MAX_LIST_LIMIT` (1000) rather than rejected, and an explicit\n`0` means zero rows — it is not folded into `DEFAULT_LIST_LIMIT` (50).\n\n- `collection.get()` / `findOne()` / `findById()` emit `LIMIT 1`.\n- `collection.list()` validates `limit`/`offset` but applies **no implicit\n default**: it is the framework's bulk-read primitive and relationship,\n junction, hierarchy and `listByIds()` callers all expect every matching row, so\n a framework-wide default would truncate correct queries. Applications opt in\n per collection with `defaultListLimit` / `maxListLimit` (validated at\n construction). The generated surfaces enforce the ceiling on their own\n untrusted input regardless.\n- `orderBy` runs the same rail as `where` (#1540) and `select` (#1902): terms are\n checked against the field whitelist and refused for `@field({ sensitive: true })`\n and `@field({ readPermission })` columns — ordering is a comparison, and\n `?orderBy=api_secret&limit=1` is an oracle over a column the request may neither\n filter on nor project — and for fields that are registered but **not\n column-backed** — `oneToMany`/`manyToMany`/`meta`/`transient`, plus the\n `id`/`slug`/`context` system columns on a custom-primary-key class, which the\n schema generator omits — all of which otherwise reach the driver as\n `no such column` and surface as a 500. The whitelist is skipped only for\n manifest-less inline test classes (#869), exactly as `where` skips it. `where`\n still whitelists those omitted system columns; that is a pre-existing gap of\n the same family, not closed here because `where: { id }` is on internal\n hydration paths.\n- Every generated list surface (REST, SvelteKit, MCP, and the emitted stdio MCP\n runtime) pages with `ORDER BY created_at DESC, <pk> ASC` unless the caller\n supplies `orderBy` — `LIMIT`/`OFFSET` with no ordering is not pagination, and\n `created_at` alone still ties. `<pk>` follows a declared\n `@field({ primaryKey: true })` (read from `_meta.primaryKey` in manifests)\n because custom-primary-key classes have no synthetic `id` column. The stdio\n runtime gets the per-object ordering baked in via `RuntimeOptions.listOrderBy`;\n it cannot resolve a primary key on its own. Index: #2363.\n- `listByIds()` chunks its `IN` list at `IN_LIST_CHUNK_SIZE` (900), like the\n relationship/junction/hierarchy loaders.\n\nKeyset pagination is deliberately out of scope.\n"
|
|
930
942
|
}
|
|
931
943
|
]
|
|
932
944
|
}
|
package/dist/sync/apply.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../src/sync/apply.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../src/sync/apply.ts"],"names":[],"mappings":"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"}
|
package/dist/sync/apply.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isUniqueViolationError } from "../db-errors.js";
|
|
2
2
|
//#region src/sync/apply.ts
|
|
3
3
|
/**
|
|
4
4
|
* Idempotent sync-apply batch processing (#1759).
|
|
@@ -219,23 +219,16 @@ function rowUpdatedAtIso(row) {
|
|
|
219
219
|
}
|
|
220
220
|
/**
|
|
221
221
|
* Detect a unique/primary-key constraint failure anywhere in an error's cause
|
|
222
|
-
* chain.
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
222
|
+
* chain.
|
|
223
|
+
*
|
|
224
|
+
* Reduced to the framework contract in #2366: `save()` now classifies through
|
|
225
|
+
* the driver-error cause chain, so this reports `true` for both the typed
|
|
226
|
+
* `ValidationError` it raises and any still-wrapped adapter error that reaches
|
|
227
|
+
* here by another path. The private chain walk and duplicated dialect regexes
|
|
228
|
+
* this replaced are gone — `classifyDatabaseError()` is the single matcher.
|
|
228
229
|
*/
|
|
229
230
|
function isUniqueConstraintError(error) {
|
|
230
|
-
|
|
231
|
-
let current = error;
|
|
232
|
-
while (current instanceof Error && !seen.has(current)) {
|
|
233
|
-
seen.add(current);
|
|
234
|
-
if (current instanceof ValidationError && current.code === "VALIDATION_UNIQUE_CONSTRAINT") return true;
|
|
235
|
-
if (/UNIQUE constraint failed/i.test(current.message) || /violates unique constraint/i.test(current.message) || /violates primary key constraint/i.test(current.message)) return true;
|
|
236
|
-
current = current.cause;
|
|
237
|
-
}
|
|
238
|
-
return false;
|
|
231
|
+
return isUniqueViolationError(error);
|
|
239
232
|
}
|
|
240
233
|
async function applyValidatedItem(item, target) {
|
|
241
234
|
const base = {
|