@happyvertical/smrt-core 0.40.69 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/AGENTS.md +29 -4
  2. package/README.md +20 -1
  3. package/agents/change-feed.md +1 -1
  4. package/agents/query-bounds.md +45 -0
  5. package/agents/schema-paths.md +786 -0
  6. package/dist/browser.d.ts +1 -0
  7. package/dist/browser.d.ts.map +1 -1
  8. package/dist/browser.js +5 -3
  9. package/dist/cascade.d.ts +120 -0
  10. package/dist/cascade.d.ts.map +1 -0
  11. package/dist/cascade.js +430 -0
  12. package/dist/cascade.js.map +1 -0
  13. package/dist/change-feed.d.ts +34 -2
  14. package/dist/change-feed.d.ts.map +1 -1
  15. package/dist/change-feed.js +52 -11
  16. package/dist/change-feed.js.map +1 -1
  17. package/dist/class.d.ts +36 -3
  18. package/dist/class.d.ts.map +1 -1
  19. package/dist/class.js +87 -9
  20. package/dist/class.js.map +1 -1
  21. package/dist/collection-cache.js +0 -0
  22. package/dist/collection-cache.js.map +1 -1
  23. package/dist/collection.d.ts +130 -2
  24. package/dist/collection.d.ts.map +1 -1
  25. package/dist/collection.js +290 -57
  26. package/dist/collection.js.map +1 -1
  27. package/dist/config.d.ts +10 -0
  28. package/dist/config.d.ts.map +1 -1
  29. package/dist/config.js.map +1 -1
  30. package/dist/database.d.ts +8 -0
  31. package/dist/database.d.ts.map +1 -1
  32. package/dist/database.js +16 -8
  33. package/dist/database.js.map +1 -1
  34. package/dist/db-errors.d.ts +105 -0
  35. package/dist/db-errors.d.ts.map +1 -0
  36. package/dist/db-errors.js +382 -0
  37. package/dist/db-errors.js.map +1 -0
  38. package/dist/decorators/index.d.ts +80 -6
  39. package/dist/decorators/index.d.ts.map +1 -1
  40. package/dist/decorators/index.js +102 -12
  41. package/dist/decorators/index.js.map +1 -1
  42. package/dist/dispatch/bus.d.ts.map +1 -1
  43. package/dist/dispatch/bus.js +4 -3
  44. package/dist/dispatch/bus.js.map +1 -1
  45. package/dist/dispatch/collections/Dispatches.d.ts.map +1 -1
  46. package/dist/dispatch/collections/Dispatches.js +19 -4
  47. package/dist/dispatch/collections/Dispatches.js.map +1 -1
  48. package/dist/dispatch/types.d.ts +5 -0
  49. package/dist/dispatch/types.d.ts.map +1 -1
  50. package/dist/embedded-write-queue.d.ts +46 -0
  51. package/dist/embedded-write-queue.d.ts.map +1 -0
  52. package/dist/embedded-write-queue.js +66 -0
  53. package/dist/embedded-write-queue.js.map +1 -0
  54. package/dist/embeddings/storage.d.ts +7 -0
  55. package/dist/embeddings/storage.d.ts.map +1 -1
  56. package/dist/embeddings/storage.js +29 -12
  57. package/dist/embeddings/storage.js.map +1 -1
  58. package/dist/errors.d.ts +31 -3
  59. package/dist/errors.d.ts.map +1 -1
  60. package/dist/errors.js +34 -2
  61. package/dist/errors.js.map +1 -1
  62. package/dist/generators/changes-route.d.ts.map +1 -1
  63. package/dist/generators/changes-route.js +6 -3
  64. package/dist/generators/changes-route.js.map +1 -1
  65. package/dist/generators/mcp-runtime-template.d.ts +8 -0
  66. package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
  67. package/dist/generators/mcp-runtime-template.js +38 -4
  68. package/dist/generators/mcp-runtime-template.js.map +1 -1
  69. package/dist/generators/mcp.d.ts +16 -0
  70. package/dist/generators/mcp.d.ts.map +1 -1
  71. package/dist/generators/mcp.js +41 -3
  72. package/dist/generators/mcp.js.map +1 -1
  73. package/dist/generators/rest.d.ts +22 -0
  74. package/dist/generators/rest.d.ts.map +1 -1
  75. package/dist/generators/rest.js +34 -3
  76. package/dist/generators/rest.js.map +1 -1
  77. package/dist/hierarchical.js +1 -1
  78. package/dist/index.d.ts +7 -1
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/index.js +12 -5
  81. package/dist/interceptors.d.ts +21 -0
  82. package/dist/interceptors.d.ts.map +1 -1
  83. package/dist/interceptors.js +27 -1
  84. package/dist/interceptors.js.map +1 -1
  85. package/dist/manifest/generator.d.ts.map +1 -1
  86. package/dist/manifest/generator.js +4 -7
  87. package/dist/manifest/generator.js.map +1 -1
  88. package/dist/manifest/static-manifest.js +10 -10
  89. package/dist/manifest/static-manifest.js.map +1 -1
  90. package/dist/manifest/store.js +1 -1
  91. package/dist/manifest/store.js.map +1 -1
  92. package/dist/manifest.json +19 -19
  93. package/dist/migrations/differ.d.ts +211 -9
  94. package/dist/migrations/differ.d.ts.map +1 -1
  95. package/dist/migrations/differ.js +613 -50
  96. package/dist/migrations/differ.js.map +1 -1
  97. package/dist/migrations/generator.d.ts +31 -4
  98. package/dist/migrations/generator.d.ts.map +1 -1
  99. package/dist/migrations/generator.js +49 -5
  100. package/dist/migrations/generator.js.map +1 -1
  101. package/dist/migrations/index.d.ts +4 -2
  102. package/dist/migrations/index.d.ts.map +1 -1
  103. package/dist/migrations/index.js +6 -3
  104. package/dist/migrations/minor-units.d.ts +162 -0
  105. package/dist/migrations/minor-units.d.ts.map +1 -0
  106. package/dist/migrations/minor-units.js +381 -0
  107. package/dist/migrations/minor-units.js.map +1 -0
  108. package/dist/migrations/orchestrate.js +35 -6
  109. package/dist/migrations/orchestrate.js.map +1 -1
  110. package/dist/migrations/sqlite-rebuild.d.ts +142 -0
  111. package/dist/migrations/sqlite-rebuild.d.ts.map +1 -0
  112. package/dist/migrations/sqlite-rebuild.js +514 -0
  113. package/dist/migrations/sqlite-rebuild.js.map +1 -0
  114. package/dist/migrations/tracker.d.ts +114 -1
  115. package/dist/migrations/tracker.d.ts.map +1 -1
  116. package/dist/migrations/tracker.js +331 -16
  117. package/dist/migrations/tracker.js.map +1 -1
  118. package/dist/migrations/types.d.ts +19 -4
  119. package/dist/migrations/types.d.ts.map +1 -1
  120. package/dist/migrations.js +6 -3
  121. package/dist/object.d.ts +142 -10
  122. package/dist/object.d.ts.map +1 -1
  123. package/dist/object.js +196 -41
  124. package/dist/object.js.map +1 -1
  125. package/dist/postgres-timeouts.d.ts +240 -0
  126. package/dist/postgres-timeouts.d.ts.map +1 -0
  127. package/dist/postgres-timeouts.js +204 -0
  128. package/dist/postgres-timeouts.js.map +1 -0
  129. package/dist/query-bounds.d.ts +101 -0
  130. package/dist/query-bounds.d.ts.map +1 -0
  131. package/dist/query-bounds.js +177 -0
  132. package/dist/query-bounds.js.map +1 -0
  133. package/dist/registry/class-registration.d.ts.map +1 -1
  134. package/dist/registry/class-registration.js +3 -1
  135. package/dist/registry/class-registration.js.map +1 -1
  136. package/dist/registry/manifest-field-merge.d.ts +12 -0
  137. package/dist/registry/manifest-field-merge.d.ts.map +1 -1
  138. package/dist/registry/manifest-field-merge.js +14 -2
  139. package/dist/registry/manifest-field-merge.js.map +1 -1
  140. package/dist/registry/schema-builder.d.ts +22 -1
  141. package/dist/registry/schema-builder.d.ts.map +1 -1
  142. package/dist/registry/schema-builder.js +205 -165
  143. package/dist/registry/schema-builder.js.map +1 -1
  144. package/dist/registry/types.d.ts +35 -3
  145. package/dist/registry/types.d.ts.map +1 -1
  146. package/dist/registry.d.ts +41 -46
  147. package/dist/registry.d.ts.map +1 -1
  148. package/dist/registry.js +61 -83
  149. package/dist/registry.js.map +1 -1
  150. package/dist/scanner/manifest-generator.d.ts +45 -0
  151. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  152. package/dist/scanner/manifest-generator.js +92 -28
  153. package/dist/scanner/manifest-generator.js.map +1 -1
  154. package/dist/scanner/types.d.ts +5 -0
  155. package/dist/scanner/types.d.ts.map +1 -1
  156. package/dist/scanner/types.js.map +1 -1
  157. package/dist/schema/conflict-target.d.ts +104 -0
  158. package/dist/schema/conflict-target.d.ts.map +1 -0
  159. package/dist/schema/conflict-target.js +129 -0
  160. package/dist/schema/conflict-target.js.map +1 -0
  161. package/dist/schema/ddl/base-strategy.d.ts.map +1 -1
  162. package/dist/schema/ddl/base-strategy.js +2 -2
  163. package/dist/schema/ddl/base-strategy.js.map +1 -1
  164. package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -1
  165. package/dist/schema/ddl/duckdb-strategy.js +2 -1
  166. package/dist/schema/ddl/duckdb-strategy.js.map +1 -1
  167. package/dist/schema/ddl/postgres-strategy.d.ts.map +1 -1
  168. package/dist/schema/ddl/postgres-strategy.js +12 -1
  169. package/dist/schema/ddl/postgres-strategy.js.map +1 -1
  170. package/dist/schema/generator.d.ts +307 -6
  171. package/dist/schema/generator.d.ts.map +1 -1
  172. package/dist/schema/generator.js +510 -87
  173. package/dist/schema/generator.js.map +1 -1
  174. package/dist/schema/index-utils.d.ts +120 -0
  175. package/dist/schema/index-utils.d.ts.map +1 -1
  176. package/dist/schema/index-utils.js +242 -1
  177. package/dist/schema/index-utils.js.map +1 -1
  178. package/dist/schema/index.d.ts +3 -0
  179. package/dist/schema/index.d.ts.map +1 -1
  180. package/dist/schema/index.js +4 -1
  181. package/dist/schema/live-parity.d.ts +90 -0
  182. package/dist/schema/live-parity.d.ts.map +1 -0
  183. package/dist/schema/live-parity.js +602 -0
  184. package/dist/schema/live-parity.js.map +1 -0
  185. package/dist/schema/manifest-schema.d.ts +121 -0
  186. package/dist/schema/manifest-schema.d.ts.map +1 -0
  187. package/dist/schema/manifest-schema.js +267 -0
  188. package/dist/schema/manifest-schema.js.map +1 -0
  189. package/dist/schema/schema-aggregator.d.ts +24 -10
  190. package/dist/schema/schema-aggregator.d.ts.map +1 -1
  191. package/dist/schema/schema-aggregator.js +35 -90
  192. package/dist/schema/schema-aggregator.js.map +1 -1
  193. package/dist/schema/system-table-shapes.d.ts +65 -0
  194. package/dist/schema/system-table-shapes.d.ts.map +1 -0
  195. package/dist/schema/system-table-shapes.js +187 -0
  196. package/dist/schema/system-table-shapes.js.map +1 -0
  197. package/dist/schema/types.d.ts +103 -4
  198. package/dist/schema/types.d.ts.map +1 -1
  199. package/dist/schema/utils.d.ts +2 -1
  200. package/dist/schema/utils.d.ts.map +1 -1
  201. package/dist/schema/utils.js +5 -3
  202. package/dist/schema/utils.js.map +1 -1
  203. package/dist/schema.js +4 -1
  204. package/dist/smrt-knowledge.json +20 -8
  205. package/dist/sync/apply.d.ts.map +1 -1
  206. package/dist/sync/apply.js +9 -16
  207. package/dist/sync/apply.js.map +1 -1
  208. package/dist/system/compatibility.d.ts +42 -0
  209. package/dist/system/compatibility.d.ts.map +1 -1
  210. package/dist/system/compatibility.js +182 -9
  211. package/dist/system/compatibility.js.map +1 -1
  212. package/dist/system/index.d.ts +1 -0
  213. package/dist/system/index.d.ts.map +1 -1
  214. package/dist/system/index.js +3 -2
  215. package/dist/system/retention.d.ts +237 -0
  216. package/dist/system/retention.d.ts.map +1 -0
  217. package/dist/system/retention.js +497 -0
  218. package/dist/system/retention.js.map +1 -0
  219. package/dist/system/schema.d.ts +100 -15
  220. package/dist/system/schema.d.ts.map +1 -1
  221. package/dist/system/schema.js +81 -45
  222. package/dist/system/schema.js.map +1 -1
  223. package/dist/system/types.d.ts +0 -2
  224. package/dist/system/types.d.ts.map +1 -1
  225. package/dist/testing/database.d.ts.map +1 -1
  226. package/dist/testing/database.js +1 -0
  227. package/dist/testing/database.js.map +1 -1
  228. package/dist/vite-plugin/index.d.ts.map +1 -1
  229. package/dist/vite-plugin/index.js +4 -9
  230. package/dist/vite-plugin/index.js.map +1 -1
  231. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  232. package/dist/vite-plugin/sveltekit-generator.js +71 -5
  233. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  234. package/dist/vite-plugin/web-collections.d.ts.map +1 -1
  235. package/dist/vite-plugin/web-collections.js +6 -4
  236. package/dist/vite-plugin/web-collections.js.map +1 -1
  237. package/package.json +5 -5
package/AGENTS.md CHANGED
@@ -17,6 +17,7 @@ subsystem you are editing. This file keeps what holds across all of them.
17
17
  | `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) |
18
18
  | `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) |
19
19
  | `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) |
20
+ | `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) |
20
21
 
21
22
  ## SmrtObject Lifecycle
22
23
 
@@ -25,6 +26,7 @@ subsystem you are editing. This file keeps what holds across all of them.
25
26
  - `initialize()`: loads field initializers, applies option values (options override initializers), loads from DB if id/slug provided
26
27
  - `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)
27
28
  - `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)
29
+ - `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`
28
30
  - `getSlug()`: auto-generates from name → title → label → id
29
31
  - `loadRelated(fieldName)`: lazy-loads relationships (cached in `_loadedRelationships` Map)
30
32
 
@@ -74,7 +76,7 @@ references). Re-adding either requires the query builder to support it first;
74
76
  `src/__tests__/issue-2276-where-contract.test.ts` executes every accepted
75
77
  operator against a database to keep the two in step.
76
78
 
77
- STI child collections auto-filter by `_meta_type`.
79
+ STI 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).
78
80
 
79
81
  ## Bounded Collection Read Plans
80
82
 
@@ -97,7 +99,7 @@ Two persistence primitives every `SmrtObject`/`SmrtCollection` inherits — load
97
99
 
98
100
  ## @smrt() Decorator Options
99
101
 
100
- Key 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).
102
+ Key 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).
101
103
 
102
104
  Registration sets `SMRT_TABLE_NAME` static property (survives minification).
103
105
 
@@ -203,13 +205,36 @@ byte/provenance/path/content drift, skips scans and manifest writes, and still
203
205
  generates routes, types, registration, and virtual modules. Omit it for normal
204
206
  local development and watch mode.
205
207
 
208
+ ## Schema paths (#2382)
209
+
210
+ Production DDL comes from the **manifest** paths
211
+ (`generateSTISchemaFromManifest`/`generateCTISchemaFromManifest`, selected in
212
+ `src/scanner/manifest-generator.ts` → registered `schema` → `db:migrate`). The
213
+ **registry** paths feed `getTestDatabase()` and emit foreign-key indexes
214
+ production never gets: the suite runs on a richer schema than it ships.
215
+
216
+ - Change column/index emission on every shipping path, proven by the path-parity
217
+ test `src/schema/schema-path-parity.test.ts` (#2359; index rules in the module doc). A "same as migrations" comment is a claim to check.
218
+ - Every new query predicate ships with its index, or a reason it doesn't.
219
+ - Numeric types, uuid casts, conflict targets, timestamps, migrations: run the
220
+ `test:postgres` lane — SQLite affinity accepts what PostgreSQL rejects.
221
+ - Read `dist/manifest.json`/regenerated schemas for what a decorator produced;
222
+ count across all packages instead of sampling.
223
+ - Tenant scoping is whole-path: every unique constraint and conflict target on a
224
+ tenant-scoped table carries the tenant column, and every read path — not only
225
+ `list()` — is interceptor-aware.
226
+ - Rolling indexes out is part of the change: a bulk `CREATE INDEX` batch needs
227
+ the bounded, concurrent migrate path (#2362, Gotchas), or it takes production
228
+ down on deploy.
229
+
206
230
  ## Gotchas
207
231
 
208
232
  - **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.
209
233
  - **Never override toJSON()** — handles STI discriminator + meta field extraction. Use `transformJSON()`
210
234
  - **Property init order**: TypeScript initializers run first, then `initialize()` applies option values (options win)
211
- - **No runtime schema creation**: application tables must be prepared explicitly via migrations/tooling; runtime only verifies and fails clearly
212
- - **Retry logic**: `db.get()` (3 retries, 250ms) and `db.upsert()` (3 retries, 500ms) have built-in retry
235
+ - **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
236
+ - **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`.
237
+ - **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()`.
213
238
  - **Field caching**: `_cachedFields` populated during `Collection.create()` — eliminates async `getFields()` per query
214
239
  - **Smart cloning**: arrays/objects shallow-cloned in property init to prevent aliasing (Issue #22)
215
240
  - **Table verification cache**: `isTableVerified(dbUrl, tableName)` avoids redundant `tableExists()` calls
package/README.md CHANGED
@@ -425,7 +425,26 @@ Every `SmrtObject`/`SmrtCollection` can persist learned knowledge via `remember(
425
425
  | `AIError` | AI provider failures |
426
426
  | `ValidationError` | Field/object validation failures |
427
427
  | `RuntimeError` | General runtime failures |
428
- | `ErrorUtils` | Sanitization and formatting helpers |
428
+ | `ErrorUtils` | Retry policy (`withRetry`, `isRetryable`) plus sanitization helpers |
429
+
430
+ ### Database error classification
431
+
432
+ Driver errors reach the model layer wrapped by `@happyvertical/sql`, which
433
+ stringifies the driver text into `context.originalError` — so the constraint
434
+ wording never appears on `error.message`. Classify through the cause chain
435
+ instead of matching messages.
436
+
437
+ | Export | Description |
438
+ |--------|------------|
439
+ | `classifyDatabaseError` | Walks the cause chain and returns the kind plus `deterministic` / `retryable` |
440
+ | `classifyDialectMessage` | Matches a single raw dialect message (the DuckDB fallback) |
441
+ | `isUniqueViolationError` | Unique or primary-key violation anywhere in the chain |
442
+ | `isNotNullViolationError` | NOT NULL violation anywhere in the chain |
443
+ | `isAbortedTransactionError` | Statement issued inside an aborted PostgreSQL transaction (`25P02`) |
444
+ | `isDeterministicDatabaseError` | A retry cannot change the outcome |
445
+ | `isTransientDatabaseError` | Contention or availability; a retry may succeed |
446
+ | `DatabaseErrorKind` | Union of classification kinds |
447
+ | `DatabaseErrorClassification` | Structured result of `classifyDatabaseError` |
429
448
 
430
449
  ### Tools (AI Function Calling)
431
450
 
@@ -13,4 +13,4 @@ Adapter-agnostic change-observation spine (`src/change-feed.ts`) — the server
13
13
  - `getChangesSince(db, { since, tables?, tenantId?, limit? }) → { changes, cursor, resyncRequired?, resyncCursor? }`: strictly monotonic cursor; polling with returned cursors misses no committed change and never repeats one. A cursor that cannot be served incrementally — pruned below the retained `[floor..horizon]` run, or foreign/ahead of the horizon — gets `resyncRequired: true` with empty `changes`, an unadvanced `cursor`, and `resyncCursor` set to the current horizon so clients can full-refetch then resume incrementally; detection runs on the UNFILTERED log so `tables`/`tenantId` filters never trigger or mask it. `getTenantScopedChangesSince()` resolves tenant via the DispatchBus resolver hook (fail-closed: tenancy on + no context → global rows only; tenant `T` sees `T` + global rows, never another tenant).
14
14
  - `getTableVersion(db, table) → number`: the per-table change version (`MAX(seq)` for the table, replica-stable — no per-process divergence), the ETag source for zero-query conditional GETs (#1765). Advances on any framework write to the table (CRUD and sync-apply, which all `save()`/`delete()`). A table with no retained entry of its own falls back to the global horizon (never a resettable low value) so an all-pruned table cannot false-304 a stale client; only 0 when the feed is empty.
15
15
  - Generated `_changes` routes: REST (`GET {basePath}/_changes`, requires `authMiddleware`, otherwise 401 — per-model `api.public` does NOT apply) and SvelteKit (`{routesDir}/_changes/+server.ts`, requires an authenticated principal on `locals`; opt out via `sveltekit.changesRoute.enabled: false`). Query params: `since`, `tables` (comma-separated), `limit`. Responses stay HTTP 200 in the resync state — `resyncRequired` is protocol state, not an error, and `resyncCursor` is the resume cursor after the client completes a full refetch.
16
- - Retention: `pruneChangeFeed(db, { maxAgeMs?, maxRows? })` — schedule it. Pruning deletes oldest-first and always retains the newest entry (a non-empty feed is never emptied), which is what makes pruned-cursor detection provable and keeps caught-up consumers polling normally. Raw-SQL writes are invisible to the feed (same documented gap as the #1499 cache); `bumpChangeFeed(db, { table, rowId? })` is the manual escape hatch.
16
+ - Retention: `pruneChangeFeed(db, { maxAgeMs?, maxRows?, dryRun? })` — scheduled since #2375 by `runRetentionSweep()` (30-day default), so nothing needs to call it directly; `dryRun` counts the same predicate instead of deleting. Pruning deletes oldest-first and always retains the newest entry (a non-empty feed is never emptied), which is what makes pruned-cursor detection provable and keeps caught-up consumers polling normally. Raw-SQL writes are invisible to the feed (same documented gap as the #1499 cache); `bumpChangeFeed(db, { table, rowId? })` is the manual escape hatch.
@@ -0,0 +1,45 @@
1
+ <!-- Module doc for packages/core/AGENTS.md. Linked from the Modules table there. -->
2
+
3
+ # Query bounds (#2367)
4
+
5
+ `src/query-bounds.ts` is the single parser every generated read surface uses for
6
+ `limit`/`offset`. A bound is a non-negative integer or it is a client error:
7
+ malformed input raises a 400-typed `QueryBoundsError` (a `ValidationError`, so
8
+ `withRetry` never retries it and `normalizeTypedHttpError` renders it as a
9
+ structured 400) instead of reaching the driver as `LIMIT NaN`. Oversized pages
10
+ are **clamped** to `MAX_LIST_LIMIT` (1000) rather than rejected, and an explicit
11
+ `0` means zero rows — it is not folded into `DEFAULT_LIST_LIMIT` (50).
12
+
13
+ - `collection.get()` / `findOne()` / `findById()` emit `LIMIT 1`.
14
+ - `collection.list()` validates `limit`/`offset` but applies **no implicit
15
+ default**: it is the framework's bulk-read primitive and relationship,
16
+ junction, hierarchy and `listByIds()` callers all expect every matching row, so
17
+ a framework-wide default would truncate correct queries. Applications opt in
18
+ per collection with `defaultListLimit` / `maxListLimit` (validated at
19
+ construction). The generated surfaces enforce the ceiling on their own
20
+ untrusted input regardless.
21
+ - `orderBy` runs the same rail as `where` (#1540) and `select` (#1902): terms are
22
+ checked against the field whitelist and refused for `@field({ sensitive: true })`
23
+ and `@field({ readPermission })` columns — ordering is a comparison, and
24
+ `?orderBy=api_secret&limit=1` is an oracle over a column the request may neither
25
+ filter on nor project — and for fields that are registered but **not
26
+ column-backed** — `oneToMany`/`manyToMany`/`meta`/`transient`, plus the
27
+ `id`/`slug`/`context` system columns on a custom-primary-key class, which the
28
+ schema generator omits — all of which otherwise reach the driver as
29
+ `no such column` and surface as a 500. The whitelist is skipped only for
30
+ manifest-less inline test classes (#869), exactly as `where` skips it. `where`
31
+ still whitelists those omitted system columns; that is a pre-existing gap of
32
+ the same family, not closed here because `where: { id }` is on internal
33
+ hydration paths.
34
+ - Every generated list surface (REST, SvelteKit, MCP, and the emitted stdio MCP
35
+ runtime) pages with `ORDER BY created_at DESC, <pk> ASC` unless the caller
36
+ supplies `orderBy` — `LIMIT`/`OFFSET` with no ordering is not pagination, and
37
+ `created_at` alone still ties. `<pk>` follows a declared
38
+ `@field({ primaryKey: true })` (read from `_meta.primaryKey` in manifests)
39
+ because custom-primary-key classes have no synthetic `id` column. The stdio
40
+ runtime gets the per-object ordering baked in via `RuntimeOptions.listOrderBy`;
41
+ it cannot resolve a primary key on its own. Index: #2363.
42
+ - `listByIds()` chunks its `IN` list at `IN_LIST_CHUNK_SIZE` (900), like the
43
+ relationship/junction/hierarchy loaders.
44
+
45
+ Keyset pagination is deliberately out of scope.