@happyvertical/smrt-core 0.41.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/AGENTS.md +1 -1
  2. package/agents/schema-paths.md +79 -8
  3. package/dist/change-feed.d.ts.map +1 -1
  4. package/dist/change-feed.js +2 -2
  5. package/dist/change-feed.js.map +1 -1
  6. package/dist/change-signals.d.ts.map +1 -1
  7. package/dist/change-signals.js +8 -1
  8. package/dist/change-signals.js.map +1 -1
  9. package/dist/class.d.ts.map +1 -1
  10. package/dist/class.js +10 -9
  11. package/dist/class.js.map +1 -1
  12. package/dist/collection.d.ts.map +1 -1
  13. package/dist/collection.js +2 -2
  14. package/dist/collection.js.map +1 -1
  15. package/dist/dispatch/collections/Dispatches.d.ts.map +1 -1
  16. package/dist/dispatch/collections/Dispatches.js +5 -5
  17. package/dist/dispatch/collections/Dispatches.js.map +1 -1
  18. package/dist/dispatch/models/Dispatch.d.ts +1 -1
  19. package/dist/dispatch/models/Dispatch.d.ts.map +1 -1
  20. package/dist/dispatch/models/Dispatch.js +3 -2
  21. package/dist/dispatch/models/Dispatch.js.map +1 -1
  22. package/dist/dispatch/models/DispatchSubscription.d.ts +1 -1
  23. package/dist/dispatch/models/DispatchSubscription.d.ts.map +1 -1
  24. package/dist/dispatch/models/DispatchSubscription.js +2 -1
  25. package/dist/dispatch/models/DispatchSubscription.js.map +1 -1
  26. package/dist/embeddings/storage.d.ts.map +1 -1
  27. package/dist/embeddings/storage.js +2 -1
  28. package/dist/embeddings/storage.js.map +1 -1
  29. package/dist/learning/memory.d.ts.map +1 -1
  30. package/dist/learning/memory.js +5 -4
  31. package/dist/learning/memory.js.map +1 -1
  32. package/dist/manifest/static-manifest.js +1 -1
  33. package/dist/manifest/static-manifest.js.map +1 -1
  34. package/dist/manifest/store.js +1 -1
  35. package/dist/manifest.json +1 -1
  36. package/dist/migrations/differ.js +2 -2
  37. package/dist/migrations/differ.js.map +1 -1
  38. package/dist/migrations/index.d.ts +1 -0
  39. package/dist/migrations/index.d.ts.map +1 -1
  40. package/dist/migrations/index.js +2 -1
  41. package/dist/migrations/integer-width.d.ts +99 -0
  42. package/dist/migrations/integer-width.d.ts.map +1 -0
  43. package/dist/migrations/integer-width.js +237 -0
  44. package/dist/migrations/integer-width.js.map +1 -0
  45. package/dist/migrations/minor-units.d.ts +2 -2
  46. package/dist/migrations/minor-units.d.ts.map +1 -1
  47. package/dist/migrations/minor-units.js +16 -14
  48. package/dist/migrations/minor-units.js.map +1 -1
  49. package/dist/migrations/tracker.d.ts.map +1 -1
  50. package/dist/migrations/tracker.js +28 -20
  51. package/dist/migrations/tracker.js.map +1 -1
  52. package/dist/migrations.js +2 -1
  53. package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -1
  54. package/dist/schema/ddl/duckdb-strategy.js +1 -0
  55. package/dist/schema/ddl/duckdb-strategy.js.map +1 -1
  56. package/dist/schema/ddl/postgres-strategy.d.ts.map +1 -1
  57. package/dist/schema/ddl/postgres-strategy.js +2 -0
  58. package/dist/schema/ddl/postgres-strategy.js.map +1 -1
  59. package/dist/schema/generator.d.ts +1 -33
  60. package/dist/schema/generator.d.ts.map +1 -1
  61. package/dist/schema/generator.js +1 -150
  62. package/dist/schema/generator.js.map +1 -1
  63. package/dist/schema/index.d.ts +1 -2
  64. package/dist/schema/index.d.ts.map +1 -1
  65. package/dist/schema/index.js +1 -2
  66. package/dist/schema/live-parity.d.ts +3 -1
  67. package/dist/schema/live-parity.d.ts.map +1 -1
  68. package/dist/schema/live-parity.js +19 -0
  69. package/dist/schema/live-parity.js.map +1 -1
  70. package/dist/schema/types.d.ts +17 -10
  71. package/dist/schema/types.d.ts.map +1 -1
  72. package/dist/schema.js +1 -2
  73. package/dist/smrt-knowledge.json +7 -7
  74. package/dist/system/retention.d.ts.map +1 -1
  75. package/dist/system/retention.js +3 -3
  76. package/dist/system/retention.js.map +1 -1
  77. package/dist/system/schema.d.ts +2 -1
  78. package/dist/system/schema.d.ts.map +1 -1
  79. package/dist/system/schema.js +4 -2
  80. package/dist/system/schema.js.map +1 -1
  81. package/dist/utils/safe-integer.d.ts +19 -0
  82. package/dist/utils/safe-integer.d.ts.map +1 -0
  83. package/dist/utils/safe-integer.js +31 -0
  84. package/dist/utils/safe-integer.js.map +1 -0
  85. package/dist/utils.d.ts +2 -1
  86. package/dist/utils.d.ts.map +1 -1
  87. package/dist/utils.js +6 -3
  88. package/dist/utils.js.map +1 -1
  89. package/dist/vite-plugin/index.d.ts.map +1 -1
  90. package/dist/vite-plugin/index.js +0 -53
  91. package/dist/vite-plugin/index.js.map +1 -1
  92. package/package.json +4 -4
  93. package/dist/schema/override-system.d.ts +0 -43
  94. package/dist/schema/override-system.d.ts.map +0 -1
  95. package/dist/schema/override-system.js +0 -205
  96. package/dist/schema/override-system.js.map +0 -1
@@ -162,7 +162,8 @@ export declare const RETIRED_SYSTEM_TABLES: readonly string[];
162
162
  * Materialize the portable system-table schema for a target engine.
163
163
  *
164
164
  * PostgreSQL must use TIMESTAMPTZ because every system Date is an instant.
165
- * Other engines keep the established portable TIMESTAMP declarations.
165
+ * PostgreSQL and DuckDB also materialize integer storage as BIGINT, matching
166
+ * application-table DDL; SQLite keeps its variable-width INTEGER.
166
167
  */
167
168
  export declare function getSystemTableDDL(engine: DatabaseEngine): string[];
168
169
  /** Materialize one portable system-table statement for a target engine. */
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/system/schema.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D;;;GAGG;AACH,eAAO,MAAM,0BAA0B,qpCAoCtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,4MAQxC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mCAAmC,o0BA4B/C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,u0BA2BxC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,igDAiDtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wCAAwC,69CAkCpD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,ilCAkCtC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,yBAAyB,weAkBrC,CAAC;AAEF,wEAAwE;AACxE,eAAO,MAAM,kCAAkC,QACqB,CAAC;AAErE,0EAA0E;AAC1E,eAAO,MAAM,yCAAyC,wBAAwB,CAAC;AAE/E,iFAAiF;AACjF,eAAO,MAAM,oDAAoD,yEAAkG,CAAC;AAEpK,8EAA8E;AAC9E,eAAO,MAAM,6CAA6C,sEAA+F,CAAC;AAE1J;;;;;;;;;GASG;AACH,eAAO,MAAM,2CAA2C,ylCAiDvD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,4CAA4C,QAaxD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,2CAA2C,QAevD,CAAC;AAaF;;;;;;GAMG;AACH,eAAO,MAAM,kCAAkC,QAgB9C,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,gLAOvC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,iBAAiB,UAU7B,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,4BAA4B,EAAE,SAAS,MAAM,EAMzD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,6BAA6B,EAAE,SAAS,MAAM,EAG1D,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,qBAAqB,EAAE,SAAS,MAAM,EAGlD,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,EAAE,CAIlE;AAED,2EAA2E;AAC3E,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,cAAc,GACrB,MAAM,CAIR;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,mBAAmB,WAAW,CAAC;AAE5C;;;;;GAKG;AACH,wBAAgB,4BAA4B,IAAI,MAAM,CAErD;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,yBAAyB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAInE,CAAC"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/system/schema.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D;;;GAGG;AACH,eAAO,MAAM,0BAA0B,qpCAoCtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,4MAQxC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mCAAmC,o0BA4B/C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,u0BA2BxC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,igDAiDtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wCAAwC,69CAkCpD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,ilCAkCtC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,yBAAyB,weAkBrC,CAAC;AAEF,wEAAwE;AACxE,eAAO,MAAM,kCAAkC,QACqB,CAAC;AAErE,0EAA0E;AAC1E,eAAO,MAAM,yCAAyC,wBAAwB,CAAC;AAE/E,iFAAiF;AACjF,eAAO,MAAM,oDAAoD,yEAAkG,CAAC;AAEpK,8EAA8E;AAC9E,eAAO,MAAM,6CAA6C,sEAA+F,CAAC;AAE1J;;;;;;;;;GASG;AACH,eAAO,MAAM,2CAA2C,ylCAiDvD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,4CAA4C,QAaxD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,2CAA2C,QAevD,CAAC;AAaF;;;;;;GAMG;AACH,eAAO,MAAM,kCAAkC,QAgB9C,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,gLAOvC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,iBAAiB,UAU7B,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,4BAA4B,EAAE,SAAS,MAAM,EAMzD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,6BAA6B,EAAE,SAAS,MAAM,EAG1D,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,qBAAqB,EAAE,SAAS,MAAM,EAGlD,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,EAAE,CAIlE;AAED,2EAA2E;AAC3E,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,cAAc,GACrB,MAAM,CAQR;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,mBAAmB,WAAW,CAAC;AAE5C;;;;;GAKG;AACH,wBAAgB,4BAA4B,IAAI,MAAM,CAErD;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,yBAAyB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAInE,CAAC"}
@@ -503,14 +503,16 @@ var RETIRED_SYSTEM_TABLES = ["_smrt_registry", "_smrt_signals"];
503
503
  * Materialize the portable system-table schema for a target engine.
504
504
  *
505
505
  * PostgreSQL must use TIMESTAMPTZ because every system Date is an instant.
506
- * Other engines keep the established portable TIMESTAMP declarations.
506
+ * PostgreSQL and DuckDB also materialize integer storage as BIGINT, matching
507
+ * application-table DDL; SQLite keeps its variable-width INTEGER.
507
508
  */
508
509
  function getSystemTableDDL(engine) {
509
510
  return ALL_SYSTEM_TABLES.map((ddl) => getSystemTableDDLForEngine(ddl, engine));
510
511
  }
511
512
  /** Materialize one portable system-table statement for a target engine. */
512
513
  function getSystemTableDDLForEngine(ddl, engine) {
513
- return engine === "postgres" ? ddl.replace(/\bTIMESTAMP\b/g, "TIMESTAMPTZ") : ddl;
514
+ const integerDdl = engine === "postgres" || engine === "duckdb" || engine === "json" ? ddl.replace(/\bINTEGER\b/g, "BIGINT") : ddl;
515
+ return engine === "postgres" ? integerDdl.replace(/\bTIMESTAMP\b/g, "TIMESTAMPTZ") : integerDdl;
514
516
  }
515
517
  /**
516
518
  * Current SMRT system schema version.
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","names":[],"sources":["../../src/system/schema.ts"],"sourcesContent":["/**\n * SMRT System Tables Schema\n *\n * System tables use _smrt_ prefix to avoid conflicts with user tables.\n * All system tables are created in the same database as user data.\n */\n\nimport type { DatabaseEngine } from '../schema/ddl/types.js';\n\n/**\n * Context memory storage\n * Stores remembered context (learned strategies, patterns, selectors) for reuse\n */\nexport const CREATE_SMRT_CONTEXTS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_contexts (\n id TEXT PRIMARY KEY,\n owner_class TEXT NOT NULL,\n owner_id TEXT NOT NULL,\n scope TEXT NOT NULL,\n key TEXT NOT NULL,\n value TEXT,\n metadata TEXT,\n version INTEGER DEFAULT 1,\n confidence REAL DEFAULT 1.0,\n success_count INTEGER DEFAULT 0,\n failure_count INTEGER DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n last_used_at TIMESTAMP,\n expires_at TIMESTAMP,\n UNIQUE(owner_class, owner_id, scope, key, version)\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_owner\n ON _smrt_contexts(owner_class, owner_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_scope\n ON _smrt_contexts(scope);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_confidence\n ON _smrt_contexts(confidence);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_last_used\n ON _smrt_contexts(last_used_at);\n\n-- Retention predicate for pruneExpiredContexts() (#2375). Rows that never\n-- expire carry NULL here, so the index only has to cover the ones that do.\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_expires_at\n ON _smrt_contexts(expires_at);\n`;\n\n/**\n * Schema version tracking\n * Records which SMRT framework versions have been applied\n */\nexport const CREATE_SMRT_MIGRATIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_migrations (\n id TEXT PRIMARY KEY,\n version TEXT NOT NULL UNIQUE,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n description TEXT,\n checksum TEXT\n);\n`;\n\n/**\n * Schema migration tracking\n * Tracks applied schema migrations for idempotency, audit, and rollback\n */\nexport const CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_schema_migrations (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n version TEXT NOT NULL,\n checksum TEXT NOT NULL,\n applied_checksum TEXT,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n execution_time_ms INTEGER,\n package_name TEXT,\n source_file TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n error_message TEXT,\n attempts INTEGER DEFAULT 0,\n is_reversible INTEGER DEFAULT 1,\n rolled_back_at TIMESTAMP,\n applied_by TEXT,\n batch INTEGER\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_status\n ON _smrt_schema_migrations(status);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_applied_at\n ON _smrt_schema_migrations(applied_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_batch\n ON _smrt_schema_migrations(batch);\n`;\n\n/**\n * Embedding storage for semantic search\n * Stores embedding vectors for SMRT objects to enable vector similarity search\n */\nexport const CREATE_SMRT_EMBEDDINGS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_embeddings (\n id TEXT PRIMARY KEY,\n object_class TEXT NOT NULL,\n object_id TEXT NOT NULL,\n field_name TEXT NOT NULL,\n content_hash TEXT NOT NULL,\n embedding TEXT NOT NULL,\n model TEXT NOT NULL,\n dimensions INTEGER NOT NULL,\n provider TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n UNIQUE(object_class, object_id, field_name, model)\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_object\n ON _smrt_embeddings(object_class, object_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_class\n ON _smrt_embeddings(object_class);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_hash\n ON _smrt_embeddings(content_hash);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_model\n ON _smrt_embeddings(model);\n`;\n\n/**\n * Dispatch queue for inter-agent communication\n * Stores dispatch messages for asynchronous agent-to-agent signaling\n */\nexport const CREATE_SMRT_DISPATCH_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_dispatch (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n source TEXT NOT NULL,\n source_id TEXT,\n payload TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n attempts INTEGER DEFAULT 0,\n last_error TEXT,\n processed_at TIMESTAMP,\n processed_by TEXT,\n target_subscriber TEXT,\n correlation_id TEXT,\n tenant_id TEXT,\n metadata TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_status\n ON _smrt_dispatch(status);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_tenant_id\n ON _smrt_dispatch(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_type\n ON _smrt_dispatch(type);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_source\n ON _smrt_dispatch(source);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_created\n ON _smrt_dispatch(created_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_target\n ON _smrt_dispatch(target_subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_correlation\n ON _smrt_dispatch(correlation_id);\n\n-- Retention predicates of DispatchCollection.cleanup() (#2375): completed rows\n-- are aged out on processed_at, failed rows on updated_at. The status-only\n-- index above cannot serve either range.\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_status_processed\n ON _smrt_dispatch(status, processed_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_status_updated\n ON _smrt_dispatch(status, updated_at);\n`;\n\n/**\n * Dispatch subscriptions for persistent handlers\n * Stores subscriptions to dispatch types for agent processing\n */\nexport const CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_dispatch_subscriptions (\n id TEXT PRIMARY KEY,\n signal_type TEXT NOT NULL,\n subscriber TEXT NOT NULL,\n handler TEXT NOT NULL DEFAULT 'handleDispatch',\n delivery TEXT NOT NULL DEFAULT 'compete',\n enabled INTEGER DEFAULT 1,\n tenant_id TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\n-- Subscription identity is tenant-scoped (S5 #1398): the same\n-- (signal_type, subscriber) pair may exist independently in different tenants,\n-- so tenant B can no longer overwrite/delete/enable tenant A's subscription.\n-- A named UNIQUE index (rather than an inline UNIQUE constraint) is used so the\n-- compatibility migration can additively reshape existing tables. NULL tenant_id\n-- (global subscriptions) is deduped at the application layer by the NULL-aware\n-- upsert in @happyvertical/sql.\nCREATE UNIQUE INDEX IF NOT EXISTS uq_smrt_dispatch_subs_tenant_signal_subscriber\n ON _smrt_dispatch_subscriptions(tenant_id, signal_type, subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_subscriber\n ON _smrt_dispatch_subscriptions(subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_tenant_id\n ON _smrt_dispatch_subscriptions(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_signal_type\n ON _smrt_dispatch_subscriptions(signal_type);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_enabled\n ON _smrt_dispatch_subscriptions(enabled);\n`;\n\n/**\n * AI usage telemetry storage\n * Stores normalized AI usage records for reporting and billing hooks\n */\nexport const CREATE_SMRT_AI_USAGE_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_ai_usage (\n id TEXT PRIMARY KEY,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n operation TEXT NOT NULL,\n prompt_tokens INTEGER,\n completion_tokens INTEGER,\n total_tokens INTEGER,\n estimated_cost REAL,\n duration INTEGER NOT NULL,\n class_name TEXT,\n tenant_id TEXT,\n tags TEXT,\n created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_created\n ON _smrt_ai_usage(created_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_class\n ON _smrt_ai_usage(class_name);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_tenant\n ON _smrt_ai_usage(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_provider_model\n ON _smrt_ai_usage(provider, model);\n\n-- Serves both the tenant-scoped retention predicate of pruneAiUsage() and the\n-- subscriptions billing meter's tenant_id + created_at range scan, neither of\n-- which the single-column indexes above can satisfy (#2375).\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_tenant_created\n ON _smrt_ai_usage(tenant_id, created_at);\n`;\n\n/**\n * Append-only change feed (issue #1758)\n *\n * One row per framework save/delete: monotonic per-database sequence,\n * table, row id, operation (create/update/delete — deletes are tombstones),\n * tenant, timestamp. Written by the change-feed interceptor and read\n * through `getChangesSince()`.\n *\n * `seq` is deliberately a plain BIGINT PRIMARY KEY rather than a native\n * AUTOINCREMENT/identity/serial column: the appender allocates\n * `COALESCE(MAX(seq), 0) + 1` inside the INSERT (with a conflict retry),\n * which keeps committed sequences contiguous so commit order equals\n * sequence order on every engine. Native identity columns allocate before\n * commit, so under concurrent writers on MVCC engines a reader could\n * observe seq N+1 while seq N is still uncommitted and advance its cursor\n * past it — breaking the feed's no-missed-changes cursor guarantee. The\n * plain column also keeps this DDL portable across SQLite, Postgres and\n * DuckDB with no per-engine branching. See `change-feed.ts`.\n *\n * `row_id` is nullable: manual bumps (`bumpChangeFeed`) may record a\n * table-level change without a specific row.\n */\nexport const CREATE_SMRT_CHANGES_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_changes (\n seq BIGINT PRIMARY KEY,\n table_name TEXT NOT NULL,\n row_id TEXT,\n operation TEXT NOT NULL,\n tenant_id TEXT,\n created_at TIMESTAMP NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_table_seq\n ON _smrt_changes(table_name, seq);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_tenant_seq\n ON _smrt_changes(tenant_id, seq);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_created_at\n ON _smrt_changes(created_at);\n`;\n\n/** PostgreSQL materialization of the portable change-feed table DDL. */\nexport const CREATE_POSTGRES_SMRT_CHANGES_TABLE =\n CREATE_SMRT_CHANGES_TABLE.replace(/\\bTIMESTAMP\\b/g, 'TIMESTAMPTZ');\n\n/** PostgreSQL helper used to isolate best-effort feed appends (#2026). */\nexport const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME = '_smrt_append_change';\n\n/** Legacy helper identity used before PostgreSQL Date values became instants. */\nexport const LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(text,text,text,text,timestamp without time zone)`;\n\n/** Exact PostgreSQL identity used for catalog lookup of the append helper. */\nexport const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(text,text,text,text,timestamp with time zone)`;\n\n/**\n * PostgreSQL-only change-feed append function.\n *\n * A PL/pgSQL block with an EXCEPTION handler runs its body in an internal\n * subtransaction. Returning SQLSTATE as data lets the caller log or retry a\n * failed best-effort append without leaving its surrounding transaction in\n * PostgreSQL's aborted (25P02) state. This statement contains dollar-quoted\n * semicolons, so callers must execute it whole rather than adding it to\n * {@link ALL_SYSTEM_TABLES}, whose portable DDL entries are semicolon-split.\n */\nexport const CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nCREATE OR REPLACE FUNCTION ${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(\n p_table_name TEXT,\n p_row_id TEXT,\n p_operation TEXT,\n p_tenant_id TEXT,\n p_created_at TIMESTAMPTZ\n)\nRETURNS TABLE(\n allocated_seq BIGINT,\n error_code TEXT,\n error_message TEXT\n)\nLANGUAGE plpgsql\nSECURITY INVOKER\nAS $smrt_change_feed$\nDECLARE\n v_seq BIGINT;\n v_error_code TEXT;\n v_error_message TEXT;\nBEGIN\n BEGIN\n INSERT INTO _smrt_changes (\n seq,\n table_name,\n row_id,\n operation,\n tenant_id,\n created_at\n )\n SELECT\n COALESCE(MAX(changes.seq), 0) + 1,\n p_table_name,\n p_row_id,\n p_operation,\n p_tenant_id,\n p_created_at\n FROM _smrt_changes AS changes\n RETURNING _smrt_changes.seq INTO v_seq;\n\n RETURN QUERY SELECT v_seq, NULL::TEXT, NULL::TEXT;\n EXCEPTION WHEN query_canceled OR assert_failure OR OTHERS THEN\n GET STACKED DIAGNOSTICS\n v_error_code = RETURNED_SQLSTATE,\n v_error_message = MESSAGE_TEXT;\n RETURN QUERY SELECT NULL::BIGINT, v_error_code, v_error_message;\n END;\nEND;\n$smrt_change_feed$;\n`;\n\n/**\n * Serialize PostgreSQL helper replacement inside one server-side statement.\n *\n * The transaction-scoped advisory lock prevents concurrent bootstraps from\n * racing on PostgreSQL's `pg_proc` uniqueness constraint. The nested dollar\n * quote keeps the complete function DDL atomic from the client's perspective.\n */\nexport const REPLACE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nDO $smrt_replace_change_feed$\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\nEND;\n$smrt_replace_change_feed$;\n`;\n\n/**\n * Install the PostgreSQL helper only when missing, serialized server-side.\n *\n * A client-side catalog probe remains the fast path for already-initialized\n * read handles. This guarded statement is the cold-path race boundary: both\n * the advisory lock and the post-lock catalog check run before function DDL.\n */\nexport const ENSURE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nDO $smrt_ensure_change_feed$\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n IF to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}') IS NULL THEN\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\n END IF;\nEND;\n$smrt_ensure_change_feed$;\n`;\n\nconst POSTGRES_CHANGE_FEED_SCHEMA_DDL =\n CREATE_POSTGRES_SMRT_CHANGES_TABLE.split(';')\n .map((statement) => statement.trim())\n .filter((statement) => statement.length > 0)\n .map(\n (statement) => `EXECUTE $smrt_change_feed_schema_ddl$\n${statement}\n$smrt_change_feed_schema_ddl$;`,\n )\n .join('\\n');\n\n/**\n * Install the complete PostgreSQL change-feed schema under the bootstrap lock.\n *\n * The lock is deliberately acquired before table or index DDL. Framework\n * bootstrap uses the same ordering, so a raw-handle cold start cannot retain\n * catalog locks while waiting behind a framework bootstrap transaction.\n */\nexport const ENSURE_POSTGRES_CHANGE_FEED_SCHEMA = `\nDO $smrt_ensure_change_feed_schema$\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n${POSTGRES_CHANGE_FEED_SCHEMA_DDL}\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n IF to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}') IS NULL THEN\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\n END IF;\nEND;\n$smrt_ensure_change_feed_schema$;\n`;\n\n/**\n * Data backfill tracking\n *\n * Distinct from `_smrt_schema_migrations` — backfills are app-specific\n * data corrections (slug rewrites, model splits, lookup-table seeds) that\n * don't have schema diffs or rollback semantics. Apps register backfills\n * by name and the tracker handles idempotency.\n */\nexport const CREATE_SMRT_BACKFILLS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_backfills (\n name TEXT PRIMARY KEY,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n description TEXT,\n package_name TEXT\n);\n`;\n\n/**\n * All system table creation statements\n */\nexport const ALL_SYSTEM_TABLES = [\n CREATE_SMRT_CONTEXTS_TABLE,\n CREATE_SMRT_MIGRATIONS_TABLE,\n CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE,\n CREATE_SMRT_BACKFILLS_TABLE,\n CREATE_SMRT_EMBEDDINGS_TABLE,\n CREATE_SMRT_DISPATCH_TABLE,\n CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE,\n CREATE_SMRT_AI_USAGE_TABLE,\n CREATE_SMRT_CHANGES_TABLE,\n];\n\n/**\n * Framework-owned tables that are backed by `@smrt()` models rather than by\n * {@link ALL_SYSTEM_TABLES}, and whose write volume disqualifies them from the\n * change feed.\n *\n * This is the jobs runner's own state: the claim loop and the forge projection\n * poll every second and rewrite these rows several times per job, so feeding\n * them into an append-only log whose pruning is not scheduled (issue #2375)\n * would dominate it with data no client syncs.\n *\n * The list is deliberately short and biased toward *observing*. A table wrongly\n * excluded is an invisible bug — exactly the one issue #2376 fixes — while a\n * table wrongly included costs a few extra feed rows. Everything else keeping\n * the `_smrt_` prefix (feature flags, prompt overrides, subscription plans,\n * field policies, agent and report schedules, report state) is observed.\n */\nexport const FRAMEWORK_OPERATIONAL_TABLES: readonly string[] = [\n '_smrt_jobs',\n '_smrt_job_events',\n '_smrt_workers',\n '_smrt_forge_deliveries',\n '_smrt_forge_projection_checkpoints',\n];\n\n/**\n * Framework tables that are backed by `@smrt()` models *and* reshaped by\n * `system/compatibility.ts`.\n *\n * They are dual-owned: `db:migrate` creates them from the jobs manifest, and\n * the compatibility pass adds columns/indexes that predate the manifest. On a\n * fresh install the manifest wins the race — bootstrap runs before the tables\n * exist — so their compatibility pass is deferred and re-checked until the\n * tables show up (issue #2376).\n */\nexport const DEFERRED_COMPATIBILITY_TABLES: readonly string[] = [\n '_smrt_jobs',\n '_smrt_job_events',\n];\n\n/**\n * System tables SMRT used to create on every database and no longer does.\n *\n * `_smrt_signals` never had a writer and `_smrt_registry` was only written by\n * an `ObjectRegistry.persistToDatabase()` helper that nothing ever called, so\n * both were empty on every deployment. New databases no longer get them.\n * Existing databases keep the empty tables until an operator drops them:\n *\n * ```sql\n * DROP TABLE IF EXISTS _smrt_signals;\n * DROP TABLE IF EXISTS _smrt_registry;\n * ```\n *\n * The framework deliberately does not issue those drops itself — dropping a\n * table is not reversible and a deployment may have repurposed the name.\n */\nexport const RETIRED_SYSTEM_TABLES: readonly string[] = [\n '_smrt_registry',\n '_smrt_signals',\n];\n\n/**\n * Materialize the portable system-table schema for a target engine.\n *\n * PostgreSQL must use TIMESTAMPTZ because every system Date is an instant.\n * Other engines keep the established portable TIMESTAMP declarations.\n */\nexport function getSystemTableDDL(engine: DatabaseEngine): string[] {\n return ALL_SYSTEM_TABLES.map((ddl) =>\n getSystemTableDDLForEngine(ddl, engine),\n );\n}\n\n/** Materialize one portable system-table statement for a target engine. */\nexport function getSystemTableDDLForEngine(\n ddl: string,\n engine: DatabaseEngine,\n): string {\n return engine === 'postgres'\n ? ddl.replace(/\\bTIMESTAMP\\b/g, 'TIMESTAMPTZ')\n : ddl;\n}\n\n/**\n * Current SMRT system schema version.\n *\n * `bootstrapSystemTables()` skips replaying system DDL once this exact version\n * is recorded in `_smrt_migrations`, so every change to {@link ALL_SYSTEM_TABLES}\n * must bump it — otherwise existing databases never see the change. New\n * *columns* additionally need an `addColumnIfMissing()` entry in\n * `system/compatibility.ts`, because `CREATE TABLE IF NOT EXISTS` is a no-op on\n * a table that already exists.\n *\n * 1.10.1 retires the never-written `_smrt_registry` / `_smrt_signals` tables\n * (see {@link RETIRED_SYSTEM_TABLES}) and adds the retention-predicate indexes\n * from #2375 (`_smrt_contexts.expires_at`, `_smrt_ai_usage(tenant_id,\n * created_at)`, `_smrt_dispatch(status, processed_at)` and\n * `(status, updated_at)`) in the same replay. (`1.10.0` briefly existed only\n * on two unreleased, unmerged branches with two different, incompatible DDL\n * sets — never published, never stamped in any real `_smrt_migrations` table\n * — so merging them reuses no version an existing install could have already\n * recorded; see the note on {@link SMRT_SCHEMA_DDL_CHECKSUMS}.)\n */\nexport const SMRT_SCHEMA_VERSION = '1.10.1';\n\n/**\n * Canonical form of the system DDL that {@link SMRT_SCHEMA_DDL_CHECKSUMS} covers.\n *\n * Whitespace is normalized so reformatting alone does not trip the guard while\n * any token change does.\n */\nexport function getSystemSchemaChecksumInput(): string {\n return ALL_SYSTEM_TABLES.join('\\n').replace(/\\s+/g, ' ').trim();\n}\n\n/**\n * SHA-256 of {@link getSystemSchemaChecksumInput} for each system schema\n * version, oldest first.\n *\n * The discipline that keeps fresh installs and upgraded installs identical was\n * documented but unenforced (issue #2376): a contributor could edit the DDL and\n * ship it without bumping the version, and no existing database would ever\n * apply the change. `system-schema-evolution.test.ts` recomputes the current\n * hash and requires it to equal this map's entry for\n * {@link SMRT_SCHEMA_VERSION}.\n *\n * It is a *history*, not a single constant, so the guard cannot be silenced by\n * overwriting one value: every recorded hash must stay distinct, which means a\n * changed DDL has nowhere to be recorded except under a new version key. Adding\n * that key is the bump. This is also why merging two branches that each\n * independently bumped to the same version string (both `1.10.0`, covering two\n * different DDL sets — #2376's table retirement and #2375's retention indexes)\n * must land under a *new* key rather than picking a \"final\" checksum for the\n * shared number: nothing published or otherwise reachable ever stamped\n * `1.10.0`, so recording the merged DDL as `1.10.1` costs nothing and removes\n * any doubt about a version string meaning two different things to two\n * installs.\n *\n * Entries before 1.10.1 are not recorded — the guard starts here.\n */\nexport const SMRT_SCHEMA_DDL_CHECKSUMS: Readonly<Record<string, string>> =\n Object.freeze({\n '1.10.1':\n 'f796ee3b3f7ab8b9dc659ecaa68884ec01277408c3dd6edea540853fce369c16',\n });\n"],"mappings":";;;;;AAaA,IAAa,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0C1C,IAAa,+BAA+B;;;;;;;;;;;;;AAc5C,IAAa,sCAAsC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCnD,IAAa,+BAA+B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiC5C,IAAa,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuD1C,IAAa,2CAA2C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCxD,IAAa,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0D1C,IAAa,4BAA4B;;;;;;;;;;;;;;;;;;;;AAqBzC,IAAa,qCACX,0BAA0B,QAAQ,kBAAkB,aAAa;;AAGnE,IAAa,4CAA4C;;AAGzD,IAAa,uDAAuD,GAAG,0CAA0C;;AAGjH,IAAa,gDAAgD,GAAG,0CAA0C;;;;;;;;;;;AAY1G,IAAa,8CAA8C;6BAC9B,0CAA0C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDvE,IAAa,+CAA+C;;;;;;;4BAOhC,qDAAqD;;EAE/E,4CAA4C,KAAK,EAAE;;;;;;;;;;;;AAarD,IAAa,8CAA8C;;;;;;;4BAO/B,qDAAqD;wBACzD,8CAA8C;;EAEpE,4CAA4C,KAAK,EAAE;;;;;;;;;;;;;AAyBrD,IAAa,qCAAqC;;;;;;;EAjBhD,mCAAmC,MAAM,GAAG,CAAC,CAC1C,KAAK,cAAc,UAAU,KAAK,CAAC,CAAC,CACpC,QAAQ,cAAc,UAAU,SAAS,CAAC,CAAC,CAC3C,KACE,cAAc;EACnB,UAAU;+BAER,CAAC,CACA,KAAK,IAgBR,EAAgC;4BACN,qDAAqD;wBACzD,8CAA8C;;EAEpE,4CAA4C,KAAK,EAAE;;;;;;;;;;;;;;AAerD,IAAa,8BAA8B;;;;;;;;;;;AAY3C,IAAa,oBAAoB;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;;;;;;;;;;;AAkBA,IAAa,+BAAkD;CAC7D;CACA;CACA;CACA;CACA;AACF;;;;;;;;;;;;;;;;;AAiCA,IAAa,wBAA2C,CACtD,kBACA,eACF;;;;;;;AAQA,SAAgB,kBAAkB,QAAkC;CAClE,OAAO,kBAAkB,KAAK,QAC5B,2BAA2B,KAAK,MAAM,CACxC;AACF;;AAGA,SAAgB,2BACd,KACA,QACQ;CACR,OAAO,WAAW,aACd,IAAI,QAAQ,kBAAkB,aAAa,IAC3C;AACN;;;;;;;;;;;;;;;;;;;;;AAsBA,IAAa,sBAAsB;AAsCjC,OAAO,OAAO,EACZ,UACE,mEACJ,CAAC"}
1
+ {"version":3,"file":"schema.js","names":[],"sources":["../../src/system/schema.ts"],"sourcesContent":["/**\n * SMRT System Tables Schema\n *\n * System tables use _smrt_ prefix to avoid conflicts with user tables.\n * All system tables are created in the same database as user data.\n */\n\nimport type { DatabaseEngine } from '../schema/ddl/types.js';\n\n/**\n * Context memory storage\n * Stores remembered context (learned strategies, patterns, selectors) for reuse\n */\nexport const CREATE_SMRT_CONTEXTS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_contexts (\n id TEXT PRIMARY KEY,\n owner_class TEXT NOT NULL,\n owner_id TEXT NOT NULL,\n scope TEXT NOT NULL,\n key TEXT NOT NULL,\n value TEXT,\n metadata TEXT,\n version INTEGER DEFAULT 1,\n confidence REAL DEFAULT 1.0,\n success_count INTEGER DEFAULT 0,\n failure_count INTEGER DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n last_used_at TIMESTAMP,\n expires_at TIMESTAMP,\n UNIQUE(owner_class, owner_id, scope, key, version)\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_owner\n ON _smrt_contexts(owner_class, owner_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_scope\n ON _smrt_contexts(scope);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_confidence\n ON _smrt_contexts(confidence);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_last_used\n ON _smrt_contexts(last_used_at);\n\n-- Retention predicate for pruneExpiredContexts() (#2375). Rows that never\n-- expire carry NULL here, so the index only has to cover the ones that do.\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_expires_at\n ON _smrt_contexts(expires_at);\n`;\n\n/**\n * Schema version tracking\n * Records which SMRT framework versions have been applied\n */\nexport const CREATE_SMRT_MIGRATIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_migrations (\n id TEXT PRIMARY KEY,\n version TEXT NOT NULL UNIQUE,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n description TEXT,\n checksum TEXT\n);\n`;\n\n/**\n * Schema migration tracking\n * Tracks applied schema migrations for idempotency, audit, and rollback\n */\nexport const CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_schema_migrations (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n version TEXT NOT NULL,\n checksum TEXT NOT NULL,\n applied_checksum TEXT,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n execution_time_ms INTEGER,\n package_name TEXT,\n source_file TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n error_message TEXT,\n attempts INTEGER DEFAULT 0,\n is_reversible INTEGER DEFAULT 1,\n rolled_back_at TIMESTAMP,\n applied_by TEXT,\n batch INTEGER\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_status\n ON _smrt_schema_migrations(status);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_applied_at\n ON _smrt_schema_migrations(applied_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_batch\n ON _smrt_schema_migrations(batch);\n`;\n\n/**\n * Embedding storage for semantic search\n * Stores embedding vectors for SMRT objects to enable vector similarity search\n */\nexport const CREATE_SMRT_EMBEDDINGS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_embeddings (\n id TEXT PRIMARY KEY,\n object_class TEXT NOT NULL,\n object_id TEXT NOT NULL,\n field_name TEXT NOT NULL,\n content_hash TEXT NOT NULL,\n embedding TEXT NOT NULL,\n model TEXT NOT NULL,\n dimensions INTEGER NOT NULL,\n provider TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n UNIQUE(object_class, object_id, field_name, model)\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_object\n ON _smrt_embeddings(object_class, object_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_class\n ON _smrt_embeddings(object_class);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_hash\n ON _smrt_embeddings(content_hash);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_model\n ON _smrt_embeddings(model);\n`;\n\n/**\n * Dispatch queue for inter-agent communication\n * Stores dispatch messages for asynchronous agent-to-agent signaling\n */\nexport const CREATE_SMRT_DISPATCH_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_dispatch (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n source TEXT NOT NULL,\n source_id TEXT,\n payload TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n attempts INTEGER DEFAULT 0,\n last_error TEXT,\n processed_at TIMESTAMP,\n processed_by TEXT,\n target_subscriber TEXT,\n correlation_id TEXT,\n tenant_id TEXT,\n metadata TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_status\n ON _smrt_dispatch(status);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_tenant_id\n ON _smrt_dispatch(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_type\n ON _smrt_dispatch(type);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_source\n ON _smrt_dispatch(source);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_created\n ON _smrt_dispatch(created_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_target\n ON _smrt_dispatch(target_subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_correlation\n ON _smrt_dispatch(correlation_id);\n\n-- Retention predicates of DispatchCollection.cleanup() (#2375): completed rows\n-- are aged out on processed_at, failed rows on updated_at. The status-only\n-- index above cannot serve either range.\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_status_processed\n ON _smrt_dispatch(status, processed_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_status_updated\n ON _smrt_dispatch(status, updated_at);\n`;\n\n/**\n * Dispatch subscriptions for persistent handlers\n * Stores subscriptions to dispatch types for agent processing\n */\nexport const CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_dispatch_subscriptions (\n id TEXT PRIMARY KEY,\n signal_type TEXT NOT NULL,\n subscriber TEXT NOT NULL,\n handler TEXT NOT NULL DEFAULT 'handleDispatch',\n delivery TEXT NOT NULL DEFAULT 'compete',\n enabled INTEGER DEFAULT 1,\n tenant_id TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\n-- Subscription identity is tenant-scoped (S5 #1398): the same\n-- (signal_type, subscriber) pair may exist independently in different tenants,\n-- so tenant B can no longer overwrite/delete/enable tenant A's subscription.\n-- A named UNIQUE index (rather than an inline UNIQUE constraint) is used so the\n-- compatibility migration can additively reshape existing tables. NULL tenant_id\n-- (global subscriptions) is deduped at the application layer by the NULL-aware\n-- upsert in @happyvertical/sql.\nCREATE UNIQUE INDEX IF NOT EXISTS uq_smrt_dispatch_subs_tenant_signal_subscriber\n ON _smrt_dispatch_subscriptions(tenant_id, signal_type, subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_subscriber\n ON _smrt_dispatch_subscriptions(subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_tenant_id\n ON _smrt_dispatch_subscriptions(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_signal_type\n ON _smrt_dispatch_subscriptions(signal_type);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_enabled\n ON _smrt_dispatch_subscriptions(enabled);\n`;\n\n/**\n * AI usage telemetry storage\n * Stores normalized AI usage records for reporting and billing hooks\n */\nexport const CREATE_SMRT_AI_USAGE_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_ai_usage (\n id TEXT PRIMARY KEY,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n operation TEXT NOT NULL,\n prompt_tokens INTEGER,\n completion_tokens INTEGER,\n total_tokens INTEGER,\n estimated_cost REAL,\n duration INTEGER NOT NULL,\n class_name TEXT,\n tenant_id TEXT,\n tags TEXT,\n created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_created\n ON _smrt_ai_usage(created_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_class\n ON _smrt_ai_usage(class_name);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_tenant\n ON _smrt_ai_usage(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_provider_model\n ON _smrt_ai_usage(provider, model);\n\n-- Serves both the tenant-scoped retention predicate of pruneAiUsage() and the\n-- subscriptions billing meter's tenant_id + created_at range scan, neither of\n-- which the single-column indexes above can satisfy (#2375).\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_tenant_created\n ON _smrt_ai_usage(tenant_id, created_at);\n`;\n\n/**\n * Append-only change feed (issue #1758)\n *\n * One row per framework save/delete: monotonic per-database sequence,\n * table, row id, operation (create/update/delete — deletes are tombstones),\n * tenant, timestamp. Written by the change-feed interceptor and read\n * through `getChangesSince()`.\n *\n * `seq` is deliberately a plain BIGINT PRIMARY KEY rather than a native\n * AUTOINCREMENT/identity/serial column: the appender allocates\n * `COALESCE(MAX(seq), 0) + 1` inside the INSERT (with a conflict retry),\n * which keeps committed sequences contiguous so commit order equals\n * sequence order on every engine. Native identity columns allocate before\n * commit, so under concurrent writers on MVCC engines a reader could\n * observe seq N+1 while seq N is still uncommitted and advance its cursor\n * past it — breaking the feed's no-missed-changes cursor guarantee. The\n * plain column also keeps this DDL portable across SQLite, Postgres and\n * DuckDB with no per-engine branching. See `change-feed.ts`.\n *\n * `row_id` is nullable: manual bumps (`bumpChangeFeed`) may record a\n * table-level change without a specific row.\n */\nexport const CREATE_SMRT_CHANGES_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_changes (\n seq BIGINT PRIMARY KEY,\n table_name TEXT NOT NULL,\n row_id TEXT,\n operation TEXT NOT NULL,\n tenant_id TEXT,\n created_at TIMESTAMP NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_table_seq\n ON _smrt_changes(table_name, seq);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_tenant_seq\n ON _smrt_changes(tenant_id, seq);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_created_at\n ON _smrt_changes(created_at);\n`;\n\n/** PostgreSQL materialization of the portable change-feed table DDL. */\nexport const CREATE_POSTGRES_SMRT_CHANGES_TABLE =\n CREATE_SMRT_CHANGES_TABLE.replace(/\\bTIMESTAMP\\b/g, 'TIMESTAMPTZ');\n\n/** PostgreSQL helper used to isolate best-effort feed appends (#2026). */\nexport const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME = '_smrt_append_change';\n\n/** Legacy helper identity used before PostgreSQL Date values became instants. */\nexport const LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(text,text,text,text,timestamp without time zone)`;\n\n/** Exact PostgreSQL identity used for catalog lookup of the append helper. */\nexport const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(text,text,text,text,timestamp with time zone)`;\n\n/**\n * PostgreSQL-only change-feed append function.\n *\n * A PL/pgSQL block with an EXCEPTION handler runs its body in an internal\n * subtransaction. Returning SQLSTATE as data lets the caller log or retry a\n * failed best-effort append without leaving its surrounding transaction in\n * PostgreSQL's aborted (25P02) state. This statement contains dollar-quoted\n * semicolons, so callers must execute it whole rather than adding it to\n * {@link ALL_SYSTEM_TABLES}, whose portable DDL entries are semicolon-split.\n */\nexport const CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nCREATE OR REPLACE FUNCTION ${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(\n p_table_name TEXT,\n p_row_id TEXT,\n p_operation TEXT,\n p_tenant_id TEXT,\n p_created_at TIMESTAMPTZ\n)\nRETURNS TABLE(\n allocated_seq BIGINT,\n error_code TEXT,\n error_message TEXT\n)\nLANGUAGE plpgsql\nSECURITY INVOKER\nAS $smrt_change_feed$\nDECLARE\n v_seq BIGINT;\n v_error_code TEXT;\n v_error_message TEXT;\nBEGIN\n BEGIN\n INSERT INTO _smrt_changes (\n seq,\n table_name,\n row_id,\n operation,\n tenant_id,\n created_at\n )\n SELECT\n COALESCE(MAX(changes.seq), 0) + 1,\n p_table_name,\n p_row_id,\n p_operation,\n p_tenant_id,\n p_created_at\n FROM _smrt_changes AS changes\n RETURNING _smrt_changes.seq INTO v_seq;\n\n RETURN QUERY SELECT v_seq, NULL::TEXT, NULL::TEXT;\n EXCEPTION WHEN query_canceled OR assert_failure OR OTHERS THEN\n GET STACKED DIAGNOSTICS\n v_error_code = RETURNED_SQLSTATE,\n v_error_message = MESSAGE_TEXT;\n RETURN QUERY SELECT NULL::BIGINT, v_error_code, v_error_message;\n END;\nEND;\n$smrt_change_feed$;\n`;\n\n/**\n * Serialize PostgreSQL helper replacement inside one server-side statement.\n *\n * The transaction-scoped advisory lock prevents concurrent bootstraps from\n * racing on PostgreSQL's `pg_proc` uniqueness constraint. The nested dollar\n * quote keeps the complete function DDL atomic from the client's perspective.\n */\nexport const REPLACE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nDO $smrt_replace_change_feed$\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\nEND;\n$smrt_replace_change_feed$;\n`;\n\n/**\n * Install the PostgreSQL helper only when missing, serialized server-side.\n *\n * A client-side catalog probe remains the fast path for already-initialized\n * read handles. This guarded statement is the cold-path race boundary: both\n * the advisory lock and the post-lock catalog check run before function DDL.\n */\nexport const ENSURE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nDO $smrt_ensure_change_feed$\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n IF to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}') IS NULL THEN\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\n END IF;\nEND;\n$smrt_ensure_change_feed$;\n`;\n\nconst POSTGRES_CHANGE_FEED_SCHEMA_DDL =\n CREATE_POSTGRES_SMRT_CHANGES_TABLE.split(';')\n .map((statement) => statement.trim())\n .filter((statement) => statement.length > 0)\n .map(\n (statement) => `EXECUTE $smrt_change_feed_schema_ddl$\n${statement}\n$smrt_change_feed_schema_ddl$;`,\n )\n .join('\\n');\n\n/**\n * Install the complete PostgreSQL change-feed schema under the bootstrap lock.\n *\n * The lock is deliberately acquired before table or index DDL. Framework\n * bootstrap uses the same ordering, so a raw-handle cold start cannot retain\n * catalog locks while waiting behind a framework bootstrap transaction.\n */\nexport const ENSURE_POSTGRES_CHANGE_FEED_SCHEMA = `\nDO $smrt_ensure_change_feed_schema$\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n${POSTGRES_CHANGE_FEED_SCHEMA_DDL}\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n IF to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}') IS NULL THEN\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\n END IF;\nEND;\n$smrt_ensure_change_feed_schema$;\n`;\n\n/**\n * Data backfill tracking\n *\n * Distinct from `_smrt_schema_migrations` — backfills are app-specific\n * data corrections (slug rewrites, model splits, lookup-table seeds) that\n * don't have schema diffs or rollback semantics. Apps register backfills\n * by name and the tracker handles idempotency.\n */\nexport const CREATE_SMRT_BACKFILLS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_backfills (\n name TEXT PRIMARY KEY,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n description TEXT,\n package_name TEXT\n);\n`;\n\n/**\n * All system table creation statements\n */\nexport const ALL_SYSTEM_TABLES = [\n CREATE_SMRT_CONTEXTS_TABLE,\n CREATE_SMRT_MIGRATIONS_TABLE,\n CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE,\n CREATE_SMRT_BACKFILLS_TABLE,\n CREATE_SMRT_EMBEDDINGS_TABLE,\n CREATE_SMRT_DISPATCH_TABLE,\n CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE,\n CREATE_SMRT_AI_USAGE_TABLE,\n CREATE_SMRT_CHANGES_TABLE,\n];\n\n/**\n * Framework-owned tables that are backed by `@smrt()` models rather than by\n * {@link ALL_SYSTEM_TABLES}, and whose write volume disqualifies them from the\n * change feed.\n *\n * This is the jobs runner's own state: the claim loop and the forge projection\n * poll every second and rewrite these rows several times per job, so feeding\n * them into an append-only log whose pruning is not scheduled (issue #2375)\n * would dominate it with data no client syncs.\n *\n * The list is deliberately short and biased toward *observing*. A table wrongly\n * excluded is an invisible bug — exactly the one issue #2376 fixes — while a\n * table wrongly included costs a few extra feed rows. Everything else keeping\n * the `_smrt_` prefix (feature flags, prompt overrides, subscription plans,\n * field policies, agent and report schedules, report state) is observed.\n */\nexport const FRAMEWORK_OPERATIONAL_TABLES: readonly string[] = [\n '_smrt_jobs',\n '_smrt_job_events',\n '_smrt_workers',\n '_smrt_forge_deliveries',\n '_smrt_forge_projection_checkpoints',\n];\n\n/**\n * Framework tables that are backed by `@smrt()` models *and* reshaped by\n * `system/compatibility.ts`.\n *\n * They are dual-owned: `db:migrate` creates them from the jobs manifest, and\n * the compatibility pass adds columns/indexes that predate the manifest. On a\n * fresh install the manifest wins the race — bootstrap runs before the tables\n * exist — so their compatibility pass is deferred and re-checked until the\n * tables show up (issue #2376).\n */\nexport const DEFERRED_COMPATIBILITY_TABLES: readonly string[] = [\n '_smrt_jobs',\n '_smrt_job_events',\n];\n\n/**\n * System tables SMRT used to create on every database and no longer does.\n *\n * `_smrt_signals` never had a writer and `_smrt_registry` was only written by\n * an `ObjectRegistry.persistToDatabase()` helper that nothing ever called, so\n * both were empty on every deployment. New databases no longer get them.\n * Existing databases keep the empty tables until an operator drops them:\n *\n * ```sql\n * DROP TABLE IF EXISTS _smrt_signals;\n * DROP TABLE IF EXISTS _smrt_registry;\n * ```\n *\n * The framework deliberately does not issue those drops itself — dropping a\n * table is not reversible and a deployment may have repurposed the name.\n */\nexport const RETIRED_SYSTEM_TABLES: readonly string[] = [\n '_smrt_registry',\n '_smrt_signals',\n];\n\n/**\n * Materialize the portable system-table schema for a target engine.\n *\n * PostgreSQL must use TIMESTAMPTZ because every system Date is an instant.\n * PostgreSQL and DuckDB also materialize integer storage as BIGINT, matching\n * application-table DDL; SQLite keeps its variable-width INTEGER.\n */\nexport function getSystemTableDDL(engine: DatabaseEngine): string[] {\n return ALL_SYSTEM_TABLES.map((ddl) =>\n getSystemTableDDLForEngine(ddl, engine),\n );\n}\n\n/** Materialize one portable system-table statement for a target engine. */\nexport function getSystemTableDDLForEngine(\n ddl: string,\n engine: DatabaseEngine,\n): string {\n const integerDdl =\n engine === 'postgres' || engine === 'duckdb' || engine === 'json'\n ? ddl.replace(/\\bINTEGER\\b/g, 'BIGINT')\n : ddl;\n return engine === 'postgres'\n ? integerDdl.replace(/\\bTIMESTAMP\\b/g, 'TIMESTAMPTZ')\n : integerDdl;\n}\n\n/**\n * Current SMRT system schema version.\n *\n * `bootstrapSystemTables()` skips replaying system DDL once this exact version\n * is recorded in `_smrt_migrations`, so every change to {@link ALL_SYSTEM_TABLES}\n * must bump it — otherwise existing databases never see the change. New\n * *columns* additionally need an `addColumnIfMissing()` entry in\n * `system/compatibility.ts`, because `CREATE TABLE IF NOT EXISTS` is a no-op on\n * a table that already exists.\n *\n * 1.10.1 retires the never-written `_smrt_registry` / `_smrt_signals` tables\n * (see {@link RETIRED_SYSTEM_TABLES}) and adds the retention-predicate indexes\n * from #2375 (`_smrt_contexts.expires_at`, `_smrt_ai_usage(tenant_id,\n * created_at)`, `_smrt_dispatch(status, processed_at)` and\n * `(status, updated_at)`) in the same replay. (`1.10.0` briefly existed only\n * on two unreleased, unmerged branches with two different, incompatible DDL\n * sets — never published, never stamped in any real `_smrt_migrations` table\n * — so merging them reuses no version an existing install could have already\n * recorded; see the note on {@link SMRT_SCHEMA_DDL_CHECKSUMS}.)\n */\nexport const SMRT_SCHEMA_VERSION = '1.10.1';\n\n/**\n * Canonical form of the system DDL that {@link SMRT_SCHEMA_DDL_CHECKSUMS} covers.\n *\n * Whitespace is normalized so reformatting alone does not trip the guard while\n * any token change does.\n */\nexport function getSystemSchemaChecksumInput(): string {\n return ALL_SYSTEM_TABLES.join('\\n').replace(/\\s+/g, ' ').trim();\n}\n\n/**\n * SHA-256 of {@link getSystemSchemaChecksumInput} for each system schema\n * version, oldest first.\n *\n * The discipline that keeps fresh installs and upgraded installs identical was\n * documented but unenforced (issue #2376): a contributor could edit the DDL and\n * ship it without bumping the version, and no existing database would ever\n * apply the change. `system-schema-evolution.test.ts` recomputes the current\n * hash and requires it to equal this map's entry for\n * {@link SMRT_SCHEMA_VERSION}.\n *\n * It is a *history*, not a single constant, so the guard cannot be silenced by\n * overwriting one value: every recorded hash must stay distinct, which means a\n * changed DDL has nowhere to be recorded except under a new version key. Adding\n * that key is the bump. This is also why merging two branches that each\n * independently bumped to the same version string (both `1.10.0`, covering two\n * different DDL sets — #2376's table retirement and #2375's retention indexes)\n * must land under a *new* key rather than picking a \"final\" checksum for the\n * shared number: nothing published or otherwise reachable ever stamped\n * `1.10.0`, so recording the merged DDL as `1.10.1` costs nothing and removes\n * any doubt about a version string meaning two different things to two\n * installs.\n *\n * Entries before 1.10.1 are not recorded — the guard starts here.\n */\nexport const SMRT_SCHEMA_DDL_CHECKSUMS: Readonly<Record<string, string>> =\n Object.freeze({\n '1.10.1':\n 'f796ee3b3f7ab8b9dc659ecaa68884ec01277408c3dd6edea540853fce369c16',\n });\n"],"mappings":";;;;;AAaA,IAAa,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0C1C,IAAa,+BAA+B;;;;;;;;;;;;;AAc5C,IAAa,sCAAsC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCnD,IAAa,+BAA+B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiC5C,IAAa,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuD1C,IAAa,2CAA2C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCxD,IAAa,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0D1C,IAAa,4BAA4B;;;;;;;;;;;;;;;;;;;;AAqBzC,IAAa,qCACX,0BAA0B,QAAQ,kBAAkB,aAAa;;AAGnE,IAAa,4CAA4C;;AAGzD,IAAa,uDAAuD,GAAG,0CAA0C;;AAGjH,IAAa,gDAAgD,GAAG,0CAA0C;;;;;;;;;;;AAY1G,IAAa,8CAA8C;6BAC9B,0CAA0C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDvE,IAAa,+CAA+C;;;;;;;4BAOhC,qDAAqD;;EAE/E,4CAA4C,KAAK,EAAE;;;;;;;;;;;;AAarD,IAAa,8CAA8C;;;;;;;4BAO/B,qDAAqD;wBACzD,8CAA8C;;EAEpE,4CAA4C,KAAK,EAAE;;;;;;;;;;;;;AAyBrD,IAAa,qCAAqC;;;;;;;EAjBhD,mCAAmC,MAAM,GAAG,CAAC,CAC1C,KAAK,cAAc,UAAU,KAAK,CAAC,CAAC,CACpC,QAAQ,cAAc,UAAU,SAAS,CAAC,CAAC,CAC3C,KACE,cAAc;EACnB,UAAU;+BAER,CAAC,CACA,KAAK,IAgBR,EAAgC;4BACN,qDAAqD;wBACzD,8CAA8C;;EAEpE,4CAA4C,KAAK,EAAE;;;;;;;;;;;;;;AAerD,IAAa,8BAA8B;;;;;;;;;;;AAY3C,IAAa,oBAAoB;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;;;;;;;;;;;AAkBA,IAAa,+BAAkD;CAC7D;CACA;CACA;CACA;CACA;AACF;;;;;;;;;;;;;;;;;AAiCA,IAAa,wBAA2C,CACtD,kBACA,eACF;;;;;;;;AASA,SAAgB,kBAAkB,QAAkC;CAClE,OAAO,kBAAkB,KAAK,QAC5B,2BAA2B,KAAK,MAAM,CACxC;AACF;;AAGA,SAAgB,2BACd,KACA,QACQ;CACR,MAAM,aACJ,WAAW,cAAc,WAAW,YAAY,WAAW,SACvD,IAAI,QAAQ,gBAAgB,QAAQ,IACpC;CACN,OAAO,WAAW,aACd,WAAW,QAAQ,kBAAkB,aAAa,IAClD;AACN;;;;;;;;;;;;;;;;;;;;;AAsBA,IAAa,sBAAsB;AAsCjC,OAAO,OAAO,EACZ,UACE,mEACJ,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Convert a database integer to JavaScript's only safe integer representation.
3
+ *
4
+ * PostgreSQL surfaces `int8` as a string and DuckDB can surface it as a
5
+ * `bigint`; SQLite returns a number that may already be outside the safe
6
+ * range. SMRT's public model and generated API contracts use `number`, so
7
+ * preserving an out-of-range value would silently change it. Reject it at the
8
+ * read boundary instead.
9
+ */
10
+ export declare function toSafeInteger(value: unknown, context?: string): number;
11
+ /**
12
+ * Convert an integer-backed database boolean to a JavaScript boolean.
13
+ *
14
+ * System tables use integer flags so PostgreSQL and DuckDB surface the same
15
+ * values as SQLite. Do not use JavaScript truthiness here: PostgreSQL returns
16
+ * BIGINT values as strings, and `Boolean('0')` is true.
17
+ */
18
+ export declare function toSafeBooleanInteger(value: unknown, context?: string): boolean;
19
+ //# sourceMappingURL=safe-integer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-integer.d.ts","sourceRoot":"","sources":["../../src/utils/safe-integer.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,SAAY,GAAG,MAAM,CAQzE;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,OAAO,EACd,OAAO,SAAoB,GAC1B,OAAO,CAUT"}
@@ -0,0 +1,31 @@
1
+ //#region src/utils/safe-integer.ts
2
+ /**
3
+ * Convert a database integer to JavaScript's only safe integer representation.
4
+ *
5
+ * PostgreSQL surfaces `int8` as a string and DuckDB can surface it as a
6
+ * `bigint`; SQLite returns a number that may already be outside the safe
7
+ * range. SMRT's public model and generated API contracts use `number`, so
8
+ * preserving an out-of-range value would silently change it. Reject it at the
9
+ * read boundary instead.
10
+ */
11
+ function toSafeInteger(value, context = "integer") {
12
+ const parsed = Number(value);
13
+ if (!Number.isSafeInteger(parsed)) throw new RangeError(`${context} must be a JavaScript safe integer; received ${String(value)}`);
14
+ return parsed;
15
+ }
16
+ /**
17
+ * Convert an integer-backed database boolean to a JavaScript boolean.
18
+ *
19
+ * System tables use integer flags so PostgreSQL and DuckDB surface the same
20
+ * values as SQLite. Do not use JavaScript truthiness here: PostgreSQL returns
21
+ * BIGINT values as strings, and `Boolean('0')` is true.
22
+ */
23
+ function toSafeBooleanInteger(value, context = "boolean integer") {
24
+ if (value === 0 || value === "0" || value === 0n) return false;
25
+ if (value === 1 || value === "1" || value === 1n) return true;
26
+ throw new RangeError(`${context} must be an integer boolean (0 or 1); received ${String(value)}`);
27
+ }
28
+ //#endregion
29
+ export { toSafeBooleanInteger, toSafeInteger };
30
+
31
+ //# sourceMappingURL=safe-integer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-integer.js","names":[],"sources":["../../src/utils/safe-integer.ts"],"sourcesContent":["/**\n * Convert a database integer to JavaScript's only safe integer representation.\n *\n * PostgreSQL surfaces `int8` as a string and DuckDB can surface it as a\n * `bigint`; SQLite returns a number that may already be outside the safe\n * range. SMRT's public model and generated API contracts use `number`, so\n * preserving an out-of-range value would silently change it. Reject it at the\n * read boundary instead.\n */\nexport function toSafeInteger(value: unknown, context = 'integer'): number {\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed)) {\n throw new RangeError(\n `${context} must be a JavaScript safe integer; received ${String(value)}`,\n );\n }\n return parsed;\n}\n\n/**\n * Convert an integer-backed database boolean to a JavaScript boolean.\n *\n * System tables use integer flags so PostgreSQL and DuckDB surface the same\n * values as SQLite. Do not use JavaScript truthiness here: PostgreSQL returns\n * BIGINT values as strings, and `Boolean('0')` is true.\n */\nexport function toSafeBooleanInteger(\n value: unknown,\n context = 'boolean integer',\n): boolean {\n if (value === 0 || value === '0' || value === 0n) {\n return false;\n }\n if (value === 1 || value === '1' || value === 1n) {\n return true;\n }\n throw new RangeError(\n `${context} must be an integer boolean (0 or 1); received ${String(value)}`,\n );\n}\n"],"mappings":";;;;;;;;;;AASA,SAAgB,cAAc,OAAgB,UAAU,WAAmB;CACzE,MAAM,SAAS,OAAO,KAAK;CAC3B,IAAI,CAAC,OAAO,cAAc,MAAM,GAC9B,MAAM,IAAI,WACR,GAAG,QAAQ,+CAA+C,OAAO,KAAK,GACxE;CAEF,OAAO;AACT;;;;;;;;AASA,SAAgB,qBACd,OACA,UAAU,mBACD;CACT,IAAI,UAAU,KAAK,UAAU,OAAO,UAAU,IAC5C,OAAO;CAET,IAAI,UAAU,KAAK,UAAU,OAAO,UAAU,IAC5C,OAAO;CAET,MAAM,IAAI,WACR,GAAG,QAAQ,iDAAiD,OAAO,KAAK,GAC1E;AACF"}
package/dist/utils.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { SmrtObject } from './object';
2
2
  import { classnameToTablename, pluralize, toSnakeCase } from './utils/naming.js';
3
- export { classnameToTablename, pluralize, toSnakeCase };
3
+ import { toSafeBooleanInteger, toSafeInteger } from './utils/safe-integer.js';
4
+ export { classnameToTablename, pluralize, toSafeBooleanInteger, toSafeInteger, toSnakeCase, };
4
5
  /**
5
6
  * Converts a snake_case string to camelCase
6
7
  *
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAG3C,OAAO,EACL,oBAAoB,EACpB,SAAS,EACT,WAAW,EACZ,MAAM,mBAAmB,CAAC;AAQ3B,OAAO,EAAE,oBAAoB,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC;AAExD;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMzB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMzB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,WAItC;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,QAK/C;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,UAK/C;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,UAAU,EAC/C,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,oCAmCjC;AAMD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAMpE;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,6BAA6B,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAa5E;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,kBAAkB,CAEhC,SAAS,EAAE,QAAQ,GAAG,CAAC,KAAK,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,UAAU,CAAC,UAiB7D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAC1B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,CAAC,CAuHxD;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,KAAK,CAE9D;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,2BAc1D"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAG3C,OAAO,EACL,oBAAoB,EACpB,SAAS,EACT,WAAW,EACZ,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAQ9E,OAAO,EACL,oBAAoB,EACpB,SAAS,EACT,oBAAoB,EACpB,aAAa,EACb,WAAW,GACZ,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMzB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMzB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,WAItC;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,QAK/C;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,UAK/C;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,UAAU,EAC/C,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,oCAmCjC;AAMD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAMpE;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,6BAA6B,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAa5E;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,kBAAkB,CAEhC,SAAS,EAAE,QAAQ,GAAG,CAAC,KAAK,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,UAAU,CAAC,UAiB7D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAC1B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,CAAC,CAqIxD;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,KAAK,CAE9D;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,2BAc1D"}
package/dist/utils.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { classnameToTablename, pluralize, toSnakeCase } from "./utils/naming.js";
2
+ import { toSafeBooleanInteger, toSafeInteger } from "./utils/safe-integer.js";
2
3
  import { ObjectRegistry } from "./registry.js";
3
4
  import { createLogger } from "@happyvertical/logger";
4
5
  //#region src/utils.ts
@@ -247,14 +248,16 @@ function formatDataJs(data, fields) {
247
248
  normalizedData[outputKey] = value;
248
249
  }
249
250
  else if (fieldType === "integer") {
250
- const parsed = Number.parseInt(value, 10);
251
- normalizedData[outputKey] = Number.isNaN(parsed) ? value : parsed;
251
+ const parsed = Number(value);
252
+ normalizedData[outputKey] = Number.isFinite(parsed) && Number.isInteger(parsed) ? toSafeInteger(value, `Integer field ${outputKey}`) : value;
252
253
  } else if (fieldType === "real" || fieldType === "decimal") {
253
254
  const parsed = Number.parseFloat(value);
254
255
  normalizedData[outputKey] = Number.isNaN(parsed) ? value : parsed;
255
256
  } else normalizedData[outputKey] = value;
256
257
  else if (typeof value === "number") if (fieldType === "boolean") normalizedData[outputKey] = value === 1;
258
+ else if (fieldType === "integer" && Number.isInteger(value)) normalizedData[outputKey] = toSafeInteger(value, `Integer field ${outputKey}`);
257
259
  else normalizedData[outputKey] = value;
260
+ else if (typeof value === "bigint" && fieldType === "integer") normalizedData[outputKey] = toSafeInteger(value, `Integer field ${outputKey}`);
258
261
  else normalizedData[outputKey] = value;
259
262
  }
260
263
  if (process.env.DEBUG_STI) logger.debug("[formatDataJs] Output normalizedData", {
@@ -290,6 +293,6 @@ function formatDataSql(data) {
290
293
  return normalizedData;
291
294
  }
292
295
  //#endregion
293
- export { classnameToTablename, dateAsObject, dateAsString, fieldsFromClass, formatDataJs, formatDataSql, generateTableRenameMigrations, isDateField, isFieldInstance, keysToCamelCase, keysToSnakeCase, legacyClassnameToTablename, pluralize, tableNameFromClass, toCamelCase, toSnakeCase };
296
+ export { classnameToTablename, dateAsObject, dateAsString, fieldsFromClass, formatDataJs, formatDataSql, generateTableRenameMigrations, isDateField, isFieldInstance, keysToCamelCase, keysToSnakeCase, legacyClassnameToTablename, pluralize, tableNameFromClass, toCamelCase, toSafeBooleanInteger, toSafeInteger, toSnakeCase };
294
297
 
295
298
  //# sourceMappingURL=utils.js.map
package/dist/utils.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","names":[],"sources":["../src/utils.ts"],"sourcesContent":["import { createLogger } from '@happyvertical/logger';\nimport type { SmrtObject } from './object';\nimport { ObjectRegistry } from './registry';\nimport type { SmrtObjectConstructor } from './registry/types';\nimport {\n classnameToTablename,\n pluralize,\n toSnakeCase,\n} from './utils/naming.js';\n\n// formatDataJs' debug traces are gated by DEBUG_STI, so the level must allow\n// debug when it's set (a fixed 'info' would filter them out and break the flag).\nconst logger = createLogger({\n level: process.env.DEBUG_STI ? 'debug' : 'info',\n});\n\nexport { classnameToTablename, pluralize, toSnakeCase };\n\n/**\n * Converts a snake_case string to camelCase\n *\n * @param str - String in snake_case format\n * @returns String in camelCase format\n * @example\n * ```typescript\n * toCamelCase('meetings_url'); // 'meetingsUrl'\n * toCamelCase('created_at'); // 'createdAt'\n * toCamelCase('id'); // 'id'\n * ```\n */\nexport function toCamelCase(str: string): string {\n return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());\n}\n\n/**\n * Converts all keys in an object from camelCase to snake_case\n *\n * @param obj - Object with camelCase keys\n * @returns Object with snake_case keys\n */\nexport function keysToSnakeCase(\n obj: Record<string, unknown>,\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n result[toSnakeCase(key)] = value;\n }\n return result;\n}\n\n/**\n * Converts all keys in an object from snake_case to camelCase\n *\n * @param obj - Object with snake_case keys\n * @returns Object with camelCase keys\n */\nexport function keysToCamelCase(\n obj: Record<string, unknown>,\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n result[toCamelCase(key)] = value;\n }\n return result;\n}\n\n/**\n * Checks if a field name indicates a date field based on naming conventions\n *\n * Recognizes common date field patterns like '_at', '_date', and 'date'.\n * Used for automatic type inference during schema generation.\n *\n * @param key - Field name to check\n * @returns Boolean indicating if the field is likely a date field\n * @example\n * ```typescript\n * isDateField('created_at'); // true\n * isDateField('updated_date'); // true\n * isDateField('name'); // false\n * ```\n */\nexport function isDateField(key: string) {\n // Fallback pattern matching when schema is not available\n // Primary date detection should use schema field types\n return key.endsWith('_date') || key.endsWith('_at') || key === 'date';\n}\n\n/**\n * Converts a date string to a Date object\n *\n * @param date - Date as string or Date object\n * @returns Date object\n */\nexport function dateAsString(date: Date | string) {\n if (typeof date === 'string') {\n return new Date(date);\n }\n return date;\n}\n\n/**\n * Converts a Date object to an ISO string\n *\n * @param date - Date as Date object or string\n * @returns ISO date string or the original string\n */\nexport function dateAsObject(date: Date | string) {\n if (date instanceof Date) {\n return date.toISOString();\n }\n return date;\n}\n\n/**\n * Extracts field definitions from a class constructor\n *\n * Uses ObjectRegistry cached fields from AST manifest exclusively.\n * No runtime introspection fallback - classes must be decorated with @smrt()\n * for schema generation to work.\n *\n * @param ClassType - Class constructor to extract fields from\n * @param values - Optional values to set for the fields\n * @returns Object containing field definitions with names, types, and values\n * @throws {Error} If the class is not registered in ObjectRegistry\n * @example\n * ```typescript\n * @smrt()\n * class Product extends SmrtObject {\n * name: string = '';\n * price: number = 0.0;\n * }\n *\n * const fields = await fieldsFromClass(Product);\n * console.log(fields.name.type); // 'TEXT'\n * console.log(fields.price.type); // 'REAL'\n * ```\n */\nexport async function fieldsFromClass(\n ClassType: new (...args: never[]) => SmrtObject,\n values?: Record<string, unknown>,\n) {\n // `getClassByConstructor` expects the registry's `SmrtObjectConstructor`\n // (`new (...args: any[]) => SmrtObject`); the narrower `never[]` param type\n // is contravariantly assignable, so this widening cast is purely structural.\n const className =\n ObjectRegistry.getClassByConstructor(ClassType as SmrtObjectConstructor)\n ?.name || ClassType.name;\n // NEW: Use getAllFields() to include inherited fields from parent classes\n const cachedFields = await ObjectRegistry.getAllFields(className);\n\n // Phase 2: AST manifest only - no runtime introspection fallback\n if (cachedFields.size === 0) {\n // Return empty fields for unregistered classes (for backward compatibility)\n // generateSchema() will throw if it needs field definitions\n return {};\n }\n\n // Use cached field definitions from AST manifest\n const fields: Record<string, unknown> = {};\n\n // Add/override with fields from cached registry\n for (const [key, field] of cachedFields.entries()) {\n const meta = { ...(field._meta || {}) };\n delete meta.__smrtSystemField;\n\n fields[key] = {\n name: key,\n type: field.type || 'TEXT',\n _meta: meta,\n ...(values && key in values ? { value: values[key] } : {}),\n };\n }\n\n return fields;\n}\n\n// NOTE: generateSchema moved to schema/utils.ts\n// to prevent bundling Node.js-only code (SchemaGenerator with node:crypto) in browser builds.\n// Import from './schema/utils' in Node.js code that needs schema generation.\n\n/**\n * Returns the old (incorrect) pluralized table name for migration purposes.\n *\n * This function replicates the previous buggy behavior where 'y' → 'ies'\n * transformation was applied AFTER adding 's', resulting in incorrect\n * pluralization (e.g., 'currency' → 'currencys' instead of 'currencies').\n *\n * Use this to generate migration SQL that renames old tables to new names.\n *\n * @param className - Name of the class\n * @returns The incorrectly pluralized table name (old behavior)\n * @example\n * ```typescript\n * // Generate migration for a class\n * const oldName = legacyClassnameToTablename('Currency'); // 'currencys'\n * const newName = classnameToTablename('Currency'); // 'currencies'\n * console.log(`ALTER TABLE ${oldName} RENAME TO ${newName};`);\n * ```\n */\nexport function legacyClassnameToTablename(className: string): string {\n return className\n .replace(/([a-z])([A-Z])/g, '$1_$2')\n .toLowerCase()\n .replace(/([^s])$/, '$1s')\n .replace(/y$/, 'ies'); // This runs AFTER 's' is added, so it's a no-op for 'y' words\n}\n\n/**\n * Generates migration SQL for renaming tables from old to new naming convention.\n *\n * Returns an array of SQL statements to rename tables that were affected\n * by the pluralization bug (Issue #839).\n *\n * @param classNames - Array of class names to check for migration\n * @returns Array of SQL RENAME statements (empty if no changes needed)\n * @example\n * ```typescript\n * const migrations = generateTableRenameMigrations([\n * 'Currency',\n * 'JournalEntry',\n * 'Product' // Not affected\n * ]);\n * // Returns:\n * // [\n * // 'ALTER TABLE currencys RENAME TO currencies;',\n * // 'ALTER TABLE journal_entrys RENAME TO journal_entries;'\n * // ]\n * ```\n */\nexport function generateTableRenameMigrations(classNames: string[]): string[] {\n const migrations: string[] = [];\n\n for (const className of classNames) {\n const oldName = legacyClassnameToTablename(className);\n const newName = classnameToTablename(className);\n\n if (oldName !== newName) {\n migrations.push(`ALTER TABLE ${oldName} RENAME TO ${newName};`);\n }\n }\n\n return migrations;\n}\n\n/**\n * Generates a table name from a class constructor\n *\n * Checks for SMRT_TABLE_NAME static property first (set by @smrt() decorator),\n * which survives code minification. Falls back to deriving from ClassType.name\n * for backward compatibility.\n *\n * @param ClassType - Class constructor or function\n * @returns Pluralized snake_case table name\n * @example\n * ```typescript\n * // With @smrt() decorator (recommended)\n * @smrt()\n * class Product extends SmrtObject { }\n * tableNameFromClass(Product); // \"products\" (captured before minification)\n *\n * // Without decorator (fallback)\n * class Category extends SmrtObject { }\n * tableNameFromClass(Category); // \"categories\" (derived from runtime name)\n * ```\n */\nexport function tableNameFromClass(\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n ClassType: Function | (new (...args: never[]) => SmrtObject),\n) {\n // Check for SMRT_TABLE_NAME property set by @smrt() decorator (survives minification)\n if ('SMRT_TABLE_NAME' in ClassType) {\n // The `in` guard above proves the static decorator property exists; read it\n // through a typed shape rather than `any`.\n return (ClassType as { SMRT_TABLE_NAME: string }).SMRT_TABLE_NAME;\n }\n\n // Fallback: derive from class name (breaks with minification)\n const snakeCase = ClassType.name\n // Insert underscore between lower & upper case letters\n .replace(/([a-z])([A-Z])/g, '$1_$2')\n // Convert to lowercase\n .toLowerCase();\n\n return pluralize(snakeCase);\n}\n\n/**\n * Formats data for JavaScript by converting date strings to Date objects\n * and snake_case column names to camelCase properties\n *\n * Generic over the input row type `T` so callers preserve their (typically\n * loose) row typing across the hydration boundary — this matches the historic\n * behavior where a `Record<string, any>` / `any` row produced an equally loose\n * result. Keys are re-cased and values are type-converted at runtime; the\n * return is the same `Record` shape, so it is reasserted as `T` at this DB\n * boundary rather than widened to an unrelated type.\n *\n * @param data - Object with data to format (snake_case column names from DB)\n * @param fields - Optional field definitions to determine types (from fieldsFromClass)\n * @returns Object with properly typed values and camelCase property names for JavaScript\n */\nexport function formatDataJs<\n T extends Record<string, unknown> = Record<string, unknown>,\n>(data: T, fields?: Record<string, { type?: string }>): T {\n const normalizedData: Record<string, unknown> = {};\n\n if (process.env.DEBUG_STI) {\n logger.debug('[formatDataJs] Input data', {\n hasMetaData: !!data._meta_data,\n metaType: data._meta_type,\n keys: Object.keys(data),\n });\n }\n\n // STI: If _meta_data exists, merge it into a FRESH object first so meta\n // fields are available during formatting. This must never mutate the caller's\n // `data` argument — `formatDataJs` is a public export and external callers may\n // pass shared objects that would otherwise get silent field injection (#1378).\n let mergedData: Record<string, unknown> = data;\n if (data._meta_data) {\n // `_meta_data` is the STI meta column: a JSON string from the DB or an\n // already-parsed object. Either way it is an object of meta fields; type it\n // as such at this boundary so the merge/key-walk below stays sound.\n const metaData: Record<string, unknown> =\n typeof data._meta_data === 'string'\n ? JSON.parse(data._meta_data)\n : (data._meta_data as Record<string, unknown>);\n\n if (process.env.DEBUG_STI) {\n logger.debug('[formatDataJs] Merging _meta_data', {\n metaDataKeys: Object.keys(metaData),\n metaData,\n });\n }\n\n // Merge meta fields into a copy (will be formatted below). Spreading\n // `metaData` last preserves the original precedence of Object.assign.\n mergedData = { ...data, ...metaData };\n\n if (process.env.DEBUG_STI) {\n logger.debug('[formatDataJs] After merge, data keys', {\n keys: Object.keys(mergedData),\n });\n }\n }\n\n for (const [key, value] of Object.entries(mergedData)) {\n // Preserve keys with leading underscore (e.g., _meta_type, _meta_data)\n // These are special STI/framework fields that should not be camelCased\n const camelKey = key.startsWith('_') ? key : toCamelCase(key);\n\n // Determine the actual output key based on what's in fields\n // If the original key (snake_case) is in fields, use it; otherwise use camelCase\n // This supports both conventions (e.g., publish_date vs publishDate)\n let outputKey = camelKey;\n if (fields && key in fields && !(camelKey in fields)) {\n // Original key exists in fields but camelCase doesn't - use original (snake_case)\n outputKey = key;\n }\n\n // Get field type from fields, trying both key variants\n const fieldDef = fields?.[outputKey] ?? fields?.[camelKey] ?? fields?.[key];\n const fieldType = fieldDef?.type?.toLowerCase();\n\n if (value instanceof Date) {\n normalizedData[outputKey] = value;\n } else if (typeof value === 'string') {\n // Use field definitions if available, otherwise fall back to name patterns\n const isDate = fieldType === 'datetime' || isDateField(key);\n\n if (isDate) {\n const parsedDate = value.trim() ? new Date(value) : null;\n normalizedData[outputKey] =\n parsedDate && !Number.isNaN(parsedDate.getTime())\n ? parsedDate\n : value;\n } else if (fieldType === 'json') {\n // Parse JSON strings back to objects\n try {\n normalizedData[outputKey] = JSON.parse(value);\n } catch {\n // Keep as string if parsing fails\n normalizedData[outputKey] = value;\n }\n } else if (fieldType === 'integer') {\n // Convert string numbers to integers for INTEGER fields\n // SQLite may return \"2.0\" as a string in some cases\n const parsed = Number.parseInt(value, 10);\n normalizedData[outputKey] = Number.isNaN(parsed) ? value : parsed;\n } else if (fieldType === 'real' || fieldType === 'decimal') {\n // Convert string numbers to floats for REAL/DECIMAL fields\n const parsed = Number.parseFloat(value);\n normalizedData[outputKey] = Number.isNaN(parsed) ? value : parsed;\n } else {\n normalizedData[outputKey] = value;\n }\n } else if (typeof value === 'number') {\n if (fieldType === 'boolean') {\n // Convert SQLite integers (0/1) to booleans for boolean fields\n normalizedData[outputKey] = value === 1;\n } else {\n // Pass through numeric values as-is\n // Note: In JavaScript, 2.0 === 2 so no conversion needed\n // Non-integers in INTEGER fields (e.g., 2.9) are kept to surface data issues\n normalizedData[outputKey] = value;\n }\n } else {\n normalizedData[outputKey] = value;\n }\n }\n\n if (process.env.DEBUG_STI) {\n logger.debug('[formatDataJs] Output normalizedData', {\n keys: Object.keys(normalizedData),\n metaType: normalizedData._meta_type,\n });\n }\n\n // Reassert the re-cased/type-converted row as the caller's row type `T`.\n // The output is structurally a `Record<string, unknown>`; this boundary cast\n // preserves the historic loose-in/loose-out hydration contract without `any`.\n return normalizedData as T;\n}\n\n/**\n * Type guard to check if a value is a Field instance\n *\n * @deprecated Field helpers have been removed. This function always returns false.\n * @param value - Value to check\n * @returns Always false (Field class no longer exists)\n */\nexport function isFieldInstance(value: unknown): value is never {\n return false;\n}\n\n/**\n * Formats data for SQL by converting Date objects to ISO strings\n * and camelCase property names to snake_case column names\n *\n * @param data - Object with data to format (camelCase property names from JavaScript)\n * @returns Object with properly formatted values and snake_case column names for SQL\n */\nexport function formatDataSql(data: Record<string, unknown>) {\n const normalizedData: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(data)) {\n // Convert camelCase to snake_case for SQL\n const snakeKey = toSnakeCase(key);\n\n // Field helpers removed - no need to extract values (deprecated code removed)\n if (value instanceof Date) {\n normalizedData[snakeKey] = value.toISOString(); // Postgres accepts ISO format with timezone\n } else {\n normalizedData[snakeKey] = value;\n }\n }\n return normalizedData;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"utils.js","names":[],"sources":["../src/utils.ts"],"sourcesContent":["import { createLogger } from '@happyvertical/logger';\nimport type { SmrtObject } from './object';\nimport { ObjectRegistry } from './registry';\nimport type { SmrtObjectConstructor } from './registry/types';\nimport {\n classnameToTablename,\n pluralize,\n toSnakeCase,\n} from './utils/naming.js';\nimport { toSafeBooleanInteger, toSafeInteger } from './utils/safe-integer.js';\n\n// formatDataJs' debug traces are gated by DEBUG_STI, so the level must allow\n// debug when it's set (a fixed 'info' would filter them out and break the flag).\nconst logger = createLogger({\n level: process.env.DEBUG_STI ? 'debug' : 'info',\n});\n\nexport {\n classnameToTablename,\n pluralize,\n toSafeBooleanInteger,\n toSafeInteger,\n toSnakeCase,\n};\n\n/**\n * Converts a snake_case string to camelCase\n *\n * @param str - String in snake_case format\n * @returns String in camelCase format\n * @example\n * ```typescript\n * toCamelCase('meetings_url'); // 'meetingsUrl'\n * toCamelCase('created_at'); // 'createdAt'\n * toCamelCase('id'); // 'id'\n * ```\n */\nexport function toCamelCase(str: string): string {\n return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());\n}\n\n/**\n * Converts all keys in an object from camelCase to snake_case\n *\n * @param obj - Object with camelCase keys\n * @returns Object with snake_case keys\n */\nexport function keysToSnakeCase(\n obj: Record<string, unknown>,\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n result[toSnakeCase(key)] = value;\n }\n return result;\n}\n\n/**\n * Converts all keys in an object from snake_case to camelCase\n *\n * @param obj - Object with snake_case keys\n * @returns Object with camelCase keys\n */\nexport function keysToCamelCase(\n obj: Record<string, unknown>,\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n result[toCamelCase(key)] = value;\n }\n return result;\n}\n\n/**\n * Checks if a field name indicates a date field based on naming conventions\n *\n * Recognizes common date field patterns like '_at', '_date', and 'date'.\n * Used for automatic type inference during schema generation.\n *\n * @param key - Field name to check\n * @returns Boolean indicating if the field is likely a date field\n * @example\n * ```typescript\n * isDateField('created_at'); // true\n * isDateField('updated_date'); // true\n * isDateField('name'); // false\n * ```\n */\nexport function isDateField(key: string) {\n // Fallback pattern matching when schema is not available\n // Primary date detection should use schema field types\n return key.endsWith('_date') || key.endsWith('_at') || key === 'date';\n}\n\n/**\n * Converts a date string to a Date object\n *\n * @param date - Date as string or Date object\n * @returns Date object\n */\nexport function dateAsString(date: Date | string) {\n if (typeof date === 'string') {\n return new Date(date);\n }\n return date;\n}\n\n/**\n * Converts a Date object to an ISO string\n *\n * @param date - Date as Date object or string\n * @returns ISO date string or the original string\n */\nexport function dateAsObject(date: Date | string) {\n if (date instanceof Date) {\n return date.toISOString();\n }\n return date;\n}\n\n/**\n * Extracts field definitions from a class constructor\n *\n * Uses ObjectRegistry cached fields from AST manifest exclusively.\n * No runtime introspection fallback - classes must be decorated with @smrt()\n * for schema generation to work.\n *\n * @param ClassType - Class constructor to extract fields from\n * @param values - Optional values to set for the fields\n * @returns Object containing field definitions with names, types, and values\n * @throws {Error} If the class is not registered in ObjectRegistry\n * @example\n * ```typescript\n * @smrt()\n * class Product extends SmrtObject {\n * name: string = '';\n * price: number = 0.0;\n * }\n *\n * const fields = await fieldsFromClass(Product);\n * console.log(fields.name.type); // 'TEXT'\n * console.log(fields.price.type); // 'REAL'\n * ```\n */\nexport async function fieldsFromClass(\n ClassType: new (...args: never[]) => SmrtObject,\n values?: Record<string, unknown>,\n) {\n // `getClassByConstructor` expects the registry's `SmrtObjectConstructor`\n // (`new (...args: any[]) => SmrtObject`); the narrower `never[]` param type\n // is contravariantly assignable, so this widening cast is purely structural.\n const className =\n ObjectRegistry.getClassByConstructor(ClassType as SmrtObjectConstructor)\n ?.name || ClassType.name;\n // NEW: Use getAllFields() to include inherited fields from parent classes\n const cachedFields = await ObjectRegistry.getAllFields(className);\n\n // Phase 2: AST manifest only - no runtime introspection fallback\n if (cachedFields.size === 0) {\n // Return empty fields for unregistered classes (for backward compatibility)\n // generateSchema() will throw if it needs field definitions\n return {};\n }\n\n // Use cached field definitions from AST manifest\n const fields: Record<string, unknown> = {};\n\n // Add/override with fields from cached registry\n for (const [key, field] of cachedFields.entries()) {\n const meta = { ...(field._meta || {}) };\n delete meta.__smrtSystemField;\n\n fields[key] = {\n name: key,\n type: field.type || 'TEXT',\n _meta: meta,\n ...(values && key in values ? { value: values[key] } : {}),\n };\n }\n\n return fields;\n}\n\n// NOTE: generateSchema moved to schema/utils.ts\n// to prevent bundling Node.js-only code (SchemaGenerator with node:crypto) in browser builds.\n// Import from './schema/utils' in Node.js code that needs schema generation.\n\n/**\n * Returns the old (incorrect) pluralized table name for migration purposes.\n *\n * This function replicates the previous buggy behavior where 'y' → 'ies'\n * transformation was applied AFTER adding 's', resulting in incorrect\n * pluralization (e.g., 'currency' → 'currencys' instead of 'currencies').\n *\n * Use this to generate migration SQL that renames old tables to new names.\n *\n * @param className - Name of the class\n * @returns The incorrectly pluralized table name (old behavior)\n * @example\n * ```typescript\n * // Generate migration for a class\n * const oldName = legacyClassnameToTablename('Currency'); // 'currencys'\n * const newName = classnameToTablename('Currency'); // 'currencies'\n * console.log(`ALTER TABLE ${oldName} RENAME TO ${newName};`);\n * ```\n */\nexport function legacyClassnameToTablename(className: string): string {\n return className\n .replace(/([a-z])([A-Z])/g, '$1_$2')\n .toLowerCase()\n .replace(/([^s])$/, '$1s')\n .replace(/y$/, 'ies'); // This runs AFTER 's' is added, so it's a no-op for 'y' words\n}\n\n/**\n * Generates migration SQL for renaming tables from old to new naming convention.\n *\n * Returns an array of SQL statements to rename tables that were affected\n * by the pluralization bug (Issue #839).\n *\n * @param classNames - Array of class names to check for migration\n * @returns Array of SQL RENAME statements (empty if no changes needed)\n * @example\n * ```typescript\n * const migrations = generateTableRenameMigrations([\n * 'Currency',\n * 'JournalEntry',\n * 'Product' // Not affected\n * ]);\n * // Returns:\n * // [\n * // 'ALTER TABLE currencys RENAME TO currencies;',\n * // 'ALTER TABLE journal_entrys RENAME TO journal_entries;'\n * // ]\n * ```\n */\nexport function generateTableRenameMigrations(classNames: string[]): string[] {\n const migrations: string[] = [];\n\n for (const className of classNames) {\n const oldName = legacyClassnameToTablename(className);\n const newName = classnameToTablename(className);\n\n if (oldName !== newName) {\n migrations.push(`ALTER TABLE ${oldName} RENAME TO ${newName};`);\n }\n }\n\n return migrations;\n}\n\n/**\n * Generates a table name from a class constructor\n *\n * Checks for SMRT_TABLE_NAME static property first (set by @smrt() decorator),\n * which survives code minification. Falls back to deriving from ClassType.name\n * for backward compatibility.\n *\n * @param ClassType - Class constructor or function\n * @returns Pluralized snake_case table name\n * @example\n * ```typescript\n * // With @smrt() decorator (recommended)\n * @smrt()\n * class Product extends SmrtObject { }\n * tableNameFromClass(Product); // \"products\" (captured before minification)\n *\n * // Without decorator (fallback)\n * class Category extends SmrtObject { }\n * tableNameFromClass(Category); // \"categories\" (derived from runtime name)\n * ```\n */\nexport function tableNameFromClass(\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n ClassType: Function | (new (...args: never[]) => SmrtObject),\n) {\n // Check for SMRT_TABLE_NAME property set by @smrt() decorator (survives minification)\n if ('SMRT_TABLE_NAME' in ClassType) {\n // The `in` guard above proves the static decorator property exists; read it\n // through a typed shape rather than `any`.\n return (ClassType as { SMRT_TABLE_NAME: string }).SMRT_TABLE_NAME;\n }\n\n // Fallback: derive from class name (breaks with minification)\n const snakeCase = ClassType.name\n // Insert underscore between lower & upper case letters\n .replace(/([a-z])([A-Z])/g, '$1_$2')\n // Convert to lowercase\n .toLowerCase();\n\n return pluralize(snakeCase);\n}\n\n/**\n * Formats data for JavaScript by converting date strings to Date objects\n * and snake_case column names to camelCase properties\n *\n * Generic over the input row type `T` so callers preserve their (typically\n * loose) row typing across the hydration boundary — this matches the historic\n * behavior where a `Record<string, any>` / `any` row produced an equally loose\n * result. Keys are re-cased and values are type-converted at runtime; the\n * return is the same `Record` shape, so it is reasserted as `T` at this DB\n * boundary rather than widened to an unrelated type.\n *\n * @param data - Object with data to format (snake_case column names from DB)\n * @param fields - Optional field definitions to determine types (from fieldsFromClass)\n * @returns Object with properly typed values and camelCase property names for JavaScript\n */\nexport function formatDataJs<\n T extends Record<string, unknown> = Record<string, unknown>,\n>(data: T, fields?: Record<string, { type?: string }>): T {\n const normalizedData: Record<string, unknown> = {};\n\n if (process.env.DEBUG_STI) {\n logger.debug('[formatDataJs] Input data', {\n hasMetaData: !!data._meta_data,\n metaType: data._meta_type,\n keys: Object.keys(data),\n });\n }\n\n // STI: If _meta_data exists, merge it into a FRESH object first so meta\n // fields are available during formatting. This must never mutate the caller's\n // `data` argument — `formatDataJs` is a public export and external callers may\n // pass shared objects that would otherwise get silent field injection (#1378).\n let mergedData: Record<string, unknown> = data;\n if (data._meta_data) {\n // `_meta_data` is the STI meta column: a JSON string from the DB or an\n // already-parsed object. Either way it is an object of meta fields; type it\n // as such at this boundary so the merge/key-walk below stays sound.\n const metaData: Record<string, unknown> =\n typeof data._meta_data === 'string'\n ? JSON.parse(data._meta_data)\n : (data._meta_data as Record<string, unknown>);\n\n if (process.env.DEBUG_STI) {\n logger.debug('[formatDataJs] Merging _meta_data', {\n metaDataKeys: Object.keys(metaData),\n metaData,\n });\n }\n\n // Merge meta fields into a copy (will be formatted below). Spreading\n // `metaData` last preserves the original precedence of Object.assign.\n mergedData = { ...data, ...metaData };\n\n if (process.env.DEBUG_STI) {\n logger.debug('[formatDataJs] After merge, data keys', {\n keys: Object.keys(mergedData),\n });\n }\n }\n\n for (const [key, value] of Object.entries(mergedData)) {\n // Preserve keys with leading underscore (e.g., _meta_type, _meta_data)\n // These are special STI/framework fields that should not be camelCased\n const camelKey = key.startsWith('_') ? key : toCamelCase(key);\n\n // Determine the actual output key based on what's in fields\n // If the original key (snake_case) is in fields, use it; otherwise use camelCase\n // This supports both conventions (e.g., publish_date vs publishDate)\n let outputKey = camelKey;\n if (fields && key in fields && !(camelKey in fields)) {\n // Original key exists in fields but camelCase doesn't - use original (snake_case)\n outputKey = key;\n }\n\n // Get field type from fields, trying both key variants\n const fieldDef = fields?.[outputKey] ?? fields?.[camelKey] ?? fields?.[key];\n const fieldType = fieldDef?.type?.toLowerCase();\n\n if (value instanceof Date) {\n normalizedData[outputKey] = value;\n } else if (typeof value === 'string') {\n // Use field definitions if available, otherwise fall back to name patterns\n const isDate = fieldType === 'datetime' || isDateField(key);\n\n if (isDate) {\n const parsedDate = value.trim() ? new Date(value) : null;\n normalizedData[outputKey] =\n parsedDate && !Number.isNaN(parsedDate.getTime())\n ? parsedDate\n : value;\n } else if (fieldType === 'json') {\n // Parse JSON strings back to objects\n try {\n normalizedData[outputKey] = JSON.parse(value);\n } catch {\n // Keep as string if parsing fails\n normalizedData[outputKey] = value;\n }\n } else if (fieldType === 'integer') {\n // PostgreSQL returns int8 as strings. SQLite may spell an integer as\n // \"2.0\"; reject values JavaScript cannot represent exactly rather than\n // rounding them while hydrating a model.\n const parsed = Number(value);\n normalizedData[outputKey] =\n Number.isFinite(parsed) && Number.isInteger(parsed)\n ? toSafeInteger(value, `Integer field ${outputKey}`)\n : value;\n } else if (fieldType === 'real' || fieldType === 'decimal') {\n // Convert string numbers to floats for REAL/DECIMAL fields\n const parsed = Number.parseFloat(value);\n normalizedData[outputKey] = Number.isNaN(parsed) ? value : parsed;\n } else {\n normalizedData[outputKey] = value;\n }\n } else if (typeof value === 'number') {\n if (fieldType === 'boolean') {\n // Convert SQLite integers (0/1) to booleans for boolean fields\n normalizedData[outputKey] = value === 1;\n } else if (fieldType === 'integer' && Number.isInteger(value)) {\n normalizedData[outputKey] = toSafeInteger(\n value,\n `Integer field ${outputKey}`,\n );\n } else {\n // Pass through numeric values as-is\n // Note: In JavaScript, 2.0 === 2 so no conversion needed\n // Non-integers in INTEGER fields (e.g., 2.9) are kept to surface data issues\n normalizedData[outputKey] = value;\n }\n } else if (typeof value === 'bigint' && fieldType === 'integer') {\n normalizedData[outputKey] = toSafeInteger(\n value,\n `Integer field ${outputKey}`,\n );\n } else {\n normalizedData[outputKey] = value;\n }\n }\n\n if (process.env.DEBUG_STI) {\n logger.debug('[formatDataJs] Output normalizedData', {\n keys: Object.keys(normalizedData),\n metaType: normalizedData._meta_type,\n });\n }\n\n // Reassert the re-cased/type-converted row as the caller's row type `T`.\n // The output is structurally a `Record<string, unknown>`; this boundary cast\n // preserves the historic loose-in/loose-out hydration contract without `any`.\n return normalizedData as T;\n}\n\n/**\n * Type guard to check if a value is a Field instance\n *\n * @deprecated Field helpers have been removed. This function always returns false.\n * @param value - Value to check\n * @returns Always false (Field class no longer exists)\n */\nexport function isFieldInstance(value: unknown): value is never {\n return false;\n}\n\n/**\n * Formats data for SQL by converting Date objects to ISO strings\n * and camelCase property names to snake_case column names\n *\n * @param data - Object with data to format (camelCase property names from JavaScript)\n * @returns Object with properly formatted values and snake_case column names for SQL\n */\nexport function formatDataSql(data: Record<string, unknown>) {\n const normalizedData: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(data)) {\n // Convert camelCase to snake_case for SQL\n const snakeKey = toSnakeCase(key);\n\n // Field helpers removed - no need to extract values (deprecated code removed)\n if (value instanceof Date) {\n normalizedData[snakeKey] = value.toISOString(); // Postgres accepts ISO format with timezone\n } else {\n normalizedData[snakeKey] = value;\n }\n }\n return normalizedData;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EACV,qBAAqB,EAEtB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,MAAM,EAAiC,MAAM,MAAM,CAAC;AAClE,OAAO,EAEL,KAAK,6BAA6B,EACnC,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,qBAAqB,CAAC;AA0B7B,OAAO,EACL,kCAAkC,EAClC,KAAK,sCAAsC,EAC3C,KAAK,8BAA8B,EACnC,KAAK,6BAA6B,EAClC,KAAK,0BAA0B,EAC/B,+BAA+B,EAC/B,4BAA4B,GAC7B,MAAM,2BAA2B,CAAC;AACnC,YAAY,EACV,wBAAwB,EACxB,gBAAgB,GACjB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,6BAA6B,EAC7B,uBAAuB,EACvB,iBAAiB,EACjB,2BAA2B,EAC3B,mBAAmB,EACnB,4BAA4B,GAC7B,MAAM,0BAA0B,CAAC;AAalC,MAAM,WAAW,iBAAiB;IAChC,yEAAyE;IACzE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,6BAA6B,CAAC;IACnD,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,0BAA0B;IAC1B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,6BAA6B;IAC7B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qBAAqB;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,sCAAsC;IACtC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,kFAAkF;IAClF,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,4EAA4E;IAC5E,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,wEAAwE;IACxE,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;IACpC;;OAEG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,8CAA8C;IAC9C,SAAS,CAAC,EAAE;QACV,yDAAyD;QACzD,OAAO,EAAE,OAAO,CAAC;QACjB,wEAAwE;QACxE,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,qEAAqE;QACrE,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,mEAAmE;QACnE,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,mDAAmD;QACnD,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB;;;;WAIG;QACH,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB;;;WAGG;QACH,YAAY,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,OAAO,CAAA;SAAE,CAAC;QACrC;;;;WAIG;QACH,WAAW,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,OAAO,CAAC;YAAC,cAAc,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAC9D,CAAC;IACF,mEAAmE;IACnE,SAAS,CAAC,EAAE,qBAAqB,GAAG,KAAK,CAAC;IAC1C;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;AA8BD,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAG1D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,mBAAmB,GAC5B,MAAM,CAaR;AAED,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,MAAM,CA4+BlE;AAwTD;;;GAGG;AACH,wBAAsB,2BAA2B,CAC/C,QAAQ,EAAE,mBAAmB,EAC7B,WAAW,EAAE,MAAM,EACnB,oBAAoB,EAAE,MAAM,GAC3B,OAAO,CAAC,IAAI,CAAC,CAgaf"}
@@ -22,7 +22,6 @@ var VIRTUAL_MODULES = {
22
22
  "@happyvertical/smrt-virt-mcp": "smrt:mcp",
23
23
  "@happyvertical/smrt-virt-types": "smrt:types",
24
24
  "@happyvertical/smrt-virt-manifest": "smrt:manifest",
25
- "@happyvertical/smrt-virt-schema": "smrt:schema",
26
25
  "@happyvertical/smrt-virt-ui": "smrt:ui",
27
26
  "@happyvertical/smrt-virt-cli": "smrt:cli",
28
27
  "@happyvertical/smrt-virt-web": "smrt:web"
@@ -447,7 +446,6 @@ function smrtPlugin(options = {}) {
447
446
  case "smrt:mcp": return await generateMCPModule(manifest);
448
447
  case "smrt:types": return await generateTypesModule(manifest, pluginMode);
449
448
  case "smrt:manifest": return generateManifestModule(manifest);
450
- case "smrt:schema": return await generateSchemaModule(manifest);
451
449
  case "smrt:ui": return await loadDefaultUI();
452
450
  case "smrt:index-html": return await loadDefaultHTML();
453
451
  case "smrt:cli": return await generateCLIModule(manifest);
@@ -1349,57 +1347,6 @@ export default setupCLI;
1349
1347
  `;
1350
1348
  }
1351
1349
  }
1352
- /**
1353
- * Generate virtual schema module with JSON manifests
1354
- */
1355
- async function generateSchemaModule(manifest) {
1356
- try {
1357
- const { SchemaGenerator } = await importBuildAwareModule({
1358
- source: "../schema/generator.ts",
1359
- dist: "../schema/generator.js"
1360
- });
1361
- const schemaGenerator = new SchemaGenerator();
1362
- const schemas = {};
1363
- for (const [className, objectDef] of Object.entries(manifest.objects)) schemas[className] = schemaGenerator.generateSchema(objectDef);
1364
- const schemaManifest = {
1365
- version: "1.0.0",
1366
- timestamp: 0,
1367
- packageName: manifest.packageName || "unknown",
1368
- schemas,
1369
- dependencies: Array.from(new Set(Object.values(schemas).flatMap((s) => s.dependencies || [])))
1370
- };
1371
- return `// Auto-generated schema manifest from SMRT objects
1372
- // This file is generated automatically - do not edit
1373
-
1374
- // Schema manifest as JSON for SQL adapters
1375
- export const schemaManifest = ${JSON.stringify(schemaManifest, null, 2)};
1376
-
1377
- // Schema registry for runtime access
1378
- export const schemas = schemaManifest.schemas;
1379
-
1380
- // Schema lookup function
1381
- export function getSchema(className: string) {
1382
- return schemas[className];
1383
- }
1384
-
1385
- // All schemas as array for dependency resolution
1386
- export const allSchemas = Object.values(schemas);
1387
-
1388
- // Package information
1389
- export const packageName = schemaManifest.packageName;
1390
- export const dependencies = schemaManifest.dependencies;
1391
-
1392
- export default schemaManifest;`;
1393
- } catch (error) {
1394
- console.error("[smrt] Error generating schema module:", error);
1395
- return `// Error generating schema module
1396
- export const schemaManifest = { schemas: {}, dependencies: [] };
1397
- export const schemas = {};
1398
- export function getSchema() { return null; }
1399
- export const allSchemas = [];
1400
- export default {};`;
1401
- }
1402
- }
1403
1350
  //#endregion
1404
1351
  export { findCliApiCoherenceViolations, generateInlineRegisterModule, generateSvelteKitRoutes, generateTypeDeclarationFile, isRegisterShimModuleId, loadVerifiedSmrtGenerationSnapshot, methodNameToKebab, resolveApiActionRouteConfig, resolveApiActionSet, serializeSmrtGenerationSnapshot, sha256SmrtGenerationSnapshot, smrtPlugin, validateCliIncludeAgainstApi };
1405
1352