@happyvertical/smrt-core 0.51.32 → 0.51.33

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 (172) hide show
  1. package/agents/build-knowledge.md +13 -0
  2. package/dist/__tests__/fixtures/collection-coverage-fixtures.d.ts +8 -0
  3. package/dist/__tests__/fixtures/inheritance-resolver-fixtures.d.ts +9 -0
  4. package/dist/__tests__/fixtures/inheritance-test-classes.d.ts +5 -0
  5. package/dist/__tests__/fixtures/object-ai-memory-fixtures.d.ts +10 -0
  6. package/dist/__typechecks__/collection-read-plan.d.ts +2 -2
  7. package/dist/adapters/ai-usage.d.ts +5 -2
  8. package/dist/adapters/cost-rates.d.ts +1 -1
  9. package/dist/adapters/metrics.d.ts +7 -1
  10. package/dist/adapters/pubsub.d.ts +7 -1
  11. package/dist/cascade.d.ts +69 -3
  12. package/dist/change-feed-authz.d.ts +85 -2
  13. package/dist/change-feed.d.ts +133 -2
  14. package/dist/change-signals.d.ts +66 -1
  15. package/dist/child-accessors.d.ts +39 -1
  16. package/dist/class.d.ts +8 -8
  17. package/dist/collection-cache.d.ts +47 -1
  18. package/dist/collection-read-plan.d.ts +3 -3
  19. package/dist/collection.d.ts +4 -3
  20. package/dist/config.d.ts +15 -6
  21. package/dist/consumer-plugin/index.d.ts +7 -3
  22. package/dist/consumer-plugin/index.d.ts.map +1 -1
  23. package/dist/consumer-plugin/index.js +2 -0
  24. package/dist/consumer-plugin/index.js.map +1 -1
  25. package/dist/data-query.d.ts +12 -1
  26. package/dist/database.d.ts +10 -2
  27. package/dist/decisions.d.ts +9 -2
  28. package/dist/decorators/index.d.ts +11 -5
  29. package/dist/dispatch/bus.d.ts +27 -2
  30. package/dist/dispatch/collections/DispatchSubscriptions.d.ts +7 -2
  31. package/dist/dispatch/collections/Dispatches.d.ts +8 -3
  32. package/dist/dispatch/models/Dispatch.d.ts +7 -1
  33. package/dist/dispatch/types.d.ts +7 -2
  34. package/dist/embedded-write-queue.d.ts +1 -1
  35. package/dist/embeddings/provider.d.ts +8 -1
  36. package/dist/embeddings/storage.d.ts +8 -2
  37. package/dist/filesystem-loader.d.ts +16 -1
  38. package/dist/generators/changes-route.d.ts +34 -1
  39. package/dist/generators/custom-action.d.ts +10 -2
  40. package/dist/generators/events-route.d.ts +41 -4
  41. package/dist/generators/mcp-runtime-template.d.ts +13 -2
  42. package/dist/generators/mcp.d.ts +6 -1
  43. package/dist/generators/rest.d.ts +10 -5
  44. package/dist/generators/tool-schema.d.ts +1 -1
  45. package/dist/hierarchical.d.ts +37 -1
  46. package/dist/interceptors.d.ts +45 -1
  47. package/dist/junction.d.ts +24 -0
  48. package/dist/knowledge-config.d.ts +1 -1
  49. package/dist/knowledge-discovery.d.ts +2 -25
  50. package/dist/knowledge-discovery.d.ts.map +1 -1
  51. package/dist/knowledge-discovery.js +1 -53
  52. package/dist/knowledge-graph.d.ts +2 -94
  53. package/dist/knowledge-graph.d.ts.map +1 -1
  54. package/dist/knowledge-graph.js +1 -267
  55. package/dist/knowledge.d.ts +4 -23
  56. package/dist/knowledge.d.ts.map +1 -1
  57. package/dist/knowledge.js +3 -52
  58. package/dist/knowledge.js.map +1 -1
  59. package/dist/learning/memory.d.ts +21 -1
  60. package/dist/manifest/generator.d.ts +5 -1
  61. package/dist/manifest/index.d.ts +5 -1
  62. package/dist/manifest/manager.d.ts +1 -1
  63. package/dist/manifest/manifest-loader.d.ts +24 -2
  64. package/dist/manifest/sources/composite.d.ts +17 -1
  65. package/dist/manifest/sources/embedded.d.ts +10 -1
  66. package/dist/manifest/sources/explicit-paths.d.ts +13 -1
  67. package/dist/manifest/sources/fallback.d.ts +15 -1
  68. package/dist/manifest/sources/local-test.d.ts +10 -1
  69. package/dist/manifest/sources/static.d.ts +12 -1
  70. package/dist/manifest/sources/test.d.ts +9 -1
  71. package/dist/manifest/sources/types.d.ts +7 -1
  72. package/dist/manifest/static-manifest.d.ts +6 -1
  73. package/dist/manifest/static-manifest.js +1 -1
  74. package/dist/manifest/static-manifest.js.map +1 -1
  75. package/dist/manifest/store.d.ts +22 -3
  76. package/dist/manifest/store.js +1 -1
  77. package/dist/manifest.json +1 -1
  78. package/dist/migrations/backfill-tracker.d.ts +15 -1
  79. package/dist/migrations/differ.d.ts +8 -2
  80. package/dist/migrations/framework-base-tables.d.ts +57 -2
  81. package/dist/migrations/generator.d.ts +8 -2
  82. package/dist/migrations/integer-width.d.ts +10 -2
  83. package/dist/migrations/minor-units.d.ts +40 -1
  84. package/dist/migrations/null-equal-indexes.d.ts +3 -2
  85. package/dist/migrations/orchestrate.d.ts +15 -3
  86. package/dist/migrations/sqlite-rebuild.d.ts +67 -2
  87. package/dist/migrations/tracker.d.ts +9 -3
  88. package/dist/migrations/types.d.ts +7 -2
  89. package/dist/object.d.ts +7 -6
  90. package/dist/polymorphic-association.d.ts +44 -1
  91. package/dist/postgres-permissions.d.ts +1 -1
  92. package/dist/prebuild/index.d.ts +5 -1
  93. package/dist/query-bounds.d.ts +26 -0
  94. package/dist/registry/cache-config.d.ts +10 -1
  95. package/dist/registry/class-registration.d.ts +10 -2
  96. package/dist/registry/collection-resolution.d.ts +1 -1
  97. package/dist/registry/embedding-manager.d.ts +9 -1
  98. package/dist/registry/inheritance-resolver.d.ts +8 -3
  99. package/dist/registry/manifest-field-merge.d.ts +2 -2
  100. package/dist/registry/name-resolver.d.ts +22 -2
  101. package/dist/registry/relationship-graph.d.ts +9 -1
  102. package/dist/registry/schema-builder.d.ts +8 -3
  103. package/dist/registry/shared-state.d.ts +12 -2
  104. package/dist/registry/types.d.ts +15 -7
  105. package/dist/registry/validator.d.ts +10 -2
  106. package/dist/registry.d.ts +40 -11
  107. package/dist/relationship-loader.d.ts +1 -1
  108. package/dist/run-once.d.ts +110 -1
  109. package/dist/runtime/client.d.ts +4 -1
  110. package/dist/runtime/server.d.ts +5 -2
  111. package/dist/scanner/import-scanner.d.ts +1 -1
  112. package/dist/scanner/manifest-generator.d.ts +4 -1
  113. package/dist/scanner/types.d.ts +4 -1
  114. package/dist/schema/code-generator.d.ts +6 -2
  115. package/dist/schema/column-data-probes.d.ts +27 -2
  116. package/dist/schema/ddl/base-strategy.d.ts +8 -2
  117. package/dist/schema/ddl/duckdb-strategy.d.ts +14 -2
  118. package/dist/schema/ddl/index.d.ts +2 -2
  119. package/dist/schema/ddl/json-duckdb-strategy.d.ts +10 -2
  120. package/dist/schema/ddl/materialize-manifest.d.ts +1 -1
  121. package/dist/schema/ddl/null-equal-index.d.ts +1 -1
  122. package/dist/schema/ddl/postgres-strategy.d.ts +12 -2
  123. package/dist/schema/ddl/sqlite-strategy.d.ts +12 -2
  124. package/dist/schema/ddl/types.d.ts +10 -1
  125. package/dist/schema/foreign-key-ddl.d.ts +2 -2
  126. package/dist/schema/foreign-key-orphan-report.d.ts +25 -3
  127. package/dist/schema/foreign-key-planner.d.ts +2 -2
  128. package/dist/schema/foreign-key-policy.d.ts +1 -1
  129. package/dist/schema/generator.d.ts +7 -3
  130. package/dist/schema/index-utils.d.ts +11 -2
  131. package/dist/schema/live-parity.d.ts +27 -3
  132. package/dist/schema/manifest-schema.d.ts +21 -2
  133. package/dist/schema/schema-aggregator.d.ts +35 -2
  134. package/dist/schema/schema-manager.d.ts +13 -4
  135. package/dist/schema/system-table-shapes.d.ts +20 -1
  136. package/dist/schema/table-verifier.d.ts +1 -1
  137. package/dist/schema/text-cast-probe.d.ts +24 -1
  138. package/dist/schema/utils.d.ts +13 -4
  139. package/dist/schema/uuid-convergence.d.ts +32 -1
  140. package/dist/signals/bus.d.ts +8 -2
  141. package/dist/signals/sanitizer.d.ts +7 -1
  142. package/dist/smrt-knowledge.json +8 -9
  143. package/dist/system/bootstrap.d.ts +2 -1
  144. package/dist/system/compatibility.d.ts +1 -1
  145. package/dist/system/diagnostics.d.ts +17 -2
  146. package/dist/system/retention.d.ts +46 -1
  147. package/dist/system/schema.d.ts +7 -1
  148. package/dist/system-fields.d.ts +9 -1
  149. package/dist/test-utils.d.ts +7 -1
  150. package/dist/testing/database.d.ts +18 -1
  151. package/dist/testing/notifications-stub.d.ts +15 -1
  152. package/dist/tools/tool-executor.d.ts +6 -1
  153. package/dist/tools/tool-generator.d.ts +8 -2
  154. package/dist/utils/json.d.ts +9 -1
  155. package/dist/utils/qualified-names.d.ts +10 -1
  156. package/dist/utils/scanner-module.d.ts +2 -2
  157. package/dist/utils.d.ts +1 -1
  158. package/dist/vite-plugin/api-client-entries.d.ts +9 -1
  159. package/dist/vite-plugin/changes-route.d.ts +33 -2
  160. package/dist/vite-plugin/dev-plane-route.d.ts +12 -1
  161. package/dist/vite-plugin/events-route.d.ts +37 -2
  162. package/dist/vite-plugin/generated-client.d.ts +9 -1
  163. package/dist/vite-plugin/index.d.ts +9 -5
  164. package/dist/vite-plugin/resources-route.d.ts +82 -1
  165. package/dist/vite-plugin/sveltekit-generator.d.ts +8 -4
  166. package/dist/vite-plugin/sveltekit-route-coordinator.d.ts +3 -3
  167. package/dist/vite-plugin/sync-apply-route.d.ts +19 -2
  168. package/dist/vite-plugin/web-collections.d.ts +25 -2
  169. package/dist/vite-plugin/worker-registration.d.ts +21 -1
  170. package/package.json +7 -8
  171. package/dist/knowledge-discovery.js.map +0 -1
  172. package/dist/knowledge-graph.js.map +0 -1
@@ -33,6 +33,10 @@ importing one fails at worker startup. A failure fails the build.
33
33
 
34
34
  ## Lightweight discovery
35
35
 
36
+ Filesystem-only discovery, module-doc parsing, and graph freshness are owned by
37
+ `@happyvertical/smrt-scanner/knowledge`. Core's knowledge exports remain compatible
38
+ reexports. Runtime manifest projection and custom-action semantics stay in core.
39
+
36
40
  `src/knowledge-discovery.ts`, exported through `smrt-core/knowledge`, enumerates
37
41
  installed scope directories and reads canonical AGENTS/legacy CLAUDE docs without
38
42
  loading package code, artifacts, or scanning objects. CLI snapshots and MCP share
@@ -119,3 +123,12 @@ digests; each plugin selects its own view. Reuse mode fails closed on
119
123
  byte/provenance/path/content drift, skips scans and manifest writes, and still
120
124
  generates routes, types, registration, and virtual modules. Omit it for normal
121
125
  local development and watch mode.
126
+
127
+ ## Clean consumer type checks (TypeScript 6)
128
+
129
+ The consumer config hook writes `.smrt/register.js` alongside generated types
130
+ after route preflight succeeds. Generated SvelteKit registration imports that
131
+ module, so it must exist during `svelte-kit sync`, before Vite's `buildStart`.
132
+ Both hosted routes and the legacy `svelteKit: true` mode follow this order.
133
+ The clean-consumer regression fixture ships real provider declarations and
134
+ checks the generated imports with `svelte-check`.
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Fixtures for collection.ts coverage tests (convenience finders, count,
3
+ * getDiff, getOrUpsert, and collection-scoped memory).
4
+ *
5
+ * Separate file so the @smrt() classes land in the test manifest.
6
+ *
7
+ * @see src/collection.ts (issue #1500 coverage, ORM group)
8
+ */
1
9
  import { SmrtCollection } from '../../collection.js';
2
10
  import { SmrtObject } from '../../object.js';
3
11
  export declare class CovWidget extends SmrtObject {
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Fixture classes for inheritance-resolver coverage tests.
3
+ *
4
+ * Kept in a separate file (not the test file) so they are picked up by
5
+ * test-manifest generation. Mirrors the convention used by
6
+ * inheritance-test-classes.ts.
7
+ *
8
+ * @see src/registry/inheritance-resolver.ts (issue #1500 coverage, ORM group)
9
+ */
1
10
  import { SmrtHierarchical } from '../../hierarchical.js';
2
11
  import { SmrtObject } from '../../object.js';
3
12
  export declare class IRBase extends SmrtObject {
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Test fixture classes for inheritance tests
3
+ * These classes must be in a separate file (not in the test file itself)
4
+ * so they can be included in the test manifest generation
5
+ */
1
6
  import { SmrtObject } from '../../object.js';
2
7
  export declare class InheritanceTestContent extends SmrtObject {
3
8
  title: string;
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Fixtures for object.ts AI + memory coverage tests.
3
+ *
4
+ * Separate file so the @smrt() classes are picked up by test-manifest
5
+ * generation. The AI client is injectable so is()/do()/describe() can run
6
+ * against a stub without a real provider (mocking AI is the only mocking
7
+ * allowed by the testing rules).
8
+ *
9
+ * @see src/object.ts (issue #1500 coverage, ORM group)
10
+ */
1
11
  import { SmrtObject } from '../../object.js';
2
12
  /** Minimal stub matching the slice of AIClient that is()/do()/describe() use. */
3
13
  export interface StubAi {
@@ -1,5 +1,5 @@
1
- import { SmrtCollectionReadPlan, SmrtCollectionReadPlanEntry, SmrtCollectionReadPlanResult } from '../collection-read-plan';
2
- import { SmrtObject } from '../object';
1
+ import { type SmrtCollectionReadPlan, type SmrtCollectionReadPlanEntry, type SmrtCollectionReadPlanResult } from '../collection-read-plan';
2
+ import type { SmrtObject } from '../object';
3
3
  type Equal<Left, Right> = (<Value>() => Value extends Left ? 1 : 2) extends <Value>() => Value extends Right ? 1 : 2 ? true : false;
4
4
  type Expect<Value extends true> = Value;
5
5
  type ReadPlanTypeProbe = SmrtObject & {
@@ -1,5 +1,8 @@
1
- import { AiUsageHandler, AiUsageSnapshot, SmrtAiUsageEvent } from '@happyvertical/smrt-types';
2
- import { DatabaseInterface } from '@happyvertical/sql';
1
+ /**
2
+ * In-memory AI usage tracking adapters.
3
+ */
4
+ import type { AiUsageHandler, AiUsageSnapshot, SmrtAiUsageEvent } from '@happyvertical/smrt-types';
5
+ import type { DatabaseInterface } from '@happyvertical/sql';
3
6
  /**
4
7
  * Collects normalized AI usage events in memory for quick inspection.
5
8
  */
@@ -1,4 +1,4 @@
1
- import { AiTokenUsage } from '@happyvertical/smrt-types';
1
+ import type { AiTokenUsage } from '@happyvertical/smrt-types';
2
2
  /**
3
3
  * Cost rates are expressed in USD per 1K tokens.
4
4
  *
@@ -1,4 +1,10 @@
1
- import { Signal, SignalAdapter } from '@happyvertical/smrt-types';
1
+ /**
2
+ * Metrics Adapter for Signal System
3
+ *
4
+ * Tracks execution metrics for SMRT objects in Prometheus-compatible format.
5
+ * Collects counts, durations, error rates, and success rates.
6
+ */
7
+ import type { Signal, SignalAdapter } from '@happyvertical/smrt-types';
2
8
  /**
3
9
  * Execution metrics for a specific method
4
10
  */
@@ -1,4 +1,10 @@
1
- import { Signal, SignalAdapter } from '@happyvertical/smrt-types';
1
+ /**
2
+ * Pub/Sub Adapter for Signal System
3
+ *
4
+ * Broadcasts signals to subscribers for real-time updates.
5
+ * Supports multiple subscriber types (callbacks, WebSocket, SSE).
6
+ */
7
+ import type { Signal, SignalAdapter } from '@happyvertical/smrt-types';
2
8
  /**
3
9
  * Signal filter function
4
10
  *
package/dist/cascade.d.ts CHANGED
@@ -1,6 +1,72 @@
1
- import { DatabaseInterface } from '@happyvertical/sql';
2
- import { ObjectRegistry } from './registry.js';
3
- import { ForeignKeyAction } from './schema/types.js';
1
+ /**
2
+ * App-side referential integrity for `SmrtObject.delete()` (#2371).
3
+ *
4
+ * Same-package `@foreignKey` fields now emit database constraints where the
5
+ * engine can enforce the declared policy. Cross-package references and older
6
+ * schemas still rely on this application-side path. Before this module,
7
+ * `delete()` removed the object's own row and nothing else: junction rows,
8
+ * polymorphic association rows, `_smrt_embeddings` and `_smrt_contexts` entries
9
+ * were all left pointing at an id that no longer resolved, and
10
+ * `@foreignKey(..., { onDelete })` was metadata nobody read.
11
+ *
12
+ * This module implements those semantics in the application layer. It mirrors
13
+ * what a DB-level constraint would do, deliberately including the parts that
14
+ * make it *unlike* a model-layer delete:
15
+ *
16
+ * - Cascaded rows are removed with set-based statements. Their `beforeDelete` /
17
+ * `afterDelete` hooks and interceptors do **not** run, and no change-feed
18
+ * tombstone is written for them — exactly as `ON DELETE CASCADE` behaves.
19
+ * Only the object `delete()` was called on runs the full lifecycle.
20
+ * - Every delete runs inside a single transaction when the adapter exposes
21
+ * one, including cleanup of framework-owned context and embedding rows, so
22
+ * a partial cascade cannot survive a failure. A `metaType` column can point
23
+ * at any class at runtime, so a polymorphic association class is always
24
+ * plausibly relevant.
25
+ *
26
+ * ## Which references are followed
27
+ *
28
+ * A reference is any `@foreignKey` / `@crossPackageRef` field on a registered
29
+ * class whose target resolves to the deleted object's class (or one of its
30
+ * registered STI ancestors), plus the polymorphic `(metaType, metaId)` pair on
31
+ * {@link SmrtPolymorphicAssociation} subclasses.
32
+ *
33
+ * The action comes from the field's declared `onDelete`. When it is not
34
+ * declared, the default is derived from the referencing class's natural key:
35
+ *
36
+ * | Reference | Default when `onDelete` is absent |
37
+ * |---|---|
38
+ * | Column is part of the referencing class's `conflictColumns`, and is not a `@tenantId()` field | `CASCADE` |
39
+ * | Polymorphic `(metaType, metaId)` association row | `CASCADE` |
40
+ * | Ordinary same-package reference | `NO ACTION` (deletion is refused while references remain) |
41
+ * | Every `@tenantId()` field | Excluded: tenant scope is not an ownership edge |
42
+ *
43
+ * The natural-key rule is what makes junction rows work without any
44
+ * per-package annotation: a junction declares
45
+ * `@smrt({ conflictColumns: ['content_id', 'asset_id', 'relationship'] })`, so
46
+ * `content_id` identifies the row and the row cannot outlive the content it
47
+ * links. An ordinary child (`Order.customerId`) is keyed by `(slug, context)`,
48
+ * so it defaults to immediate `NO ACTION` unless it opts in with
49
+ * `@foreignKey(Customer, { onDelete: 'CASCADE' })`.
50
+ *
51
+ * `@tenantId()` fields are excluded from the natural-key rule even though
52
+ * `@happyvertical/smrt-tenancy` leads a tenant-scoped class's *default*
53
+ * `conflictColumns` with the tenant column (#2360): the tenant column scopes
54
+ * ownership, it does not identify the row the way a junction's foreign key
55
+ * does, and it targets a class (`Tenant`) that is virtually always
56
+ * referenced. Without this exclusion, deleting one `Tenant` row would
57
+ * recursively CASCADE through every tenant-scoped table that has not
58
+ * declared its own `conflictColumns` — the overwhelming majority. The field
59
+ * is detected via the `__tenancy.isTenantIdField` marker `@tenantId()`
60
+ * attaches to its own registration (`FieldMeta.__tenancy`, read structurally
61
+ * so `smrt-core` never depends on `smrt-tenancy`). `@tenantId()` exposes no
62
+ * `onDelete` option today, so this cannot currently be overridden per field.
63
+ *
64
+ * @see https://github.com/happyvertical/smrt/issues/2371
65
+ * @module
66
+ */
67
+ import type { DatabaseInterface } from '@happyvertical/sql';
68
+ import type { ObjectRegistry } from './registry.js';
69
+ import type { ForeignKeyAction } from './schema/types.js';
4
70
  /**
5
71
  * Referential action applied to rows pointing at a deleted object.
6
72
  *
@@ -1,5 +1,88 @@
1
- import { DatabaseInterface } from '@happyvertical/sql';
2
- import { ChangeFeedPage, GetChangesOptions } from './change-feed.js';
1
+ /**
2
+ * Change-feed table/row authorization seam (issue #3020).
3
+ *
4
+ * The generated `_changes`/`_events` routes authorize on "an authenticated
5
+ * principal" plus tenant scope (`getTenantScopedChangesSince`,
6
+ * `resolveDispatchTenantScope`) — see `change-feed-sensitivity.ts`'s scope
7
+ * note. That is enough to keep one tenant from seeing another's rows, but it
8
+ * cannot express "table X is readable by some principals of the tenant but
9
+ * not others" or "principal P only sees their own rows of table X" (a
10
+ * bay-tablet station versus the back office). `?tables=` was a client-chosen
11
+ * filter, never enforcement: a table the client did not ask for was withheld
12
+ * only because it did not ask, not because it could not have.
13
+ *
14
+ * This module is the consumer-supplied seam that closes that gap, evaluated
15
+ * identically by **every** transport — the generated SvelteKit `_changes`/
16
+ * `_events` routes AND the runtime REST generator's `_changes`/`_events`
17
+ * (`generators/changes-route.ts`, `generators/events-route.ts`) — so no
18
+ * transport can leak what another denies. Two independent hooks,
19
+ * dependency-inverted onto `globalThis` exactly like
20
+ * {@link resolveDispatchTenantScope} (`dispatch/tenant-resolver.ts`) so a
21
+ * consumer app registers them once — wherever it already calls
22
+ * `enableTenancy()`, or its `hooks.server.ts` — with **no change to the
23
+ * generated "DO NOT EDIT" route files' imports**:
24
+ *
25
+ * - `authorizeChangeFeed({ locals, request, tables }) => allowedTables` —
26
+ * table-level. Its result is INTERSECTED with the client's `?tables=`
27
+ * filter (never unioned): a table the hook does not name is never queried,
28
+ * let alone returned, regardless of what the client asked for.
29
+ * - `isChangeFeedEntryVisible({ locals, request, entry }) => boolean` —
30
+ * row-level, for scope narrower than a whole table (a station sees only its
31
+ * own rows). Applied per entry, after the table-level filter, before
32
+ * anything is serialized to the client — including a live `_events` signal,
33
+ * which never carries a payload but still carries the row id and write
34
+ * timing the row hook exists to withhold.
35
+ *
36
+ * ## `locals` vs. `request`
37
+ *
38
+ * SvelteKit routes pass both `event.locals` and `event.request`. The REST
39
+ * generator has no `locals` concept, so it passes `locals: undefined` and the
40
+ * `authMiddleware`-processed `Request` (whatever properties/headers the
41
+ * middleware attached) as `request` — a REST-hosting consumer identifies the
42
+ * principal from `request`, not `locals`. Every real caller supplies
43
+ * `request`; it is typed optional only so a low-level unit test can omit it.
44
+ *
45
+ * Both hooks are optional and independent. When neither is registered, every
46
+ * transport behaves exactly as before (#1540 posture — authenticated +
47
+ * tenant scoped only, no per-table or per-row check): every call in this
48
+ * module is a synchronous no-op pass-through until a consumer opts in.
49
+ *
50
+ * ## Fail-closed
51
+ *
52
+ * A hook that throws, or returns something other than the documented shape,
53
+ * is treated as authorizing nothing for that call — the same posture
54
+ * {@link resolveDispatchTenantScope} takes for a throwing tenant resolver
55
+ * (fail closed rather than fail the request), and the same *shape* of answer
56
+ * (200, empty `changes`, cursor still advances) the change-feed sensitivity
57
+ * filter (#2937) already gives a request naming only tables it may not see.
58
+ * A 5xx here would tell a caller its own hook is broken, which is
59
+ * indistinguishable from "your hook denied me" from the caller's side and
60
+ * would leak that distinction; the contract that matters — never an
61
+ * unfiltered feed — holds identically either way.
62
+ *
63
+ * ## Cursor correctness
64
+ *
65
+ * Every filter here narrows the *returned* `changes` array; nothing here ever
66
+ * touches `cursor`/`resyncRequired`/`resyncCursor`. This mirrors #2937's
67
+ * sensitivity filter (`change-feed.ts`, "Filters ... affect which rows are
68
+ * returned, never how the cursor advances") and for the same reason: a
69
+ * client must advance past a denied entry without re-polling it forever, and
70
+ * the entry's row id/timing must never be inferable from a cursor that stalls
71
+ * on it. Table denial is implemented via `getChangesSince`'s explicit
72
+ * `denyAllTables` option rather than an empty `tables` array —
73
+ * `getChangesSince` treats an omitted/empty `tables` as "no filter", so an
74
+ * empty array would (perversely) widen the read to everything, and an
75
+ * earlier version of this module instead passed a guaranteed-no-match
76
+ * sentinel table name through the ordinary `table_name IN (...)` path; that
77
+ * broke on PostgreSQL, which rejects a NUL byte in a text parameter, turning
78
+ * a denied request into a 500 instead of the required 200 empty page. A
79
+ * `denyAllTables` request skips the `IN (...)` clause entirely — no
80
+ * synthesized value ever reaches the driver — while still running the same
81
+ * horizon/pruned-cursor computation any other filter does, so the cursor
82
+ * still advances correctly; it just matches zero rows.
83
+ */
84
+ import type { DatabaseInterface } from '@happyvertical/sql';
85
+ import { type ChangeFeedPage, type GetChangesOptions } from './change-feed.js';
3
86
  /**
4
87
  * Minimal shape shared by a durable {@link ChangeFeedEntry} (`change-feed.ts`)
5
88
  * and a live `ChangeSignal` (`change-signals.ts`) — both carry exactly
@@ -1,5 +1,136 @@
1
- import { DatabaseInterface } from '@happyvertical/sql';
2
- import { SmrtObject } from './object.js';
1
+ /**
2
+ * Adapter-agnostic change feed — the framework's change-observation spine
3
+ * (issue #1758, parent PRD #1755).
4
+ *
5
+ * Every framework `save()`/`delete()` appends exactly one row to the
6
+ * `_smrt_changes` system table (monotonic per-database sequence, table name,
7
+ * row id, operation, tenant id, timestamp). Deletes are recorded as
8
+ * tombstones (`operation: 'delete'`), distinguishable from updates. One read
9
+ * interface — {@link getChangesSince} — returns changes after a cursor,
10
+ * filterable by table and tenant, and serves three eventual consumers:
11
+ * client delta pull, the SSE push channel, and the per-table version source
12
+ * backing ETags.
13
+ *
14
+ * ## Cursor semantics (the precise guarantee)
15
+ *
16
+ * Sequences are allocated *inside* the append statement as
17
+ * `COALESCE(MAX(seq), 0) + 1` over the feed table itself, with a retry on
18
+ * primary-key conflict. `MAX(seq)` only observes committed rows, so a row
19
+ * with sequence `N` can only be inserted while every row with sequence
20
+ * `< N` is already committed (a conflicting in-flight allocation of the same
21
+ * value blocks, then retries). Committed rows therefore always form a
22
+ * contiguous run ending at `MAX(seq)` — the **committed horizon**. Sequence
23
+ * order equals commit order; out-of-order commit visibility (the classic
24
+ * MVCC race that makes native identity/serial columns unsafe as cursors
25
+ * under concurrent writers) cannot occur.
26
+ *
27
+ * {@link getChangesSince} reads the committed horizon `H = MAX(seq)`, then
28
+ * returns matching rows with `since < seq <= H` (bounded by `limit`), and a
29
+ * `cursor` that is either `H` (page exhaustive) or the last returned `seq`
30
+ * (page limited). Because no change can ever commit at or below an observed
31
+ * horizon after it was observed, polling with returned cursors misses no
32
+ * committed change and never returns the same change twice — under any
33
+ * number of concurrent writers, identically on SQLite, Postgres and DuckDB.
34
+ * This is the design reason the allocator is `MAX+1` rather than a native
35
+ * AUTOINCREMENT/identity column: identity values are allocated before
36
+ * commit, so a reader on Postgres could observe seq 101 while seq 100 is
37
+ * still uncommitted and advance its cursor past it. (No shared
38
+ * auto-increment mechanism exists in the system-table schema path either;
39
+ * see `system/schema.ts`.)
40
+ *
41
+ * ## Staged appends inside caller transactions (PostgreSQL, #2649)
42
+ *
43
+ * `MAX+1` allocation costs a *wait*: two appends that pick the same value
44
+ * conflict on the primary key, and the loser waits for the winner's
45
+ * transaction to end. Under autocommit that is one statement. Inside a
46
+ * caller-managed transaction it is the whole transaction — and a long write
47
+ * transaction that appends and then keeps taking row locks forms a genuine
48
+ * lock cycle with any writer that took those row locks first and then
49
+ * appended. PostgreSQL detects it (`40P01`) and aborts one side, so an
50
+ * ordinary concurrent request could abort a legitimate long write.
51
+ *
52
+ * PostgreSQL appends issued inside a caller transaction are therefore
53
+ * **staged**: `_smrt_append_change` inserts into `_smrt_changes_pending`,
54
+ * whose identity key conflicts with nothing, so the append never waits on
55
+ * another transaction and the cycle cannot form. The staged row is still
56
+ * fate-shared with the caller (a rollback removes it). {@link drainChangeFeed}
57
+ * — run by {@link getChangesSince} before it reads, and (throttled) by the
58
+ * append path itself — moves *committed* staged rows into `_smrt_changes`
59
+ * under a try-only advisory lock, numbering them `MAX(seq) + row_number()` in
60
+ * staged order. Draining is driven from JavaScript rather than inside the
61
+ * append helper so that every sequenced entry also gets its live `_events`
62
+ * signal: an entry sequenced invisibly server-side would be skipped for good
63
+ * by a subscriber whose `Last-Event-ID` came from a later, higher sequence.
64
+ * Signals for drained entries are published only once the drain is proven
65
+ * committed, so a rollback can never advertise a sequence another appender
66
+ * then reuses.
67
+ *
68
+ * The cursor guarantee is unchanged, because sequences are still allocated by
69
+ * exactly one `MAX+1` writer at a time and only ever for already-committed
70
+ * work: committed sequences stay contiguous, and no entry can appear at or
71
+ * below a horizon a reader already observed. What a staged append gives up is
72
+ * *promptness*, not durability or order: its entry becomes visible one drain
73
+ * after its transaction commits, and its position in the log is its drain
74
+ * order rather than its statement order. An autocommit append drains before
75
+ * allocating whenever its throttle allows, so staged work usually keeps its
76
+ * place ahead of later writes.
77
+ *
78
+ * SQLite and DuckDB keep the direct `MAX+1` insert unchanged — SQLite
79
+ * serializes writers outright, so the defect is unreachable there.
80
+ *
81
+ * Contention note: appends serialize on the head of the log. Each append is
82
+ * one small INSERT (issued from the write path *after* the user's row was
83
+ * written), so the serialization window is one statement; conflicts resolve
84
+ * with a bounded retry loop and are impossible on single-writer engines
85
+ * (SQLite). PostgreSQL invokes that INSERT through the framework-owned
86
+ * `_smrt_append_change` function so failure isolation remains one statement.
87
+ *
88
+ * ## Failure semantics
89
+ *
90
+ * A feed-write failure must never fail the user's write. The interceptor
91
+ * wraps the append in a try/catch: on failure it logs a warning (deduped per
92
+ * database) and continues. The trade-off is availability of the user's
93
+ * write over completeness of the feed — consumers already need a
94
+ * full-resync path for cursors older than the retention window, and the
95
+ * same path covers a (rare) dropped feed row. PostgreSQL runs the INSERT in a
96
+ * PL/pgSQL exception subtransaction and returns a caught SQLSTATE as data;
97
+ * JavaScript only throws/logs after PostgreSQL has restored the caller's
98
+ * transaction, so a swallowed append failure cannot surface later as 25P02.
99
+ * The append still joins a caller-managed transaction on the same handle and
100
+ * shares its fate (a rollback removes the staged row with the data row).
101
+ *
102
+ * ## Known gaps (documented in the PRD)
103
+ *
104
+ * - Writes that bypass framework mutation paths (raw SQL) are invisible to
105
+ * the feed — the same accepted gap as the #1499 collection cache.
106
+ * {@link bumpChangeFeed} is the manual escape hatch: out-of-band writers
107
+ * append a synthetic change row for the affected table.
108
+ * - **Spurious `update` entries**: `SmrtObject.save()` has no dirty-check,
109
+ * so a field-unchanged `.save()` still appends an `update` row. This is
110
+ * by design — the writer observes writes, not diffs (it has no old-row
111
+ * access), so the feed faithfully mirrors the write path. Diff-aware
112
+ * paths (`getOrUpsert()`'s diff guard, the sync-apply endpoint's no-op
113
+ * detection) short-circuit before `save()` and append nothing.
114
+ * Subscribers must tolerate spurious entries; they are convergent — a
115
+ * re-fetch returns identical data.
116
+ *
117
+ * ## Retention
118
+ *
119
+ * The log is append-only and grows with write volume. {@link pruneChangeFeed}
120
+ * bounds it by age (`maxAgeMs`) and/or row count (`maxRows`); call it from a
121
+ * scheduled job sized so the retention window comfortably exceeds the
122
+ * slowest consumer's polling interval. Pruning deletes oldest-first and
123
+ * always retains the newest entry, so retained sequences stay a contiguous
124
+ * `[floor..horizon]` run — which is how {@link getChangesSince} *detects* a
125
+ * consumer whose cursor predates the retained window and answers it with
126
+ * `resyncRequired: true` plus a fresh resume cursor instead of silently
127
+ * skipping the pruned changes.
128
+ *
129
+ * @see https://github.com/happyvertical/smrt/issues/1758
130
+ * @packageDocumentation
131
+ */
132
+ import type { DatabaseInterface } from '@happyvertical/sql';
133
+ import type { SmrtObject } from './object.js';
3
134
  /** Name of the append-only change-feed system table. */
4
135
  export declare const CHANGE_FEED_TABLE = "_smrt_changes";
5
136
  /**
@@ -1,4 +1,69 @@
1
- import { DatabaseInterface } from '@happyvertical/sql';
1
+ /**
2
+ * Change-signal bus — the live push spine for the generated `_events` SSE
3
+ * route (issue #1763, parent PRD #1755).
4
+ *
5
+ * The change feed (#1758) is a durable, cursor-addressable log; this bus is
6
+ * its ephemeral companion. Every framework `save()`/`delete()` that appends a
7
+ * feed row also publishes a coarse {@link ChangeSignal} here, which fans out
8
+ * synchronously to in-process subscribers (the SSE controllers of connected
9
+ * `_events` clients) and, when the database adapter exposes a notification
10
+ * capability, to peer replicas over the same channel. Absence of that
11
+ * capability degrades gracefully — no cross-replica push, never an error, and
12
+ * it never blocks the write.
13
+ *
14
+ * ## What a signal carries (and deliberately does not)
15
+ *
16
+ * A signal is `{ table, operation, rowId, tenantId, seq }` — never any row
17
+ * payload. Authorization stays entirely on the read path: a subscriber learns
18
+ * *that* something changed and its cursor (`seq`), then re-reads through the
19
+ * authorized collection routes to catch up. This is why the bus can broadcast
20
+ * a tenant's writes to peer replicas without leaking data across a trust
21
+ * boundary. The `seq` is the same monotonic cursor dimension the change feed
22
+ * allocates, so a reconnecting client can resume via `getChangesSince`.
23
+ *
24
+ * ## Delivery model
25
+ *
26
+ * - Local delivery is a synchronous fan-out ({@link deliverLocally}) into each
27
+ * listener callback, wrapped in a try/catch **per listener** so one throwing
28
+ * listener (e.g. a closed SSE controller's `enqueue`) never blocks the
29
+ * others. There is no per-subscriber queue — backpressure is delegated to
30
+ * each platform `ReadableStream`.
31
+ * - Cross-replica delivery reuses the same {@link deliverLocally} helper on
32
+ * receipt, so locally-published and peer-received signals travel one code
33
+ * path. Notifications this process published are skipped by `source` id
34
+ * (echo-avoidance), exactly as the collection cache does.
35
+ *
36
+ * This mirrors `collection-cache.ts`'s notify/listen structure; study that
37
+ * module for the shared cross-process conventions (`resolveDbCacheKey`,
38
+ * `getNotifications`, the lazy listener and finally-retract).
39
+ *
40
+ * ## Known gaps
41
+ *
42
+ * - **Subscriber cap lives at the route boundary**: the bus tracks subscribers,
43
+ * but generated `_events` routes decide whether to reject a new connection
44
+ * before opening a stream (#1860).
45
+ * - **Raw-SQL writes are invisible**: signals originate from the framework
46
+ * write path (same accepted gap as the #1758 feed and #1498 cache). A
47
+ * `bumpChangeFeed` escape-hatch write appends a feed row but does not
48
+ * publish a signal.
49
+ * - **Caller-managed transactions signal late, not early** (#2649): a
50
+ * PostgreSQL append made inside a caller transaction that has already
51
+ * written is *staged*, so it has no sequence to signal with at
52
+ * `afterSave`/`afterDelete` time. Its signal is published instead by the
53
+ * drain that sequences it, which is the first post-commit moment — closing
54
+ * the older hazard where a pre-commit signal could describe a write a later
55
+ * rollback undid, and where the rolled-back seq was reused by the next
56
+ * append. The cost is latency: live subscribers see a transaction-wrapped
57
+ * write one drain after it commits, and on engines without staging
58
+ * (SQLite, DuckDB) the pre-commit caveat still applies. The autocommit
59
+ * default path — save()/delete() as independent statements — is exact and
60
+ * unchanged. Clients reconcile via cursor catch-up either way, so
61
+ * convergence holds.
62
+ *
63
+ * @see https://github.com/happyvertical/smrt/issues/1763
64
+ * @packageDocumentation
65
+ */
66
+ import type { DatabaseInterface } from '@happyvertical/sql';
2
67
  /**
3
68
  * Operation carried by a change signal. Kept as a local string union — this
4
69
  * module deliberately does NOT import `./change-feed.js` (which imports this
@@ -1,4 +1,42 @@
1
- import { RelationshipMetadata } from './registry/types';
1
+ /**
2
+ * R10 — consolidated child-accessor API for `@oneToMany` relationships.
3
+ *
4
+ * Before R10 every model hand-rolled its own child accessor (`getTerms`,
5
+ * `getChildren`, `getForProfile`, …) with inconsistent names and shapes. This
6
+ * module installs ONE canonical accessor per `@oneToMany` field at class
7
+ * registration time: a `@oneToMany('Child') items` field gains a `getItems()`
8
+ * instance method that delegates to the existing `loadRelatedMany('items')`
9
+ * resolver — the same code path used for lazy loading and `include:` eager
10
+ * loading, which itself queries through the child collection (the queryable
11
+ * primitive).
12
+ *
13
+ * Two invariants make this safe to apply to every registered class:
14
+ *
15
+ * - **Additive.** If a method of the computed name already exists anywhere on
16
+ * the prototype chain — a hand-rolled accessor like
17
+ * `ProfileRelationship.getTerms()`, a differently-shaped `Profile.getMetadata()`,
18
+ * or a `SmrtObject` base method — it is left untouched. The generated
19
+ * accessor never renames or shadows an existing public signature.
20
+ * - **Runtime-only.** Accessors are attached to the prototype at registration;
21
+ * they are invisible to the build-time manifest, so they never leak into the
22
+ * generated REST/CLI/MCP surface (those read `classInfo.tools`/scanned
23
+ * methods, not the runtime prototype). They are a developer-ergonomics
24
+ * convenience layered on top of `loadRelatedMany`.
25
+ *
26
+ * Multiplicity note: when the child class declares more than one foreign key
27
+ * back to the parent (e.g. `ProfileRelationship.fromProfileId` /
28
+ * `toProfileId`), annotate the `@oneToMany` with `{ foreignKey: 'fromProfileId' }`
29
+ * so `loadRelatedMany` resolves the correct inverse side. Without it the
30
+ * resolver falls back to the first matching foreign key (legacy behavior). An
31
+ * explicit `foreignKey` that matches no inverse FK is treated as an error.
32
+ *
33
+ * STI note: a child class inherits the base's generated accessor through the
34
+ * prototype chain. `loadRelatedMany` resolves the inverse FK against the
35
+ * instance's class AND its registered ancestors (see
36
+ * `ObjectRegistry.getInverseRelationshipsForSelf`), so calling the accessor on
37
+ * an STI subclass instance resolves a `@oneToMany` declared on its base.
38
+ */
39
+ import type { RelationshipMetadata } from './registry/types';
2
40
  /**
3
41
  * Compute the canonical child-accessor method name for a `@oneToMany` field.
4
42
  *
package/dist/class.d.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { AIClient, AIClientOptions } from '@happyvertical/ai';
2
- import { FilesystemAdapter, FilesystemAdapterOptions } from '@happyvertical/files';
3
- import { LoggerConfig } from '@happyvertical/logger';
4
- import { AiUsageListOptions, AiUsageSnapshot, AiUsageStats, AiUsageSummaryOptions, SignalAdapter, SmrtAiUsageRecord } from '@happyvertical/smrt-types';
5
- import { DatabaseInterface } from '@happyvertical/sql';
6
- import { AiUsageConfig, MetricsConfig, PubSubConfig } from './config.js';
7
- import { DatabaseConfig } from './database.js';
8
- import { DecisionClient, DecisionConfig, DecisionOptions, DecisionRequest, DecisionResult } from './decisions.js';
1
+ import type { AIClient, AIClientOptions } from '@happyvertical/ai';
2
+ import type { FilesystemAdapter, FilesystemAdapterOptions } from '@happyvertical/files';
3
+ import { type LoggerConfig } from '@happyvertical/logger';
4
+ import type { AiUsageListOptions, AiUsageSnapshot, AiUsageStats, AiUsageSummaryOptions, SignalAdapter, SmrtAiUsageRecord } from '@happyvertical/smrt-types';
5
+ import { type DatabaseInterface } from '@happyvertical/sql';
6
+ import type { AiUsageConfig, MetricsConfig, PubSubConfig } from './config.js';
7
+ import type { DatabaseConfig } from './database.js';
8
+ import { type DecisionClient, type DecisionConfig, type DecisionOptions, type DecisionRequest, type DecisionResult } from './decisions.js';
9
9
  import { SignalBus } from './signals/bus.js';
10
10
  /**
11
11
  * Configuration options for the SmrtClass
@@ -1,4 +1,50 @@
1
- import { DatabaseInterface } from '@happyvertical/sql';
1
+ /**
2
+ * Opt-in read-through cache for collection reads (issue #1498).
3
+ *
4
+ * SSR-rendered SMRT apps re-query read-heavy / write-rare collections on
5
+ * every request, and the per-query round-trip dominates wall time when a
6
+ * page needs many collections. This module memoizes `list()`/`get()` row
7
+ * sets keyed by the final SQL + parameters, for an opt-in TTL.
8
+ *
9
+ * Correctness model:
10
+ * - Caching is OFF by default. It is enabled per call
11
+ * (`list({ cache: { ttl } })`) or per model (`@smrt({ cache: { ttl } })`).
12
+ * - SMRT owns every mutation path (`save()`/`delete()` back
13
+ * `collection.create()`, `getOrUpsert()`, junction attach/detach), so all
14
+ * writes invalidate the affected table's entries in-process automatically.
15
+ * - Entries and flights are partitioned by the concrete DatabaseInterface
16
+ * object, final SQL and parameters. Distinct transaction/connection handles
17
+ * never share rows or failures, even when their database URL is identical.
18
+ * - Invalidation remains URL/table scoped, so a write clears every handle's
19
+ * entries and STI siblings (which share a table) stay coherent.
20
+ * - Caches are per-process. With multiple replicas, a local invalidation
21
+ * leaves peers stale until TTL unless cross-process invalidation is opted
22
+ * into (`crossProcess: true`), which broadcasts over the database
23
+ * adapter's notification capability (e.g. Postgres LISTEN/NOTIFY) when
24
+ * the adapter provides one.
25
+ *
26
+ * Cached values are raw result rows, not hydrated instances — hydration and
27
+ * read interceptors (tenancy, audit) run on every call, cached or not, and
28
+ * each caller receives isolated row copies.
29
+ *
30
+ * Known limitations:
31
+ * - Invalidation fires when a mutation's SQL executes, not when its
32
+ * surrounding transaction commits. A write inside an uncommitted
33
+ * transaction invalidates (and may broadcast) immediately; a concurrent
34
+ * reader could repopulate the cache from the pre-commit snapshot, and a
35
+ * rollback leaves the cache invalidated for a write that never landed.
36
+ * Caching targets read-heavy / write-rare data where this is rare; for
37
+ * models mutated inside multi-statement transactions, coherence is still
38
+ * bounded by TTL. Per-call `cache: false` forces a fresh read where it
39
+ * matters.
40
+ * - Writes that bypass the framework's mutation paths (raw `db.query`
41
+ * issued outside `collection.query()`, external processes without
42
+ * `crossProcess`) are only bounded by TTL.
43
+ *
44
+ * @see https://github.com/happyvertical/smrt/issues/1498
45
+ * @packageDocumentation
46
+ */
47
+ import type { DatabaseInterface } from '@happyvertical/sql';
2
48
  /**
3
49
  * Read-through cache configuration for collection reads.
4
50
  *
@@ -1,6 +1,6 @@
1
- import { SmrtClassOptions } from './class';
2
- import { SmrtListOptions, SmrtSelectedRow, SmrtSelectField } from './collection';
3
- import { SmrtObject } from './object';
1
+ import type { SmrtClassOptions } from './class';
2
+ import type { SmrtListOptions, SmrtSelectedRow, SmrtSelectField } from './collection';
3
+ import type { SmrtObject } from './object';
4
4
  type DynamicSmrtObject = SmrtObject & Record<string, unknown>;
5
5
  type SmrtHydratedCollectionReadOptions<ModelType extends SmrtObject> = Omit<SmrtListOptions<ModelType>, 'select'> & {
6
6
  select?: undefined;