@happyvertical/smrt-core 0.38.19 → 0.38.21
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 +8 -0
- package/dist/collection.d.ts +82 -41
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +92 -57
- package/dist/collection.js.map +1 -1
- package/dist/interceptors.d.ts +2 -1
- package/dist/interceptors.d.ts.map +1 -1
- package/dist/interceptors.js.map +1 -1
- package/dist/manifest/static-manifest.js +4 -4
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest/test-manifest-stub.d.ts.map +1 -1
- package/dist/manifest/test-manifest-stub.js +680 -4
- package/dist/manifest/test-manifest-stub.js.map +1 -1
- package/dist/manifest.json +4 -4
- package/dist/runtime/types.d.ts +1 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +6 -6
- package/dist/testing/database.d.ts +5 -1
- package/dist/testing/database.d.ts.map +1 -1
- package/dist/testing/database.js +4 -1
- package/dist/testing/database.js.map +1 -1
- package/package.json +4 -4
package/dist/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "1.0.0",
|
|
3
|
-
"timestamp":
|
|
3
|
+
"timestamp": 1783580567061,
|
|
4
4
|
"packageName": "@happyvertical/smrt-core",
|
|
5
|
-
"packageVersion": "0.38.
|
|
5
|
+
"packageVersion": "0.38.21",
|
|
6
6
|
"objects": {
|
|
7
7
|
"@happyvertical/smrt-core:SmrtClass": {
|
|
8
8
|
"name": "smrtclass",
|
|
@@ -254,11 +254,11 @@
|
|
|
254
254
|
"parameters": [
|
|
255
255
|
{
|
|
256
256
|
"name": "options",
|
|
257
|
-
"type": "
|
|
257
|
+
"type": "SmrtListOptions<ModelType>",
|
|
258
258
|
"optional": true
|
|
259
259
|
}
|
|
260
260
|
],
|
|
261
|
-
"returnType": "Promise<ModelType[]>",
|
|
261
|
+
"returnType": "Promise<ModelType[] | Record<string>[]>",
|
|
262
262
|
"isStatic": false,
|
|
263
263
|
"isPublic": true
|
|
264
264
|
},
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/runtime/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EACD,OAAO,GACP;QACE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;QAC3B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;IACN,IAAI,CAAC,EAAE;QACL,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;QAIpC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;KAC9C,CAAC;CACH;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE;QACL,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;QACzB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,QAAQ,CAAC;IACnD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAAC;IACnC,IAAI,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;CAChC;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,OAAO,CAAC,EAAE;QACb,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAChC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;QAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/runtime/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EACD,OAAO,GACP;QACE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;QAC3B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;IACN,IAAI,CAAC,EAAE;QACL,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;QAIpC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;KAC9C,CAAC;CACH;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE;QACL,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;QACzB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,QAAQ,CAAC;IACnD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAAC;IACnC,IAAI,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;CAChC;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,OAAO,CAAC,EAAE;QACb,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAChC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;QAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;KAC5B,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IAEvB,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IAEzC,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAExC,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IAE3D,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAErC,KAAK,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACvE"}
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-07-
|
|
3
|
+
"generatedAt": "2026-07-09T07:02:45.049Z",
|
|
4
4
|
"packageName": "@happyvertical/smrt-core",
|
|
5
|
-
"packageVersion": "0.38.
|
|
5
|
+
"packageVersion": "0.38.21",
|
|
6
6
|
"sourceManifestPath": "dist/manifest.json",
|
|
7
7
|
"agentDocPath": "AGENTS.md",
|
|
8
8
|
"sourceHashes": {
|
|
9
|
-
"manifest": "
|
|
10
|
-
"packageJson": "
|
|
11
|
-
"agents": "
|
|
9
|
+
"manifest": "e6f73e90bb36a3b1379676e3c228a199bc315ec891dcbc33eef2f2da2e259823",
|
|
10
|
+
"packageJson": "fa8e6079c2f9cb0615c145b0b1a1dffb1737a656048ad9bbf76723fddabb7dc9",
|
|
11
|
+
"agents": "deb986648ec705f94f28da16ca2ca02e8cbcb350334571641623c03628801159"
|
|
12
12
|
},
|
|
13
13
|
"exports": [
|
|
14
14
|
".",
|
|
@@ -323,5 +323,5 @@
|
|
|
323
323
|
"polymorphicAssociations": 1,
|
|
324
324
|
"uuidColumns": 3
|
|
325
325
|
},
|
|
326
|
-
"agentDoc": "# @happyvertical/smrt-core\n\nORM, code generation, AI integration, and the DispatchBus. Everything else builds on this.\n\n## Key Classes\n\n| Class | File | Purpose |\n|-------|------|---------|\n| SmrtObject | `src/object.ts` | Base persistent object — save, delete, is(), do(), loadFromId/Slug |\n| SmrtCollection | `src/collection.ts` | CRUD collection — list, get, create, delete, getOrUpsert |\n| ObjectRegistry | `src/registry.ts` | Global singleton (globalThis) — class metadata, fields, STI chains, manifests |\n| DispatchBus | `src/dispatch/bus.ts` | Inter-agent messaging — emit, subscribe (persistent), process |\n| GlobalInterceptors | `src/interceptors.ts` | Plugin system — beforeList/Get/Save/Delete hooks (used by tenancy) |\n| LearningMemory | `src/learning/memory.ts` | Confidence-scored recall/capture over `_smrt_contexts` + embeddings (#1886) |\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\nConfidence-scored, self-correcting memory over the existing `_smrt_contexts` (keyed recall) and `_smrt_embeddings` (semantic recall) substrate. Wires the reinforcement columns that ship on `_smrt_contexts` but were never written (`success_count`, `failure_count`, and a `last_used_at` that recall now refreshes). This is L1 of the tenant-learning-agents epic; the opt-in `Learning` trait in `@happyvertical/smrt-agents` composes it into the agent lifecycle.\n\n```typescript\nconst memory = new LearningMemory({ db: obj.systemDb, ownerClass: 'InvoiceAgent', ownerId: obj.id, tenantId });\n\n// recall — union of keyed-context lookup + (optional) semantic search, confidence-filtered\nconst [hit] = await memory.recall('parser/acme', { key: docUrl }); // or { query } with a wired semanticSearch\nconst strategy = hit?.value ?? (await generate());\n\n// capture — reinforce the outcome\nawait memory.capture({ scope: 'parser/acme', key: docUrl, value: strategy }, { success: ok });\n```\n\n- **`capture(episode, outcome)`**: success strengthens `confidence` toward 1.0 + increments `success_count`; failure decays toward `failureConfidence` (default 0.3) + increments `failure_count`. Defaults (`minConfidence` 0.7, `successConfidence` 0.9, `reinforcement` 0.5) mean a single failure drops a confident memory below the reuse floor. Seeds a new row when none exists and the episode carries a `value` (a failed first attempt is retained at low confidence for self-correction).\n- **`recall(scope, opts)`**: owner-scoped keyed lookup (thus tenant-isolated) filtered by the confidence floor, expiry, and optional time-decay, with hierarchical scope fallback; unions an injected `semanticSearch` arm when a `query` is given (tenant-scoped via its `where`). Refreshes `last_used_at` on returned rows.\n- Injected `semanticSearch` matches `SmrtCollection.semanticSearch`, so `LearningMemory` never reaches into a collection's internals.\n\n## SmrtCollection Query\n\n```typescript\nawait collection.list({\n where: { status: 'active', price: { op: '>', value: 10 } },\n limit: 50, offset: 0, orderBy: 'created_at DESC'\n});\n```\n\n**WHERE operators**: `=`, `>`, `<`, `>=`, `<=`, `!=`, `in`, `not in`, `like`, `is null`, `is not null`. Arrays auto-detect `IN`. Dot notation for JSON paths: `metadata.userId`.\n\nSTI child collections auto-filter by `_meta_type`.\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`.\n\nRegistration sets `SMRT_TABLE_NAME` static property (survives minification).\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\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## Change Feed (#1758)\n\nAdapter-agnostic change-observation spine (`src/change-feed.ts`) — the server half of the client/mobile sync contract (PRD #1755):\n\n- `_smrt_changes` system table: one append per framework save/delete via a GlobalInterceptors writer registered at framework init. Deletes are tombstones (`operation: 'delete'`). `_smrt_*` tables are skipped. Feed-append failures log and never fail the user's write. No dirty-check: a field-unchanged `.save()` appends a spurious `update` entry (diff-aware paths like `getOrUpsert`/sync-apply short-circuit before `save()` and append nothing); subscribers must tolerate spurious entries — they are convergent.\n- Sequences: allocated as `MAX(seq)+1` inside the INSERT with conflict retry — committed rows stay contiguous, so commit order == seq order on SQLite/Postgres/DuckDB (deliberately NOT identity/serial: those allocate before commit and break the cursor guarantee under concurrent writers).\n- `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).\n- `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.\n- 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.\n- 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.\n\n## Live Events / Change Signals (#1763, server half)\n\nThe push companion to the change feed (`src/change-signals.ts` + the generated `_events` SSE route) — the server half of live cache invalidation (PRD #1755). The client subscriber (two-client/reconnect/polling-fallback ACs) is a separate later slice.\n\n- **Change-signal bus** (`src/change-signals.ts`): every framework save/delete that appends a durable feed row also publishes a coarse `ChangeSignal` `{ table, operation, rowId, tenantId, seq }` — **never a row payload** (authorization stays on the read path). Structurally mirrors the collection cache's notify/listen path. `subscribeToChangeSignals(db, listener) → unsubscribe`; `publishChangeSignal`/`broadcastChangeSignal`/the listener loop stay internal. `appendChange` now returns the allocated `seq` (was `void`); the signal carries it as a coarse resume cursor (`bumpChangeFeed` ignores the return). The publish runs only after the append SUCCEEDS (no signal without a durable feed row) and in its own log-and-swallow try/catch, so a signal problem never fails the user's write. `_smrt_*` writes never signal (the writer skips them). Delivery is synchronous per-listener with per-listener try/catch (one throwing SSE controller never blocks others); no per-subscriber queue — backpressure rides the platform `ReadableStream`.\n- **In-process + cross-replica**: locally-published and peer-received signals go through the SAME `deliverLocally` path. Cross-replica fan-out rides the db adapter's optional notification capability (`db.notifications`, a NEW `smrt_change_signals` channel distinct from the cache channel) with echo-avoidance by `PROCESS_ID`. No capability → in-process only, warn-once, **never an error, never blocks the write** (subscribers on other replicas fall back to cursor polling).\n- **Generated `_events` SSE route**: REST (`GET {basePath}/_events`, requires `authMiddleware`, otherwise 401 — fail-closed, per-model `api.public` does NOT apply; 405 non-GET; 503 no db or subscriber capacity reached) and SvelteKit (`{routesDir}/_events/+server.ts`, requires an authenticated principal on `locals`; opt out via `sveltekit.eventsRoute.enabled: false`; cap via `sveltekit.eventsRoute.maxSubscribers`, where `0` means unlimited). Tenant scope is captured ONCE at connection open (`resolveDispatchTenantScope`) and filtered server-side per signal via `signalVisibleToTenant` (same rule as `getChangesSince`'s tenant filter) before any byte hits the wire — delivery runs outside any tenant ALS context, so it must use the captured value. The stream lifecycle lives in `buildChangeEventStream(db, { cursor, tenantScope, heartbeatMs?, manifestHash? })` (exported; the SvelteKit route imports it so it stays thin): subscribe-before-catch-up (closes the gap window; overlap is deduped by the SSE `id:`/seq client-side), `retry: 3000`, optional connection-open `event: manifest` carrying `{ manifestHash }` for live contract detection, cursor catch-up via `getChangesSince` filtered by the CAPTURED scope (not re-resolved from ALS, so it matches the live-signal filter exactly and can't replay another tenant's rows) (`Last-Event-ID` header beats `?since=`; default = live-forward only; `resyncRequired` → `event: resync`), heartbeat (`DEFAULT_EVENTS_HEARTBEAT_MS` = 15s), and `cancel()` teardown (clears heartbeat + unsubscribes). SSE change frame: `id: <seq>\\nevent: change\\ndata: {table,operation,rowId,tenantId}\\n\\n` — seq is ONLY in the `id:` line, never the data JSON. Subscriber cap default is `DEFAULT_EVENTS_MAX_SUBSCRIBERS` = 1000; over-cap connections return retryable 503 + `Retry-After`, and existing subscribers are unaffected. **Same-origin only** (not CORS-wrapped): `EventSource` can't set headers and credentialed cross-origin needs Allow-Credentials the CORS helper doesn't emit — cross-origin SSE is a follow-up. Client disconnect through the Node `createServer` bridge now cancels the response reader (was a teardown leak) so `cancel()` fires and the subscription is released.\n- **Known gaps** (documented in the module): raw-SQL writes don't signal (same gap as the feed); live signals for caller-managed-transaction writes are best-effort (the append + signal fire pre-commit, so a rolled-back write may emit a signal and its freed seq is later reused) — the autocommit default path is exact, and clients reconcile via full catch-up/resync (inherits the change feed's transaction caveat).\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## 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\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. Three emission sites must not drift: the runtime value (`generateWebModule`), the `@happyvertical/smrt-virt-web` ambient d.ts (`vite-plugin/index.ts`), and the physical `@smrt/web` d.ts (`prebuild/index.ts`).\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\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\n## Gotchas\n\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- **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"
|
|
326
|
+
"agentDoc": "# @happyvertical/smrt-core\n\nORM, code generation, AI integration, and the DispatchBus. Everything else builds on this.\n\n## Key Classes\n\n| Class | File | Purpose |\n|-------|------|---------|\n| SmrtObject | `src/object.ts` | Base persistent object — save, delete, is(), do(), loadFromId/Slug |\n| SmrtCollection | `src/collection.ts` | CRUD collection — list, get, create, delete, getOrUpsert |\n| ObjectRegistry | `src/registry.ts` | Global singleton (globalThis) — class metadata, fields, STI chains, manifests |\n| DispatchBus | `src/dispatch/bus.ts` | Inter-agent messaging — emit, subscribe (persistent), process |\n| GlobalInterceptors | `src/interceptors.ts` | Plugin system — beforeList/Get/Save/Delete hooks (used by tenancy) |\n| LearningMemory | `src/learning/memory.ts` | Confidence-scored recall/capture over `_smrt_contexts` + embeddings (#1886) |\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\nConfidence-scored, self-correcting memory over the existing `_smrt_contexts` (keyed recall) and `_smrt_embeddings` (semantic recall) substrate. Wires the reinforcement columns that ship on `_smrt_contexts` but were never written (`success_count`, `failure_count`, and a `last_used_at` that recall now refreshes). This is L1 of the tenant-learning-agents epic; the opt-in `Learning` trait in `@happyvertical/smrt-agents` composes it into the agent lifecycle.\n\n```typescript\nconst memory = new LearningMemory({ db: obj.systemDb, ownerClass: 'InvoiceAgent', ownerId: obj.id, tenantId });\n\n// recall — union of keyed-context lookup + (optional) semantic search, confidence-filtered\nconst [hit] = await memory.recall('parser/acme', { key: docUrl }); // or { query } with a wired semanticSearch\nconst strategy = hit?.value ?? (await generate());\n\n// capture — reinforce the outcome\nawait memory.capture({ scope: 'parser/acme', key: docUrl, value: strategy }, { success: ok });\n```\n\n- **`capture(episode, outcome)`**: success strengthens `confidence` toward 1.0 + increments `success_count`; failure decays toward `failureConfidence` (default 0.3) + increments `failure_count`. Defaults (`minConfidence` 0.7, `successConfidence` 0.9, `reinforcement` 0.5) mean a single failure drops a confident memory below the reuse floor. Seeds a new row when none exists and the episode carries a `value` (a failed first attempt is retained at low confidence for self-correction).\n- **`recall(scope, opts)`**: owner-scoped keyed lookup (thus tenant-isolated) filtered by the confidence floor, expiry, and optional time-decay, with hierarchical scope fallback; unions an injected `semanticSearch` arm when a `query` is given (tenant-scoped via its `where`). Refreshes `last_used_at` on returned rows.\n- Injected `semanticSearch` matches `SmrtCollection.semanticSearch`, so `LearningMemory` never reaches into a collection's internals.\n\n## SmrtCollection Query\n\n```typescript\nawait collection.list({\n where: { status: 'active', price: { op: '>', value: 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**WHERE operators**: `=`, `>`, `<`, `>=`, `<=`, `!=`, `in`, `not in`, `like`, `is null`, `is not null`. Arrays auto-detect `IN`. Dot notation for JSON paths: `metadata.userId`.\n\nSTI child collections auto-filter by `_meta_type`.\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`.\n\nRegistration sets `SMRT_TABLE_NAME` static property (survives minification).\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\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## Change Feed (#1758)\n\nAdapter-agnostic change-observation spine (`src/change-feed.ts`) — the server half of the client/mobile sync contract (PRD #1755):\n\n- `_smrt_changes` system table: one append per framework save/delete via a GlobalInterceptors writer registered at framework init. Deletes are tombstones (`operation: 'delete'`). `_smrt_*` tables are skipped. Feed-append failures log and never fail the user's write. No dirty-check: a field-unchanged `.save()` appends a spurious `update` entry (diff-aware paths like `getOrUpsert`/sync-apply short-circuit before `save()` and append nothing); subscribers must tolerate spurious entries — they are convergent.\n- Sequences: allocated as `MAX(seq)+1` inside the INSERT with conflict retry — committed rows stay contiguous, so commit order == seq order on SQLite/Postgres/DuckDB (deliberately NOT identity/serial: those allocate before commit and break the cursor guarantee under concurrent writers).\n- `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).\n- `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.\n- 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.\n- 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.\n\n## Live Events / Change Signals (#1763, server half)\n\nThe push companion to the change feed (`src/change-signals.ts` + the generated `_events` SSE route) — the server half of live cache invalidation (PRD #1755). The client subscriber (two-client/reconnect/polling-fallback ACs) is a separate later slice.\n\n- **Change-signal bus** (`src/change-signals.ts`): every framework save/delete that appends a durable feed row also publishes a coarse `ChangeSignal` `{ table, operation, rowId, tenantId, seq }` — **never a row payload** (authorization stays on the read path). Structurally mirrors the collection cache's notify/listen path. `subscribeToChangeSignals(db, listener) → unsubscribe`; `publishChangeSignal`/`broadcastChangeSignal`/the listener loop stay internal. `appendChange` now returns the allocated `seq` (was `void`); the signal carries it as a coarse resume cursor (`bumpChangeFeed` ignores the return). The publish runs only after the append SUCCEEDS (no signal without a durable feed row) and in its own log-and-swallow try/catch, so a signal problem never fails the user's write. `_smrt_*` writes never signal (the writer skips them). Delivery is synchronous per-listener with per-listener try/catch (one throwing SSE controller never blocks others); no per-subscriber queue — backpressure rides the platform `ReadableStream`.\n- **In-process + cross-replica**: locally-published and peer-received signals go through the SAME `deliverLocally` path. Cross-replica fan-out rides the db adapter's optional notification capability (`db.notifications`, a NEW `smrt_change_signals` channel distinct from the cache channel) with echo-avoidance by `PROCESS_ID`. No capability → in-process only, warn-once, **never an error, never blocks the write** (subscribers on other replicas fall back to cursor polling).\n- **Generated `_events` SSE route**: REST (`GET {basePath}/_events`, requires `authMiddleware`, otherwise 401 — fail-closed, per-model `api.public` does NOT apply; 405 non-GET; 503 no db or subscriber capacity reached) and SvelteKit (`{routesDir}/_events/+server.ts`, requires an authenticated principal on `locals`; opt out via `sveltekit.eventsRoute.enabled: false`; cap via `sveltekit.eventsRoute.maxSubscribers`, where `0` means unlimited). Tenant scope is captured ONCE at connection open (`resolveDispatchTenantScope`) and filtered server-side per signal via `signalVisibleToTenant` (same rule as `getChangesSince`'s tenant filter) before any byte hits the wire — delivery runs outside any tenant ALS context, so it must use the captured value. The stream lifecycle lives in `buildChangeEventStream(db, { cursor, tenantScope, heartbeatMs?, manifestHash? })` (exported; the SvelteKit route imports it so it stays thin): subscribe-before-catch-up (closes the gap window; overlap is deduped by the SSE `id:`/seq client-side), `retry: 3000`, optional connection-open `event: manifest` carrying `{ manifestHash }` for live contract detection, cursor catch-up via `getChangesSince` filtered by the CAPTURED scope (not re-resolved from ALS, so it matches the live-signal filter exactly and can't replay another tenant's rows) (`Last-Event-ID` header beats `?since=`; default = live-forward only; `resyncRequired` → `event: resync`), heartbeat (`DEFAULT_EVENTS_HEARTBEAT_MS` = 15s), and `cancel()` teardown (clears heartbeat + unsubscribes). SSE change frame: `id: <seq>\\nevent: change\\ndata: {table,operation,rowId,tenantId}\\n\\n` — seq is ONLY in the `id:` line, never the data JSON. Subscriber cap default is `DEFAULT_EVENTS_MAX_SUBSCRIBERS` = 1000; over-cap connections return retryable 503 + `Retry-After`, and existing subscribers are unaffected. **Same-origin only** (not CORS-wrapped): `EventSource` can't set headers and credentialed cross-origin needs Allow-Credentials the CORS helper doesn't emit — cross-origin SSE is a follow-up. Client disconnect through the Node `createServer` bridge now cancels the response reader (was a teardown leak) so `cancel()` fires and the subscription is released.\n- **Known gaps** (documented in the module): raw-SQL writes don't signal (same gap as the feed); live signals for caller-managed-transaction writes are best-effort (the append + signal fire pre-commit, so a rolled-back write may emit a signal and its freed seq is later reused) — the autocommit default path is exact, and clients reconcile via full catch-up/resync (inherits the change feed's transaction caveat).\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## 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\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. Three emission sites must not drift: the runtime value (`generateWebModule`), the `@happyvertical/smrt-virt-web` ambient d.ts (`vite-plugin/index.ts`), and the physical `@smrt/web` d.ts (`prebuild/index.ts`).\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\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\n## Gotchas\n\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- **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"
|
|
327
327
|
}
|
|
@@ -7,8 +7,9 @@ export interface TestDatabaseOptions {
|
|
|
7
7
|
* Database type (default: 'sqlite')
|
|
8
8
|
* - 'sqlite': SQLite database
|
|
9
9
|
* - 'json': JSON adapter (stores data as JSON files with DuckDB for querying)
|
|
10
|
+
* - 'duckdb': Native DuckDB database
|
|
10
11
|
*/
|
|
11
|
-
type?: 'sqlite' | 'json';
|
|
12
|
+
type?: 'sqlite' | 'json' | 'duckdb';
|
|
12
13
|
/**
|
|
13
14
|
* Database URL (default: ':memory:')
|
|
14
15
|
* Use ':memory:' for in-memory databases (fastest for tests)
|
|
@@ -58,6 +59,9 @@ export interface TestDatabaseOptions {
|
|
|
58
59
|
* // JSON adapter instead of SQLite
|
|
59
60
|
* const db = await getTestDatabase({ type: 'json' });
|
|
60
61
|
*
|
|
62
|
+
* // Native DuckDB adapter
|
|
63
|
+
* const db = await getTestDatabase({ type: 'duckdb', url: ':memory:' });
|
|
64
|
+
*
|
|
61
65
|
* // File-based database for persistent tests
|
|
62
66
|
* const db = await getTestDatabase({ type: 'sqlite', url: '/tmp/test.db' });
|
|
63
67
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/testing/database.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAgJ5D;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/testing/database.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAgJ5D;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;OAKG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;IAEpC;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,EAAE,CAAC,EAAE,iBAAiB,CAAC;IAEvB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAEnB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAuCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAsB,eAAe,CACnC,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,iBAAiB,CAAC,CAwH5B"}
|
package/dist/testing/database.js
CHANGED
|
@@ -99,6 +99,9 @@ function resolveRequestedSchemaClassNames(classNames) {
|
|
|
99
99
|
* // JSON adapter instead of SQLite
|
|
100
100
|
* const db = await getTestDatabase({ type: 'json' });
|
|
101
101
|
*
|
|
102
|
+
* // Native DuckDB adapter
|
|
103
|
+
* const db = await getTestDatabase({ type: 'duckdb', url: ':memory:' });
|
|
104
|
+
*
|
|
102
105
|
* // File-based database for persistent tests
|
|
103
106
|
* const db = await getTestDatabase({ type: 'sqlite', url: '/tmp/test.db' });
|
|
104
107
|
*
|
|
@@ -121,7 +124,7 @@ async function getTestDatabase(options = {}) {
|
|
|
121
124
|
const classNames = resolveRequestedSchemaClassNames(classes ?? ObjectRegistry.getQualifiedClassNames());
|
|
122
125
|
if (classNames.length === 0) return db;
|
|
123
126
|
const schemaGenerator = new SchemaGenerator();
|
|
124
|
-
const ddlEngine = type === "json" || typeof db.exportTable === "function" ? "json" : "sqlite";
|
|
127
|
+
const ddlEngine = type === "json" || typeof db.exportTable === "function" ? "json" : type === "duckdb" ? "duckdb" : "sqlite";
|
|
125
128
|
const createdTables = /* @__PURE__ */ new Set();
|
|
126
129
|
for (const className of classNames) {
|
|
127
130
|
const registered = ObjectRegistry.getClass(className);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.js","names":[],"sources":["../../src/testing/database.ts"],"sourcesContent":["/**\n * Test database utilities for SMRT framework\n *\n * Provides `getTestDatabase()` which creates an in-memory database with all\n * registered SMRT object schemas. Uses the same schema generation logic as\n * the migration system to ensure consistency between test and production.\n *\n * @example\n * ```typescript\n * import { getTestDatabase } from '@happyvertical/smrt-core/testing';\n *\n * beforeEach(async () => {\n * const db = await getTestDatabase();\n * collection = await MyCollection.create({ db });\n * });\n * ```\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { getDatabase } from '@happyvertical/sql';\nimport {\n type CollectionRegistrationLookup,\n isCollectionRegistration,\n resolveCollectionItemClassName,\n resolveRelatedRegistration,\n} from '../registry/collection-resolution.js';\nimport { ObjectRegistry } from '../registry.js';\nimport { SchemaGenerator } from '../schema/generator.js';\nimport { ensureLegacySystemTableCompatibility } from '../system/compatibility.js';\nimport { ALL_SYSTEM_TABLES } from '../system/schema.js';\n\ntype TestDatabaseConnectionOptions = Parameters<typeof getDatabase>[0] & {\n __smrtSkipVitestSchemaPreparation?: boolean;\n};\n\nfunction resolveSTIBaseRegistration(className: string, stiBaseName: string) {\n const registered = ObjectRegistry.getClass(className);\n\n if (stiBaseName.includes(':')) {\n return ObjectRegistry.getClass(stiBaseName);\n }\n\n if (registered?.packageName) {\n const samePackageBase = ObjectRegistry.getClassInPackage(\n registered.packageName,\n stiBaseName,\n );\n if (samePackageBase) {\n return samePackageBase;\n }\n }\n\n return ObjectRegistry.getClass(stiBaseName);\n}\n\nfunction resolveSTIBaseLookupName(\n className: string,\n stiBaseName: string,\n): string {\n const stiBase = resolveSTIBaseRegistration(className, stiBaseName);\n return stiBase?.qualifiedName || stiBase?.name || stiBaseName;\n}\n\nfunction isSTIChild(className: string): boolean {\n const stiBaseName = ObjectRegistry.getSTIBase(className);\n if (!stiBaseName) {\n return false;\n }\n\n const registered = ObjectRegistry.getClass(className);\n const stiBase = resolveSTIBaseRegistration(className, stiBaseName);\n\n if (registered && stiBase) {\n return registered !== stiBase;\n }\n\n return stiBaseName !== className;\n}\n\ntype RegisteredSchemaClass = NonNullable<\n ReturnType<typeof ObjectRegistry.getClass>\n>;\n\nconst collectionRegistrationLookup: CollectionRegistrationLookup = {\n findClass: (className) => ObjectRegistry.getClass(className),\n findClassInPackage: (packageName, className) =>\n ObjectRegistry.getClassInPackage(packageName, className),\n getInheritanceChain: (className) =>\n ObjectRegistry.getInheritanceChain(className),\n};\n\nfunction resolveCollectionSchemaClassName(\n className: string,\n registered: RegisteredSchemaClass,\n): string {\n const itemClassName = resolveCollectionItemClassName(\n className,\n registered,\n collectionRegistrationLookup,\n );\n if (itemClassName) {\n const itemRegistration = resolveRelatedRegistration(\n itemClassName,\n registered,\n collectionRegistrationLookup,\n );\n const itemLookupName =\n itemRegistration?.qualifiedName ||\n itemRegistration?.name ||\n itemClassName;\n const stiBase = ObjectRegistry.getSTIBase(itemLookupName);\n return stiBase\n ? resolveSTIBaseLookupName(itemLookupName, stiBase)\n : itemLookupName;\n }\n\n const tableName =\n registered.schema?.tableName ||\n registered.config.tableName ||\n ObjectRegistry.getTableName(className);\n if (!tableName) {\n return className;\n }\n\n const collectionPackage = registered.packageName;\n\n for (const candidate of ObjectRegistry.getAllClasses().values()) {\n if (\n candidate === registered ||\n isCollectionRegistration(\n candidate.qualifiedName || candidate.name,\n candidate,\n collectionRegistrationLookup,\n )\n ) {\n continue;\n }\n\n const candidateTableName =\n candidate.schema?.tableName || candidate.config.tableName;\n if (candidateTableName !== tableName) {\n continue;\n }\n\n if (\n collectionPackage &&\n candidate.packageName &&\n candidate.packageName !== collectionPackage\n ) {\n continue;\n }\n\n const candidateLookupName = candidate.qualifiedName || candidate.name;\n const stiBase = ObjectRegistry.getSTIBase(candidateLookupName);\n return stiBase\n ? resolveSTIBaseLookupName(candidateLookupName, stiBase)\n : candidateLookupName;\n }\n\n return className;\n}\n\n/**\n * Options for creating a test database\n */\nexport interface TestDatabaseOptions {\n /**\n * Database type (default: 'sqlite')\n * - 'sqlite': SQLite database\n * - 'json': JSON adapter (stores data as JSON files with DuckDB for querying)\n */\n type?: 'sqlite' | 'json';\n\n /**\n * Database URL (default: ':memory:')\n * Use ':memory:' for in-memory databases (fastest for tests)\n * Or provide a file path for persistent test databases\n */\n url?: string;\n\n /**\n * Pre-existing database to initialize schemas in.\n * If provided, `type` and `url` are ignored.\n */\n db?: DatabaseInterface;\n\n /**\n * Specific classes to setup schemas for.\n * If not provided, sets up schemas for all registered classes.\n */\n classes?: string[];\n\n /**\n * Whether to include system tables (default: true)\n * System tables include _smrt_contexts, _smrt_migrations, etc.\n */\n includeSystemTables?: boolean;\n}\n\nfunction resolveRequestedSchemaClassName(className: string): string {\n const registered = ObjectRegistry.getClass(className);\n if (!registered) {\n return className;\n }\n\n if (\n !isCollectionRegistration(\n className,\n registered,\n collectionRegistrationLookup,\n )\n ) {\n const stiBase = ObjectRegistry.getSTIBase(className);\n return stiBase ? resolveSTIBaseLookupName(className, stiBase) : className;\n }\n\n return resolveCollectionSchemaClassName(className, registered);\n}\n\nfunction resolveRequestedSchemaClassNames(classNames: string[]): string[] {\n const resolved: string[] = [];\n const seen = new Set<string>();\n\n for (const className of classNames) {\n const schemaClassName = resolveRequestedSchemaClassName(className);\n if (seen.has(schemaClassName)) {\n continue;\n }\n\n seen.add(schemaClassName);\n resolved.push(schemaClassName);\n }\n\n return resolved;\n}\n\n/**\n * Creates an in-memory test database with schemas pre-created\n *\n * This utility is designed for testing. It creates an in-memory database\n * and initializes all registered SMRT object schemas using the **same\n * schema generation logic** as the production migration system.\n *\n * **Key features:**\n * - Uses `SchemaGenerator.generateSQL()` - the single source of truth for DDL\n * - Handles STI (Single Table Inheritance) correctly\n * - Creates system tables for framework functionality\n * - Safe for parallel test execution (each call creates isolated instance)\n *\n * @param options - Configuration options\n * @returns Promise resolving to configured DatabaseInterface\n *\n * @example\n * ```typescript\n * // Basic usage - all registered schemas\n * const db = await getTestDatabase();\n *\n * // Specific classes only\n * const db = await getTestDatabase({ classes: ['Council', 'Meeting'] });\n *\n * // JSON adapter instead of SQLite\n * const db = await getTestDatabase({ type: 'json' });\n *\n * // File-based database for persistent tests\n * const db = await getTestDatabase({ type: 'sqlite', url: '/tmp/test.db' });\n *\n * // Initialize schemas in an existing database\n * const myDb = await getDatabase({ type: 'sqlite', url: ':memory:' });\n * await getTestDatabase({ db: myDb });\n *\n * // Skip system tables (rare use case)\n * const db = await getTestDatabase({ includeSystemTables: false });\n * ```\n */\nexport async function getTestDatabase(\n options: TestDatabaseOptions = {},\n): Promise<DatabaseInterface> {\n const {\n type = 'sqlite',\n url = ':memory:',\n db: existingDb,\n classes,\n includeSystemTables = true,\n } = options;\n\n // Use existing database or create new one\n const db =\n existingDb ??\n (await getDatabase({\n type,\n url,\n __smrtSkipVitestSchemaPreparation: true,\n } as TestDatabaseConnectionOptions));\n\n // Initialize system tables (same as production)\n if (includeSystemTables) {\n await initializeSystemTables(db);\n }\n\n // Get class names to setup\n const classNames = resolveRequestedSchemaClassNames(\n classes ?? ObjectRegistry.getQualifiedClassNames(),\n );\n\n // Skip if no classes registered\n if (classNames.length === 0) {\n return db;\n }\n\n // Use the same schema generation as production\n const schemaGenerator = new SchemaGenerator();\n const ddlEngine =\n type === 'json' ||\n typeof (db as { exportTable?: unknown }).exportTable === 'function'\n ? 'json'\n : 'sqlite';\n\n // Track created tables to avoid duplicates (STI base classes)\n const createdTables = new Set<string>();\n\n for (const className of classNames) {\n // R11: the registration carries idType (native uuid vs text); read it\n // below when building runtimeSchemaConfig.\n const registered = ObjectRegistry.getClass(className);\n // Skip STI children - their schema is part of the base class table.\n // main (#1324): isSTIChild() compares RegisteredClass *identity* rather\n // than raw strings, so it stays correct under R5-canon (getSTIBase returns\n // qualified names) while also handling collection/override registrations.\n if (isSTIChild(className)) {\n continue;\n }\n\n const tableName = ObjectRegistry.getTableName(className);\n if (!tableName || createdTables.has(tableName)) {\n continue;\n }\n\n const fields = await ObjectRegistry.getAllFields(className);\n const strategy = ObjectRegistry.getTableStrategy(className);\n const runtimeSchemaConfig = {\n conflictColumns: ObjectRegistry.getConflictColumns(className),\n idType: registered?.config.idType,\n registry: ObjectRegistry,\n };\n\n // Generate schema using SchemaGenerator (same as migrations)\n const schema =\n strategy === 'sti'\n ? await schemaGenerator.generateSTISchemaFromRegistry(\n className,\n tableName,\n fields,\n runtimeSchemaConfig,\n )\n : schemaGenerator.generateSchemaFromRegistry(\n className,\n tableName,\n fields,\n runtimeSchemaConfig,\n );\n\n // Generate DDL using generateSQL() - the single source of truth\n const ddl = schemaGenerator.generateSQL(schema, ddlEngine);\n\n try {\n await db.query(ddl);\n createdTables.add(tableName);\n\n // Create indexes (use DDL strategy so jsonPath / where / etc. render)\n const ddlStrategy = (\n await import('../schema/ddl/index.js')\n ).getDDLStrategy(ddlEngine);\n const indexStatements = ddlStrategy.generateIndexes(schema);\n for (const indexSQL of indexStatements) {\n try {\n await db.query(indexSQL);\n } catch (indexError) {\n // Log but don't fail on index creation errors\n // Some indexes may fail if columns don't exist (STI meta fields)\n console.warn(\n `[getTestDatabase] Warning: Failed to create index: ${indexError instanceof Error ? indexError.message : String(indexError)} (SQL: ${indexSQL})`,\n );\n }\n }\n } catch (error) {\n // Provide helpful error message for table creation failures\n throw new Error(\n `[getTestDatabase] Failed to create table '${tableName}' for class '${className}': ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n }\n\n return db;\n}\n\n/**\n * Initialize SMRT system tables in a database\n *\n * System tables use _smrt_ prefix and store framework metadata.\n * All statements use `IF NOT EXISTS` for idempotency.\n *\n * @param db - Database interface to initialize\n */\nasync function initializeSystemTables(db: DatabaseInterface): Promise<void> {\n await ensureLegacySystemTableCompatibility(db);\n\n // Split multi-statement SQL into individual statements\n const allStatements: string[] = [];\n for (const multiStatementSQL of ALL_SYSTEM_TABLES) {\n const statements = multiStatementSQL\n .split(';')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n allStatements.push(...statements);\n }\n\n // Use db.query() — system tables use CREATE TABLE/INDEX IF NOT EXISTS\n // which databases handle natively without per-column existence checks.\n for (const statement of allStatements) {\n await db.query(statement);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"database.js","names":[],"sources":["../../src/testing/database.ts"],"sourcesContent":["/**\n * Test database utilities for SMRT framework\n *\n * Provides `getTestDatabase()` which creates an in-memory database with all\n * registered SMRT object schemas. Uses the same schema generation logic as\n * the migration system to ensure consistency between test and production.\n *\n * @example\n * ```typescript\n * import { getTestDatabase } from '@happyvertical/smrt-core/testing';\n *\n * beforeEach(async () => {\n * const db = await getTestDatabase();\n * collection = await MyCollection.create({ db });\n * });\n * ```\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { getDatabase } from '@happyvertical/sql';\nimport {\n type CollectionRegistrationLookup,\n isCollectionRegistration,\n resolveCollectionItemClassName,\n resolveRelatedRegistration,\n} from '../registry/collection-resolution.js';\nimport { ObjectRegistry } from '../registry.js';\nimport { SchemaGenerator } from '../schema/generator.js';\nimport { ensureLegacySystemTableCompatibility } from '../system/compatibility.js';\nimport { ALL_SYSTEM_TABLES } from '../system/schema.js';\n\ntype TestDatabaseConnectionOptions = Parameters<typeof getDatabase>[0] & {\n __smrtSkipVitestSchemaPreparation?: boolean;\n};\n\nfunction resolveSTIBaseRegistration(className: string, stiBaseName: string) {\n const registered = ObjectRegistry.getClass(className);\n\n if (stiBaseName.includes(':')) {\n return ObjectRegistry.getClass(stiBaseName);\n }\n\n if (registered?.packageName) {\n const samePackageBase = ObjectRegistry.getClassInPackage(\n registered.packageName,\n stiBaseName,\n );\n if (samePackageBase) {\n return samePackageBase;\n }\n }\n\n return ObjectRegistry.getClass(stiBaseName);\n}\n\nfunction resolveSTIBaseLookupName(\n className: string,\n stiBaseName: string,\n): string {\n const stiBase = resolveSTIBaseRegistration(className, stiBaseName);\n return stiBase?.qualifiedName || stiBase?.name || stiBaseName;\n}\n\nfunction isSTIChild(className: string): boolean {\n const stiBaseName = ObjectRegistry.getSTIBase(className);\n if (!stiBaseName) {\n return false;\n }\n\n const registered = ObjectRegistry.getClass(className);\n const stiBase = resolveSTIBaseRegistration(className, stiBaseName);\n\n if (registered && stiBase) {\n return registered !== stiBase;\n }\n\n return stiBaseName !== className;\n}\n\ntype RegisteredSchemaClass = NonNullable<\n ReturnType<typeof ObjectRegistry.getClass>\n>;\n\nconst collectionRegistrationLookup: CollectionRegistrationLookup = {\n findClass: (className) => ObjectRegistry.getClass(className),\n findClassInPackage: (packageName, className) =>\n ObjectRegistry.getClassInPackage(packageName, className),\n getInheritanceChain: (className) =>\n ObjectRegistry.getInheritanceChain(className),\n};\n\nfunction resolveCollectionSchemaClassName(\n className: string,\n registered: RegisteredSchemaClass,\n): string {\n const itemClassName = resolveCollectionItemClassName(\n className,\n registered,\n collectionRegistrationLookup,\n );\n if (itemClassName) {\n const itemRegistration = resolveRelatedRegistration(\n itemClassName,\n registered,\n collectionRegistrationLookup,\n );\n const itemLookupName =\n itemRegistration?.qualifiedName ||\n itemRegistration?.name ||\n itemClassName;\n const stiBase = ObjectRegistry.getSTIBase(itemLookupName);\n return stiBase\n ? resolveSTIBaseLookupName(itemLookupName, stiBase)\n : itemLookupName;\n }\n\n const tableName =\n registered.schema?.tableName ||\n registered.config.tableName ||\n ObjectRegistry.getTableName(className);\n if (!tableName) {\n return className;\n }\n\n const collectionPackage = registered.packageName;\n\n for (const candidate of ObjectRegistry.getAllClasses().values()) {\n if (\n candidate === registered ||\n isCollectionRegistration(\n candidate.qualifiedName || candidate.name,\n candidate,\n collectionRegistrationLookup,\n )\n ) {\n continue;\n }\n\n const candidateTableName =\n candidate.schema?.tableName || candidate.config.tableName;\n if (candidateTableName !== tableName) {\n continue;\n }\n\n if (\n collectionPackage &&\n candidate.packageName &&\n candidate.packageName !== collectionPackage\n ) {\n continue;\n }\n\n const candidateLookupName = candidate.qualifiedName || candidate.name;\n const stiBase = ObjectRegistry.getSTIBase(candidateLookupName);\n return stiBase\n ? resolveSTIBaseLookupName(candidateLookupName, stiBase)\n : candidateLookupName;\n }\n\n return className;\n}\n\n/**\n * Options for creating a test database\n */\nexport interface TestDatabaseOptions {\n /**\n * Database type (default: 'sqlite')\n * - 'sqlite': SQLite database\n * - 'json': JSON adapter (stores data as JSON files with DuckDB for querying)\n * - 'duckdb': Native DuckDB database\n */\n type?: 'sqlite' | 'json' | 'duckdb';\n\n /**\n * Database URL (default: ':memory:')\n * Use ':memory:' for in-memory databases (fastest for tests)\n * Or provide a file path for persistent test databases\n */\n url?: string;\n\n /**\n * Pre-existing database to initialize schemas in.\n * If provided, `type` and `url` are ignored.\n */\n db?: DatabaseInterface;\n\n /**\n * Specific classes to setup schemas for.\n * If not provided, sets up schemas for all registered classes.\n */\n classes?: string[];\n\n /**\n * Whether to include system tables (default: true)\n * System tables include _smrt_contexts, _smrt_migrations, etc.\n */\n includeSystemTables?: boolean;\n}\n\nfunction resolveRequestedSchemaClassName(className: string): string {\n const registered = ObjectRegistry.getClass(className);\n if (!registered) {\n return className;\n }\n\n if (\n !isCollectionRegistration(\n className,\n registered,\n collectionRegistrationLookup,\n )\n ) {\n const stiBase = ObjectRegistry.getSTIBase(className);\n return stiBase ? resolveSTIBaseLookupName(className, stiBase) : className;\n }\n\n return resolveCollectionSchemaClassName(className, registered);\n}\n\nfunction resolveRequestedSchemaClassNames(classNames: string[]): string[] {\n const resolved: string[] = [];\n const seen = new Set<string>();\n\n for (const className of classNames) {\n const schemaClassName = resolveRequestedSchemaClassName(className);\n if (seen.has(schemaClassName)) {\n continue;\n }\n\n seen.add(schemaClassName);\n resolved.push(schemaClassName);\n }\n\n return resolved;\n}\n\n/**\n * Creates an in-memory test database with schemas pre-created\n *\n * This utility is designed for testing. It creates an in-memory database\n * and initializes all registered SMRT object schemas using the **same\n * schema generation logic** as the production migration system.\n *\n * **Key features:**\n * - Uses `SchemaGenerator.generateSQL()` - the single source of truth for DDL\n * - Handles STI (Single Table Inheritance) correctly\n * - Creates system tables for framework functionality\n * - Safe for parallel test execution (each call creates isolated instance)\n *\n * @param options - Configuration options\n * @returns Promise resolving to configured DatabaseInterface\n *\n * @example\n * ```typescript\n * // Basic usage - all registered schemas\n * const db = await getTestDatabase();\n *\n * // Specific classes only\n * const db = await getTestDatabase({ classes: ['Council', 'Meeting'] });\n *\n * // JSON adapter instead of SQLite\n * const db = await getTestDatabase({ type: 'json' });\n *\n * // Native DuckDB adapter\n * const db = await getTestDatabase({ type: 'duckdb', url: ':memory:' });\n *\n * // File-based database for persistent tests\n * const db = await getTestDatabase({ type: 'sqlite', url: '/tmp/test.db' });\n *\n * // Initialize schemas in an existing database\n * const myDb = await getDatabase({ type: 'sqlite', url: ':memory:' });\n * await getTestDatabase({ db: myDb });\n *\n * // Skip system tables (rare use case)\n * const db = await getTestDatabase({ includeSystemTables: false });\n * ```\n */\nexport async function getTestDatabase(\n options: TestDatabaseOptions = {},\n): Promise<DatabaseInterface> {\n const {\n type = 'sqlite',\n url = ':memory:',\n db: existingDb,\n classes,\n includeSystemTables = true,\n } = options;\n\n // Use existing database or create new one\n const db =\n existingDb ??\n (await getDatabase({\n type,\n url,\n __smrtSkipVitestSchemaPreparation: true,\n } as TestDatabaseConnectionOptions));\n\n // Initialize system tables (same as production)\n if (includeSystemTables) {\n await initializeSystemTables(db);\n }\n\n // Get class names to setup\n const classNames = resolveRequestedSchemaClassNames(\n classes ?? ObjectRegistry.getQualifiedClassNames(),\n );\n\n // Skip if no classes registered\n if (classNames.length === 0) {\n return db;\n }\n\n // Use the same schema generation as production\n const schemaGenerator = new SchemaGenerator();\n const ddlEngine =\n type === 'json' ||\n typeof (db as { exportTable?: unknown }).exportTable === 'function'\n ? 'json'\n : type === 'duckdb'\n ? 'duckdb'\n : 'sqlite';\n\n // Track created tables to avoid duplicates (STI base classes)\n const createdTables = new Set<string>();\n\n for (const className of classNames) {\n // R11: the registration carries idType (native uuid vs text); read it\n // below when building runtimeSchemaConfig.\n const registered = ObjectRegistry.getClass(className);\n // Skip STI children - their schema is part of the base class table.\n // main (#1324): isSTIChild() compares RegisteredClass *identity* rather\n // than raw strings, so it stays correct under R5-canon (getSTIBase returns\n // qualified names) while also handling collection/override registrations.\n if (isSTIChild(className)) {\n continue;\n }\n\n const tableName = ObjectRegistry.getTableName(className);\n if (!tableName || createdTables.has(tableName)) {\n continue;\n }\n\n const fields = await ObjectRegistry.getAllFields(className);\n const strategy = ObjectRegistry.getTableStrategy(className);\n const runtimeSchemaConfig = {\n conflictColumns: ObjectRegistry.getConflictColumns(className),\n idType: registered?.config.idType,\n registry: ObjectRegistry,\n };\n\n // Generate schema using SchemaGenerator (same as migrations)\n const schema =\n strategy === 'sti'\n ? await schemaGenerator.generateSTISchemaFromRegistry(\n className,\n tableName,\n fields,\n runtimeSchemaConfig,\n )\n : schemaGenerator.generateSchemaFromRegistry(\n className,\n tableName,\n fields,\n runtimeSchemaConfig,\n );\n\n // Generate DDL using generateSQL() - the single source of truth\n const ddl = schemaGenerator.generateSQL(schema, ddlEngine);\n\n try {\n await db.query(ddl);\n createdTables.add(tableName);\n\n // Create indexes (use DDL strategy so jsonPath / where / etc. render)\n const ddlStrategy = (\n await import('../schema/ddl/index.js')\n ).getDDLStrategy(ddlEngine);\n const indexStatements = ddlStrategy.generateIndexes(schema);\n for (const indexSQL of indexStatements) {\n try {\n await db.query(indexSQL);\n } catch (indexError) {\n // Log but don't fail on index creation errors\n // Some indexes may fail if columns don't exist (STI meta fields)\n console.warn(\n `[getTestDatabase] Warning: Failed to create index: ${indexError instanceof Error ? indexError.message : String(indexError)} (SQL: ${indexSQL})`,\n );\n }\n }\n } catch (error) {\n // Provide helpful error message for table creation failures\n throw new Error(\n `[getTestDatabase] Failed to create table '${tableName}' for class '${className}': ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n }\n\n return db;\n}\n\n/**\n * Initialize SMRT system tables in a database\n *\n * System tables use _smrt_ prefix and store framework metadata.\n * All statements use `IF NOT EXISTS` for idempotency.\n *\n * @param db - Database interface to initialize\n */\nasync function initializeSystemTables(db: DatabaseInterface): Promise<void> {\n await ensureLegacySystemTableCompatibility(db);\n\n // Split multi-statement SQL into individual statements\n const allStatements: string[] = [];\n for (const multiStatementSQL of ALL_SYSTEM_TABLES) {\n const statements = multiStatementSQL\n .split(';')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n allStatements.push(...statements);\n }\n\n // Use db.query() — system tables use CREATE TABLE/INDEX IF NOT EXISTS\n // which databases handle natively without per-column existence checks.\n for (const statement of allStatements) {\n await db.query(statement);\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-core",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.21",
|
|
4
4
|
"description": "Core AI agent framework with standardized collections, object-relational mapping, and code generators",
|
|
5
5
|
"author": "HappyVertical",
|
|
6
6
|
"type": "module",
|
|
@@ -154,9 +154,9 @@
|
|
|
154
154
|
"tsx": "^4.23.0",
|
|
155
155
|
"typescript": "^5.9.3",
|
|
156
156
|
"yaml": "^2.9.0",
|
|
157
|
-
"@happyvertical/smrt-config": "0.38.
|
|
158
|
-
"@happyvertical/smrt-
|
|
159
|
-
"@happyvertical/smrt-
|
|
157
|
+
"@happyvertical/smrt-config": "0.38.21",
|
|
158
|
+
"@happyvertical/smrt-types": "0.38.21",
|
|
159
|
+
"@happyvertical/smrt-scanner": "0.38.21"
|
|
160
160
|
},
|
|
161
161
|
"peerDependencies": {
|
|
162
162
|
"@huggingface/transformers": ">=3.0.0 <4.0.0",
|