@happyvertical/smrt-core 0.38.2 → 0.38.4

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 (130) hide show
  1. package/AGENTS.md +11 -2
  2. package/dist/__tests__/fixtures/advisor-test-classes.d.ts +9 -0
  3. package/dist/__tests__/fixtures/advisor-test-classes.d.ts.map +1 -1
  4. package/dist/change-feed.d.ts +19 -10
  5. package/dist/change-feed.d.ts.map +1 -1
  6. package/dist/change-feed.js +43 -13
  7. package/dist/change-feed.js.map +1 -1
  8. package/dist/change-signals.d.ts +83 -0
  9. package/dist/change-signals.d.ts.map +1 -0
  10. package/dist/change-signals.js +272 -0
  11. package/dist/change-signals.js.map +1 -0
  12. package/dist/collection-cache.d.ts +21 -0
  13. package/dist/collection-cache.d.ts.map +1 -1
  14. package/dist/collection-cache.js +0 -0
  15. package/dist/collection-cache.js.map +1 -1
  16. package/dist/config.d.ts +1 -1
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js.map +1 -1
  19. package/dist/decorators/index.d.ts +8 -0
  20. package/dist/decorators/index.d.ts.map +1 -1
  21. package/dist/decorators/index.js.map +1 -1
  22. package/dist/errors.js +2 -1
  23. package/dist/errors.js.map +1 -1
  24. package/dist/generators/changes-route.d.ts +15 -27
  25. package/dist/generators/changes-route.d.ts.map +1 -1
  26. package/dist/generators/changes-route.js +17 -7
  27. package/dist/generators/changes-route.js.map +1 -1
  28. package/dist/generators/cli.d.ts +3 -0
  29. package/dist/generators/cli.d.ts.map +1 -1
  30. package/dist/generators/cli.js +7 -4
  31. package/dist/generators/cli.js.map +1 -1
  32. package/dist/generators/conditional-get.d.ts +17 -6
  33. package/dist/generators/conditional-get.d.ts.map +1 -1
  34. package/dist/generators/conditional-get.js +19 -13
  35. package/dist/generators/conditional-get.js.map +1 -1
  36. package/dist/generators/events-route.d.ts +72 -0
  37. package/dist/generators/events-route.d.ts.map +1 -0
  38. package/dist/generators/events-route.js +234 -0
  39. package/dist/generators/events-route.js.map +1 -0
  40. package/dist/generators/index.d.ts +1 -0
  41. package/dist/generators/index.d.ts.map +1 -1
  42. package/dist/generators/index.js +2 -1
  43. package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
  44. package/dist/generators/mcp-runtime-template.js +11 -5
  45. package/dist/generators/mcp-runtime-template.js.map +1 -1
  46. package/dist/generators/mcp.d.ts +3 -0
  47. package/dist/generators/mcp.d.ts.map +1 -1
  48. package/dist/generators/mcp.js +18 -9
  49. package/dist/generators/mcp.js.map +1 -1
  50. package/dist/generators/rest.d.ts +7 -1
  51. package/dist/generators/rest.d.ts.map +1 -1
  52. package/dist/generators/rest.js +77 -13
  53. package/dist/generators/rest.js.map +1 -1
  54. package/dist/generators.js +2 -1
  55. package/dist/index.d.ts +1 -0
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +3 -1
  58. package/dist/manifest/generator.js.map +1 -1
  59. package/dist/manifest/manifest-loader.js.map +1 -1
  60. package/dist/manifest/static-manifest.d.ts.map +1 -1
  61. package/dist/manifest/static-manifest.js +7 -3
  62. package/dist/manifest/static-manifest.js.map +1 -1
  63. package/dist/manifest/store.js +1 -1
  64. package/dist/manifest/store.js.map +1 -1
  65. package/dist/manifest/test-manifest-stub.d.ts.map +1 -1
  66. package/dist/manifest/test-manifest-stub.js +1171 -90
  67. package/dist/manifest/test-manifest-stub.js.map +1 -1
  68. package/dist/manifest.json +9 -3
  69. package/dist/mcp-advisor/tools/preview-api-endpoints.d.ts +1 -0
  70. package/dist/mcp-advisor/tools/preview-api-endpoints.d.ts.map +1 -1
  71. package/dist/mcp-advisor/types.d.ts +4 -0
  72. package/dist/mcp-advisor/types.d.ts.map +1 -1
  73. package/dist/migrations/tracker.js +5 -4
  74. package/dist/migrations/tracker.js.map +1 -1
  75. package/dist/migrations/types.d.ts +1 -1
  76. package/dist/migrations/types.d.ts.map +1 -1
  77. package/dist/object.d.ts +17 -8
  78. package/dist/object.d.ts.map +1 -1
  79. package/dist/object.js +42 -26
  80. package/dist/object.js.map +1 -1
  81. package/dist/registry/class-registration.d.ts.map +1 -1
  82. package/dist/registry/class-registration.js +12 -0
  83. package/dist/registry/class-registration.js.map +1 -1
  84. package/dist/registry/shared-state.d.ts +2 -1
  85. package/dist/registry/shared-state.d.ts.map +1 -1
  86. package/dist/registry/shared-state.js.map +1 -1
  87. package/dist/registry/types.d.ts.map +1 -1
  88. package/dist/registry.d.ts.map +1 -1
  89. package/dist/registry.js +1 -1
  90. package/dist/registry.js.map +1 -1
  91. package/dist/runtime/client.d.ts.map +1 -1
  92. package/dist/runtime/client.js.map +1 -1
  93. package/dist/scanner/manifest-generator.js.map +1 -1
  94. package/dist/scanner/types.d.ts +7 -0
  95. package/dist/scanner/types.d.ts.map +1 -1
  96. package/dist/schema/ddl/base-strategy.js +1 -1
  97. package/dist/schema/ddl/base-strategy.js.map +1 -1
  98. package/dist/scripts/migrate-datetime-to-timestamp.ts +1 -1
  99. package/dist/signals/sanitizer.d.ts.map +1 -1
  100. package/dist/signals/sanitizer.js.map +1 -1
  101. package/dist/smrt-knowledge.json +8 -8
  102. package/dist/testing/index.d.ts +1 -0
  103. package/dist/testing/index.d.ts.map +1 -1
  104. package/dist/testing/index.js +2 -1
  105. package/dist/testing/notifications-stub.d.ts +24 -0
  106. package/dist/testing/notifications-stub.d.ts.map +1 -0
  107. package/dist/testing/notifications-stub.js +62 -0
  108. package/dist/testing/notifications-stub.js.map +1 -0
  109. package/dist/testing.js +2 -1
  110. package/dist/tools/tool-generator.js +1 -1
  111. package/dist/tools/tool-generator.js.map +1 -1
  112. package/dist/vite-plugin/changes-route.d.ts +14 -0
  113. package/dist/vite-plugin/changes-route.d.ts.map +1 -1
  114. package/dist/vite-plugin/changes-route.js +2 -2
  115. package/dist/vite-plugin/changes-route.js.map +1 -1
  116. package/dist/vite-plugin/events-route.d.ts +9 -0
  117. package/dist/vite-plugin/events-route.d.ts.map +1 -0
  118. package/dist/vite-plugin/events-route.js +155 -0
  119. package/dist/vite-plugin/events-route.js.map +1 -0
  120. package/dist/vite-plugin/index.d.ts +8 -0
  121. package/dist/vite-plugin/index.d.ts.map +1 -1
  122. package/dist/vite-plugin/index.js +8 -2
  123. package/dist/vite-plugin/index.js.map +1 -1
  124. package/dist/vite-plugin/sveltekit-generator.d.ts +8 -0
  125. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  126. package/dist/vite-plugin/sveltekit-generator.js +239 -38
  127. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  128. package/dist/vite-plugin/web-collections.js +1 -1
  129. package/dist/vite-plugin/web-collections.js.map +1 -1
  130. package/package.json +12 -12
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/scanner/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/scanner/types.ts"],"names":[],"mappings":"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"}
@@ -71,7 +71,7 @@ var BaseDDLStrategy = class {
71
71
  const constraints = [];
72
72
  if (!indexes || !Array.isArray(indexes)) return constraints;
73
73
  for (const index of indexes) {
74
- if (!index || !index.columns || !Array.isArray(index.columns)) continue;
74
+ if (!index?.columns || !Array.isArray(index.columns)) continue;
75
75
  if (index.unique && index.columns.length > 0) {
76
76
  const columns = index.columns.map((c) => quoteIdentifier(c)).join(", ");
77
77
  constraints.push(`UNIQUE(${columns})`);
@@ -1 +1 @@
1
- {"version":3,"file":"base-strategy.js","names":[],"sources":["../../../src/schema/ddl/base-strategy.ts"],"sourcesContent":["/**\n * Base DDL Strategy - Shared logic for all database engines\n *\n * Provides common DDL generation logic that engine-specific strategies\n * can override for their particular requirements.\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport {\n formatDefaultValue as formatDefaultValueShared,\n isSafeIdentifier,\n isSafeIdentifierPath,\n quoteIdentifier,\n quoteStringLiteral,\n} from '../sql-identifiers.js';\nimport type {\n ColumnDefinition,\n IndexDefinition,\n SchemaDefinition,\n SQLDataType,\n TriggerDefinition,\n} from '../types.js';\nimport type { DatabaseEngine, DDLStrategy } from './types.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Validate the column + JSON path used to build a JSON-path index expression.\n *\n * The path segment is embedded as a SQL string literal inside a dialect\n * function/operator (`json_extract(\"col\", '$.path')` / `\"col\"->>'path'`), and\n * the column as a delimited identifier. We escape both, but also reject paths\n * or columns that aren't simple (dotted) identifiers so a malformed `@meta`\n * field name can't smuggle structure into the expression even after escaping.\n * These names are developer-controlled build-time inputs, so an invalid one is\n * a programming error and throwing is the safest, loudest outcome.\n */\nfunction assertSafeJsonPathTarget(jsonColumn: string, path: string): void {\n if (!isSafeIdentifier(jsonColumn)) {\n throw new Error(\n `[DDL] Unsafe JSON-path index column \"${jsonColumn}\": must be a simple identifier`,\n );\n }\n if (!isSafeIdentifierPath(path)) {\n throw new Error(\n `[DDL] Unsafe JSON-path index path \"${path}\": must be a simple (dotted) identifier`,\n );\n }\n}\n\n/**\n * Abstract base class for DDL strategies\n *\n * Implements common DDL generation patterns. Engine-specific strategies\n * extend this class and override methods as needed.\n */\nexport abstract class BaseDDLStrategy implements DDLStrategy {\n abstract readonly engine: DatabaseEngine;\n\n /**\n * Generate CREATE TABLE statement\n */\n generateCreateTable(schema: SchemaDefinition): string {\n const { tableName, columns, indexes = [] } = schema;\n\n let sql = `CREATE TABLE IF NOT EXISTS ${quoteIdentifier(tableName)} (\\n`;\n\n // Generate column definitions\n const columnDefs: string[] = [];\n for (const [columnName, columnDef] of Object.entries(columns)) {\n columnDefs.push(this.generateColumnDefinition(columnName, columnDef));\n }\n\n // Add inline UNIQUE constraints for engines that require them\n if (this.requiresInlineUnique() && indexes.length > 0) {\n const uniqueConstraints = this.generateInlineUniqueConstraints(indexes);\n columnDefs.push(...uniqueConstraints);\n }\n\n sql += columnDefs.map((def) => ` ${def}`).join(',\\n');\n sql += '\\n);';\n\n return sql;\n }\n\n /**\n * Generate a single column definition\n *\n * Public so SchemaManager can reuse it for ALTER TABLE ADD COLUMN.\n */\n generateColumnDefinition(\n columnName: string,\n columnDef: ColumnDefinition,\n ): string {\n const parts: string[] = [\n quoteIdentifier(columnName),\n this.mapType(columnDef.type),\n ];\n\n // Primary key\n if (columnDef.primaryKey) {\n parts.push('PRIMARY KEY');\n }\n\n // NOT NULL (skip for primary key - it's implicit)\n if (columnDef.notNull && !columnDef.primaryKey) {\n parts.push('NOT NULL');\n }\n\n // UNIQUE (for single-column unique, not composite)\n // Skip if engine requires inline unique - those are handled separately\n if (columnDef.unique && !this.requiresInlineUnique()) {\n parts.push('UNIQUE');\n }\n\n // DEFAULT value\n if (columnDef.defaultValue !== undefined) {\n const formatted = this.formatDefaultValue(\n columnDef.defaultValue,\n columnDef.type,\n );\n parts.push(`DEFAULT ${formatted}`);\n }\n\n // CHECK constraint\n if (columnDef.check) {\n parts.push(`CHECK (${columnDef.check})`);\n }\n\n return parts.join(' ');\n }\n\n /**\n * Generate inline UNIQUE constraints for composite indexes\n * Used by engines that require inline UNIQUE for UPSERT to work (DuckDB)\n */\n protected generateInlineUniqueConstraints(\n indexes: IndexDefinition[],\n ): string[] {\n const constraints: string[] = [];\n\n if (!indexes || !Array.isArray(indexes)) {\n return constraints;\n }\n\n for (const index of indexes) {\n // Skip malformed index entries\n if (!index || !index.columns || !Array.isArray(index.columns)) {\n continue;\n }\n\n if (index.unique && index.columns.length > 0) {\n const columns = index.columns.map((c) => quoteIdentifier(c)).join(', ');\n constraints.push(`UNIQUE(${columns})`);\n }\n }\n\n return constraints;\n }\n\n /**\n * Generate CREATE INDEX statements\n * Override in engine-specific strategies if needed\n */\n generateIndexes(schema: SchemaDefinition): string[] {\n const { tableName, indexes = [] } = schema;\n const statements: string[] = [];\n\n if (!indexes || indexes.length === 0) {\n return statements;\n }\n\n for (const index of indexes) {\n // Narrow the jsonPath target up-front so the formatter call doesn't\n // need optional-chained args (and so the malformed-entry check has a\n // single source of truth).\n const jsonPath =\n index?.jsonPath?.column && index.jsonPath.path ? index.jsonPath : null;\n if (\n !index ||\n (!jsonPath &&\n (!index.columns ||\n !Array.isArray(index.columns) ||\n index.columns.length === 0))\n ) {\n logger.warn(`[DDL] Skipping malformed index: ${JSON.stringify(index)}`);\n continue;\n }\n\n // Skip UNIQUE indexes if engine requires them inline\n if (index.unique && this.requiresInlineUnique()) {\n continue;\n }\n\n const indexType = index.unique ? 'UNIQUE INDEX' : 'INDEX';\n\n // JSON-path indexes use a dialect-specific expression\n const target = jsonPath\n ? `(${this.formatJsonPathIndexExpression(\n jsonPath.column,\n jsonPath.path,\n )})`\n : index.columns.map((c) => quoteIdentifier(c)).join(', ');\n\n let sql = `CREATE ${indexType} IF NOT EXISTS ${quoteIdentifier(\n index.name,\n )} ON ${quoteIdentifier(tableName)} (${target})`;\n\n // Partial index condition\n if (index.where) {\n sql += ` WHERE ${index.where}`;\n }\n\n sql += ';';\n statements.push(sql);\n }\n\n return statements;\n }\n\n /**\n * Render the SQL expression used to index a JSON path inside a JSONB column.\n * Subclasses override for dialect-specific syntax.\n *\n * Default (ANSI-ish): `<jsonColumn>->>'<path>'` — works on Postgres.\n */\n protected formatJsonPathIndexExpression(\n jsonColumn: string,\n path: string,\n ): string {\n assertSafeJsonPathTarget(jsonColumn, path);\n return `${quoteIdentifier(jsonColumn)}->>${quoteStringLiteral(path)}`;\n }\n\n /**\n * Generate CREATE TRIGGER statements\n * Override in engine-specific strategies - DuckDB returns empty\n */\n generateTriggers(schema: SchemaDefinition): string[] {\n if (!this.supportsTriggers()) {\n return [];\n }\n\n const { tableName, triggers = [] } = schema;\n const statements: string[] = [];\n\n if (!triggers || triggers.length === 0) {\n return statements;\n }\n\n for (const trigger of triggers) {\n const sql = this.generateTriggerStatement(tableName, trigger);\n if (sql) {\n statements.push(sql);\n }\n }\n\n return statements;\n }\n\n /**\n * Generate a single trigger statement\n * Can be overridden for engine-specific trigger syntax\n */\n protected generateTriggerStatement(\n tableName: string,\n trigger: TriggerDefinition,\n ): string {\n // Default SQLite-style trigger syntax\n let sql = `CREATE TRIGGER IF NOT EXISTS ${quoteIdentifier(trigger.name)}\\n`;\n sql += `${trigger.when} ${trigger.event} ON ${quoteIdentifier(tableName)}\\n`;\n\n if (trigger.condition) {\n sql += `WHEN ${trigger.condition}\\n`;\n }\n\n sql += `BEGIN\\n${trigger.body}\\nEND;`;\n\n return sql;\n }\n\n /**\n * Map abstract SQL type to engine-specific type\n * Default implementation - override for engine-specific types\n */\n mapType(type: SQLDataType): string {\n // Default mapping works for most engines\n switch (type) {\n case 'TEXT':\n return 'TEXT';\n case 'INTEGER':\n return 'INTEGER';\n case 'REAL':\n return 'REAL';\n case 'BLOB':\n return 'BLOB';\n case 'BOOLEAN':\n return 'BOOLEAN';\n case 'JSON':\n return 'JSON';\n case 'TIMESTAMP':\n return 'TIMESTAMP';\n case 'UUID':\n // Fallback for engines without a native uuid type (e.g. SQLite):\n // store as TEXT. PostgreSQL/DuckDB override this with their native\n // uuid type. (R11)\n return 'TEXT';\n default:\n return 'TEXT';\n }\n }\n\n /**\n * Format default value for SQL.\n *\n * Delegates to the shared, injection-safe `formatDefaultValue`\n * (`schema/sql-identifiers.ts`) so every DDL path uses one set of rules:\n * an allowlist of SQL keyword/function defaults (not \"contains `(`\"),\n * type-driven literal quoting, and no folding of the string `\"null\"` into\n * the SQL NULL keyword. Boolean rendering is bridged through\n * `formatBooleanDefault` so engine overrides (SQLite → 0/1) still apply.\n */\n formatDefaultValue(value: unknown, type: SQLDataType): string {\n return formatDefaultValueShared(value, type, {\n booleanLiterals: [\n this.formatBooleanDefault(true),\n this.formatBooleanDefault(false),\n ],\n });\n }\n\n /**\n * Format boolean default\n * Override for engines that use INTEGER (SQLite)\n */\n protected formatBooleanDefault(value: boolean): string {\n return value ? 'TRUE' : 'FALSE';\n }\n\n /**\n * Whether this engine supports triggers\n * Override in DuckDB strategy to return false\n */\n supportsTriggers(): boolean {\n return true;\n }\n\n /**\n * Whether UNIQUE constraints must be inline for UPSERT\n * Override in DuckDB strategy to return true\n */\n requiresInlineUnique(): boolean {\n return false;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"base-strategy.js","names":[],"sources":["../../../src/schema/ddl/base-strategy.ts"],"sourcesContent":["/**\n * Base DDL Strategy - Shared logic for all database engines\n *\n * Provides common DDL generation logic that engine-specific strategies\n * can override for their particular requirements.\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport {\n formatDefaultValue as formatDefaultValueShared,\n isSafeIdentifier,\n isSafeIdentifierPath,\n quoteIdentifier,\n quoteStringLiteral,\n} from '../sql-identifiers.js';\nimport type {\n ColumnDefinition,\n IndexDefinition,\n SchemaDefinition,\n SQLDataType,\n TriggerDefinition,\n} from '../types.js';\nimport type { DatabaseEngine, DDLStrategy } from './types.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Validate the column + JSON path used to build a JSON-path index expression.\n *\n * The path segment is embedded as a SQL string literal inside a dialect\n * function/operator (`json_extract(\"col\", '$.path')` / `\"col\"->>'path'`), and\n * the column as a delimited identifier. We escape both, but also reject paths\n * or columns that aren't simple (dotted) identifiers so a malformed `@meta`\n * field name can't smuggle structure into the expression even after escaping.\n * These names are developer-controlled build-time inputs, so an invalid one is\n * a programming error and throwing is the safest, loudest outcome.\n */\nfunction assertSafeJsonPathTarget(jsonColumn: string, path: string): void {\n if (!isSafeIdentifier(jsonColumn)) {\n throw new Error(\n `[DDL] Unsafe JSON-path index column \"${jsonColumn}\": must be a simple identifier`,\n );\n }\n if (!isSafeIdentifierPath(path)) {\n throw new Error(\n `[DDL] Unsafe JSON-path index path \"${path}\": must be a simple (dotted) identifier`,\n );\n }\n}\n\n/**\n * Abstract base class for DDL strategies\n *\n * Implements common DDL generation patterns. Engine-specific strategies\n * extend this class and override methods as needed.\n */\nexport abstract class BaseDDLStrategy implements DDLStrategy {\n abstract readonly engine: DatabaseEngine;\n\n /**\n * Generate CREATE TABLE statement\n */\n generateCreateTable(schema: SchemaDefinition): string {\n const { tableName, columns, indexes = [] } = schema;\n\n let sql = `CREATE TABLE IF NOT EXISTS ${quoteIdentifier(tableName)} (\\n`;\n\n // Generate column definitions\n const columnDefs: string[] = [];\n for (const [columnName, columnDef] of Object.entries(columns)) {\n columnDefs.push(this.generateColumnDefinition(columnName, columnDef));\n }\n\n // Add inline UNIQUE constraints for engines that require them\n if (this.requiresInlineUnique() && indexes.length > 0) {\n const uniqueConstraints = this.generateInlineUniqueConstraints(indexes);\n columnDefs.push(...uniqueConstraints);\n }\n\n sql += columnDefs.map((def) => ` ${def}`).join(',\\n');\n sql += '\\n);';\n\n return sql;\n }\n\n /**\n * Generate a single column definition\n *\n * Public so SchemaManager can reuse it for ALTER TABLE ADD COLUMN.\n */\n generateColumnDefinition(\n columnName: string,\n columnDef: ColumnDefinition,\n ): string {\n const parts: string[] = [\n quoteIdentifier(columnName),\n this.mapType(columnDef.type),\n ];\n\n // Primary key\n if (columnDef.primaryKey) {\n parts.push('PRIMARY KEY');\n }\n\n // NOT NULL (skip for primary key - it's implicit)\n if (columnDef.notNull && !columnDef.primaryKey) {\n parts.push('NOT NULL');\n }\n\n // UNIQUE (for single-column unique, not composite)\n // Skip if engine requires inline unique - those are handled separately\n if (columnDef.unique && !this.requiresInlineUnique()) {\n parts.push('UNIQUE');\n }\n\n // DEFAULT value\n if (columnDef.defaultValue !== undefined) {\n const formatted = this.formatDefaultValue(\n columnDef.defaultValue,\n columnDef.type,\n );\n parts.push(`DEFAULT ${formatted}`);\n }\n\n // CHECK constraint\n if (columnDef.check) {\n parts.push(`CHECK (${columnDef.check})`);\n }\n\n return parts.join(' ');\n }\n\n /**\n * Generate inline UNIQUE constraints for composite indexes\n * Used by engines that require inline UNIQUE for UPSERT to work (DuckDB)\n */\n protected generateInlineUniqueConstraints(\n indexes: IndexDefinition[],\n ): string[] {\n const constraints: string[] = [];\n\n if (!indexes || !Array.isArray(indexes)) {\n return constraints;\n }\n\n for (const index of indexes) {\n // Skip malformed index entries\n if (!index?.columns || !Array.isArray(index.columns)) {\n continue;\n }\n\n if (index.unique && index.columns.length > 0) {\n const columns = index.columns.map((c) => quoteIdentifier(c)).join(', ');\n constraints.push(`UNIQUE(${columns})`);\n }\n }\n\n return constraints;\n }\n\n /**\n * Generate CREATE INDEX statements\n * Override in engine-specific strategies if needed\n */\n generateIndexes(schema: SchemaDefinition): string[] {\n const { tableName, indexes = [] } = schema;\n const statements: string[] = [];\n\n if (!indexes || indexes.length === 0) {\n return statements;\n }\n\n for (const index of indexes) {\n // Narrow the jsonPath target up-front so the formatter call doesn't\n // need optional-chained args (and so the malformed-entry check has a\n // single source of truth).\n const jsonPath =\n index?.jsonPath?.column && index.jsonPath.path ? index.jsonPath : null;\n if (\n !index ||\n (!jsonPath &&\n (!index.columns ||\n !Array.isArray(index.columns) ||\n index.columns.length === 0))\n ) {\n logger.warn(`[DDL] Skipping malformed index: ${JSON.stringify(index)}`);\n continue;\n }\n\n // Skip UNIQUE indexes if engine requires them inline\n if (index.unique && this.requiresInlineUnique()) {\n continue;\n }\n\n const indexType = index.unique ? 'UNIQUE INDEX' : 'INDEX';\n\n // JSON-path indexes use a dialect-specific expression\n const target = jsonPath\n ? `(${this.formatJsonPathIndexExpression(\n jsonPath.column,\n jsonPath.path,\n )})`\n : index.columns.map((c) => quoteIdentifier(c)).join(', ');\n\n let sql = `CREATE ${indexType} IF NOT EXISTS ${quoteIdentifier(\n index.name,\n )} ON ${quoteIdentifier(tableName)} (${target})`;\n\n // Partial index condition\n if (index.where) {\n sql += ` WHERE ${index.where}`;\n }\n\n sql += ';';\n statements.push(sql);\n }\n\n return statements;\n }\n\n /**\n * Render the SQL expression used to index a JSON path inside a JSONB column.\n * Subclasses override for dialect-specific syntax.\n *\n * Default (ANSI-ish): `<jsonColumn>->>'<path>'` — works on Postgres.\n */\n protected formatJsonPathIndexExpression(\n jsonColumn: string,\n path: string,\n ): string {\n assertSafeJsonPathTarget(jsonColumn, path);\n return `${quoteIdentifier(jsonColumn)}->>${quoteStringLiteral(path)}`;\n }\n\n /**\n * Generate CREATE TRIGGER statements\n * Override in engine-specific strategies - DuckDB returns empty\n */\n generateTriggers(schema: SchemaDefinition): string[] {\n if (!this.supportsTriggers()) {\n return [];\n }\n\n const { tableName, triggers = [] } = schema;\n const statements: string[] = [];\n\n if (!triggers || triggers.length === 0) {\n return statements;\n }\n\n for (const trigger of triggers) {\n const sql = this.generateTriggerStatement(tableName, trigger);\n if (sql) {\n statements.push(sql);\n }\n }\n\n return statements;\n }\n\n /**\n * Generate a single trigger statement\n * Can be overridden for engine-specific trigger syntax\n */\n protected generateTriggerStatement(\n tableName: string,\n trigger: TriggerDefinition,\n ): string {\n // Default SQLite-style trigger syntax\n let sql = `CREATE TRIGGER IF NOT EXISTS ${quoteIdentifier(trigger.name)}\\n`;\n sql += `${trigger.when} ${trigger.event} ON ${quoteIdentifier(tableName)}\\n`;\n\n if (trigger.condition) {\n sql += `WHEN ${trigger.condition}\\n`;\n }\n\n sql += `BEGIN\\n${trigger.body}\\nEND;`;\n\n return sql;\n }\n\n /**\n * Map abstract SQL type to engine-specific type\n * Default implementation - override for engine-specific types\n */\n mapType(type: SQLDataType): string {\n // Default mapping works for most engines\n switch (type) {\n case 'TEXT':\n return 'TEXT';\n case 'INTEGER':\n return 'INTEGER';\n case 'REAL':\n return 'REAL';\n case 'BLOB':\n return 'BLOB';\n case 'BOOLEAN':\n return 'BOOLEAN';\n case 'JSON':\n return 'JSON';\n case 'TIMESTAMP':\n return 'TIMESTAMP';\n case 'UUID':\n // Fallback for engines without a native uuid type (e.g. SQLite):\n // store as TEXT. PostgreSQL/DuckDB override this with their native\n // uuid type. (R11)\n return 'TEXT';\n default:\n return 'TEXT';\n }\n }\n\n /**\n * Format default value for SQL.\n *\n * Delegates to the shared, injection-safe `formatDefaultValue`\n * (`schema/sql-identifiers.ts`) so every DDL path uses one set of rules:\n * an allowlist of SQL keyword/function defaults (not \"contains `(`\"),\n * type-driven literal quoting, and no folding of the string `\"null\"` into\n * the SQL NULL keyword. Boolean rendering is bridged through\n * `formatBooleanDefault` so engine overrides (SQLite → 0/1) still apply.\n */\n formatDefaultValue(value: unknown, type: SQLDataType): string {\n return formatDefaultValueShared(value, type, {\n booleanLiterals: [\n this.formatBooleanDefault(true),\n this.formatBooleanDefault(false),\n ],\n });\n }\n\n /**\n * Format boolean default\n * Override for engines that use INTEGER (SQLite)\n */\n protected formatBooleanDefault(value: boolean): string {\n return value ? 'TRUE' : 'FALSE';\n }\n\n /**\n * Whether this engine supports triggers\n * Override in DuckDB strategy to return false\n */\n supportsTriggers(): boolean {\n return true;\n }\n\n /**\n * Whether UNIQUE constraints must be inline for UPSERT\n * Override in DuckDB strategy to return true\n */\n requiresInlineUnique(): boolean {\n return false;\n }\n}\n"],"mappings":"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"}
@@ -307,4 +307,4 @@ if (import.meta.url === `file://${process.argv[1]}`) {
307
307
  main();
308
308
  }
309
309
 
310
- export { migrate, type MigrationOptions };
310
+ export { type MigrationOptions, migrate };
@@ -1 +1 @@
1
- {"version":3,"file":"sanitizer.d.ts","sourceRoot":"","sources":["../../src/signals/sanitizer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AAExD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAEtB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;IAEpD;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAgCD;;;;;GAKG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAA+B;gBAEjC,MAAM,GAAE,kBAAuB;IAS3C;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAiBvB;;OAEG;IACH,OAAO,CAAC,aAAa;IAgDrB;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;CA+BjC"}
1
+ {"version":3,"file":"sanitizer.d.ts","sourceRoot":"","sources":["../../src/signals/sanitizer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AAExD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAEtB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;IAEpD;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAgCD;;;;;GAKG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAA+B;gBAEjC,MAAM,GAAE,kBAAuB;IAS3C;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAiBvB;;OAEG;IACH,OAAO,CAAC,aAAa;IAgDrB;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;CAiCjC"}
@@ -1 +1 @@
1
- {"version":3,"file":"sanitizer.js","names":[],"sources":["../../src/signals/sanitizer.ts"],"sourcesContent":["/**\n * Signal payload sanitization for security\n *\n * Prevents sensitive data (passwords, tokens, PII) from being leaked\n * into logs, metrics, or other signal outputs.\n */\n\nimport type { Signal } from '@happyvertical/smrt-types';\n\n/**\n * Sanitization configuration\n */\nexport interface SanitizationConfig {\n /**\n * Keys to redact from signal payloads\n * Default: common sensitive fields\n */\n redactKeys?: string[];\n\n /**\n * Custom replacer function for sanitization\n * Return undefined to redact the value entirely\n */\n replacer?: (key: string, value: unknown) => unknown;\n\n /**\n * Replacement value for redacted fields\n * Default: '[REDACTED]'\n */\n redactedValue?: string;\n\n /**\n * Maximum number of stack trace lines to include in sanitized errors\n * Default: 10\n */\n maxStackLines?: number;\n}\n\n/**\n * Default sensitive keys to redact\n */\nconst DEFAULT_REDACT_KEYS = [\n 'password',\n 'passwd',\n 'pwd',\n 'secret',\n 'token',\n 'apiKey',\n 'api_key',\n 'accessToken',\n 'access_token',\n 'refreshToken',\n 'refresh_token',\n 'privateKey',\n 'private_key',\n 'authToken',\n 'auth_token',\n 'bearerToken',\n 'bearer_token',\n 'sessionId',\n 'session_id',\n 'ssn',\n 'creditCard',\n 'credit_card',\n 'cvv',\n 'pin',\n];\n\n/**\n * Signal sanitizer\n *\n * Removes or redacts sensitive data from signal payloads before\n * they are processed by adapters.\n */\nexport class SignalSanitizer {\n private config: Required<SanitizationConfig>;\n\n constructor(config: SanitizationConfig = {}) {\n this.config = {\n redactKeys: config.redactKeys ?? DEFAULT_REDACT_KEYS,\n replacer: config.replacer ?? this.defaultReplacer.bind(this),\n redactedValue: config.redactedValue ?? '[REDACTED]',\n maxStackLines: config.maxStackLines ?? 10,\n };\n }\n\n /**\n * Default replacer function\n *\n * Redacts sensitive keys and truncates long strings\n */\n private defaultReplacer(key: string, value: unknown): unknown {\n // Check if key should be redacted\n const lowerKey = key.toLowerCase();\n if (\n this.config.redactKeys.some((k) => lowerKey.includes(k.toLowerCase()))\n ) {\n return this.config.redactedValue;\n }\n\n // Truncate very long strings (potential data dumps)\n if (typeof value === 'string' && value.length > 1000) {\n return `${value.substring(0, 1000)}... [TRUNCATED]`;\n }\n\n return value;\n }\n\n /**\n * Sanitize a value using the configured replacer\n */\n private sanitizeValue(value: unknown, seen = new WeakSet()): unknown {\n // Handle null/undefined\n if (value == null) {\n return value;\n }\n\n // Handle primitives\n if (typeof value !== 'object') {\n return value;\n }\n\n // Prevent circular reference infinite loops\n if (seen.has(value)) {\n return '[CIRCULAR]';\n }\n seen.add(value);\n\n // Handle arrays\n if (Array.isArray(value)) {\n return value.map((item) => this.sanitizeValue(item, seen));\n }\n\n // Handle Error objects specially\n if (value instanceof Error) {\n return {\n message: value.message,\n name: value.name,\n stack: value.stack\n ? value.stack\n .split('\\n')\n .slice(0, this.config.maxStackLines)\n .join('\\n')\n : undefined,\n };\n }\n\n // Handle regular objects\n const sanitized: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(value)) {\n const replacedValue = this.config.replacer(key, val);\n if (replacedValue !== undefined) {\n sanitized[key] = this.sanitizeValue(replacedValue, seen);\n }\n }\n\n return sanitized;\n }\n\n /**\n * Sanitize a signal payload\n *\n * @param signal - Signal to sanitize\n * @returns Sanitized signal (new object, doesn't mutate original)\n */\n sanitize(signal: Signal): Signal {\n return {\n id: signal.id,\n objectId: signal.objectId,\n className: signal.className,\n method: signal.method,\n type: signal.type,\n timestamp: signal.timestamp,\n ...(signal.step && { step: signal.step }),\n ...(signal.duration !== undefined && { duration: signal.duration }),\n // `signal.args` is an array, so `sanitizeValue` always returns an array\n // (the `Array.isArray` branch maps element-wise).\n ...(signal.args && { args: this.sanitizeValue(signal.args) as unknown[] }),\n ...(signal.result !== undefined\n ? { result: this.sanitizeValue(signal.result) }\n : {}),\n // `sanitizeValue` flattens an `Error` into an Error-shaped plain object\n // (`{ message, name, stack }`); the Signal contract still types it as\n // `Error`, so retain that surface here.\n ...(signal.error && {\n error: this.sanitizeValue(signal.error) as Error,\n }),\n // Sanitizing an object yields a `Record<string, unknown>`.\n ...(signal.metadata && {\n metadata: this.sanitizeValue(signal.metadata) as Record<\n string,\n unknown\n >,\n }),\n };\n }\n}\n"],"mappings":";;;;AAyCA,IAAM,sBAAsB;CAC1B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;AAQA,IAAa,kBAAb,MAA6B;CAC3B;CAEA,YAAY,SAA6B,CAAC,GAAG;EAC3C,KAAK,SAAS;GACZ,YAAY,OAAO,cAAc;GACjC,UAAU,OAAO,YAAY,KAAK,gBAAgB,KAAK,IAAI;GAC3D,eAAe,OAAO,iBAAiB;GACvC,eAAe,OAAO,iBAAiB;EACzC;CACF;;;;;;CAOA,gBAAwB,KAAa,OAAyB;EAE5D,MAAM,WAAW,IAAI,YAAY;EACjC,IACE,KAAK,OAAO,WAAW,MAAM,MAAM,SAAS,SAAS,EAAE,YAAY,CAAC,CAAC,GAErE,OAAO,KAAK,OAAO;EAIrB,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,KAC9C,OAAO,GAAG,MAAM,UAAU,GAAG,GAAI,EAAE;EAGrC,OAAO;CACT;;;;CAKA,cAAsB,OAAgB,uBAAO,IAAI,QAAQ,GAAY;EAEnE,IAAI,SAAS,MACX,OAAO;EAIT,IAAI,OAAO,UAAU,UACnB,OAAO;EAIT,IAAI,KAAK,IAAI,KAAK,GAChB,OAAO;EAET,KAAK,IAAI,KAAK;EAGd,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,KAAK,SAAS,KAAK,cAAc,MAAM,IAAI,CAAC;EAI3D,IAAI,iBAAiB,OACnB,OAAO;GACL,SAAS,MAAM;GACf,MAAM,MAAM;GACZ,OAAO,MAAM,QACT,MAAM,MACH,MAAM,IAAI,CAAC,CACX,MAAM,GAAG,KAAK,OAAO,aAAa,CAAC,CACnC,KAAK,IAAI,IACZ,KAAA;EACN;EAIF,MAAM,YAAqC,CAAC;EAC5C,KAAK,MAAM,CAAC,KAAK,QAAQ,OAAO,QAAQ,KAAK,GAAG;GAC9C,MAAM,gBAAgB,KAAK,OAAO,SAAS,KAAK,GAAG;GACnD,IAAI,kBAAkB,KAAA,GACpB,UAAU,OAAO,KAAK,cAAc,eAAe,IAAI;EAE3D;EAEA,OAAO;CACT;;;;;;;CAQA,SAAS,QAAwB;EAC/B,OAAO;GACL,IAAI,OAAO;GACX,UAAU,OAAO;GACjB,WAAW,OAAO;GAClB,QAAQ,OAAO;GACf,MAAM,OAAO;GACb,WAAW,OAAO;GAClB,GAAI,OAAO,QAAQ,EAAE,MAAM,OAAO,KAAK;GACvC,GAAI,OAAO,aAAa,KAAA,KAAa,EAAE,UAAU,OAAO,SAAS;GAGjE,GAAI,OAAO,QAAQ,EAAE,MAAM,KAAK,cAAc,OAAO,IAAI,EAAe;GACxE,GAAI,OAAO,WAAW,KAAA,IAClB,EAAE,QAAQ,KAAK,cAAc,OAAO,MAAM,EAAE,IAC5C,CAAC;GAIL,GAAI,OAAO,SAAS,EAClB,OAAO,KAAK,cAAc,OAAO,KAAK,EACxC;GAEA,GAAI,OAAO,YAAY,EACrB,UAAU,KAAK,cAAc,OAAO,QAAQ,EAI9C;EACF;CACF;AACF"}
1
+ {"version":3,"file":"sanitizer.js","names":[],"sources":["../../src/signals/sanitizer.ts"],"sourcesContent":["/**\n * Signal payload sanitization for security\n *\n * Prevents sensitive data (passwords, tokens, PII) from being leaked\n * into logs, metrics, or other signal outputs.\n */\n\nimport type { Signal } from '@happyvertical/smrt-types';\n\n/**\n * Sanitization configuration\n */\nexport interface SanitizationConfig {\n /**\n * Keys to redact from signal payloads\n * Default: common sensitive fields\n */\n redactKeys?: string[];\n\n /**\n * Custom replacer function for sanitization\n * Return undefined to redact the value entirely\n */\n replacer?: (key: string, value: unknown) => unknown;\n\n /**\n * Replacement value for redacted fields\n * Default: '[REDACTED]'\n */\n redactedValue?: string;\n\n /**\n * Maximum number of stack trace lines to include in sanitized errors\n * Default: 10\n */\n maxStackLines?: number;\n}\n\n/**\n * Default sensitive keys to redact\n */\nconst DEFAULT_REDACT_KEYS = [\n 'password',\n 'passwd',\n 'pwd',\n 'secret',\n 'token',\n 'apiKey',\n 'api_key',\n 'accessToken',\n 'access_token',\n 'refreshToken',\n 'refresh_token',\n 'privateKey',\n 'private_key',\n 'authToken',\n 'auth_token',\n 'bearerToken',\n 'bearer_token',\n 'sessionId',\n 'session_id',\n 'ssn',\n 'creditCard',\n 'credit_card',\n 'cvv',\n 'pin',\n];\n\n/**\n * Signal sanitizer\n *\n * Removes or redacts sensitive data from signal payloads before\n * they are processed by adapters.\n */\nexport class SignalSanitizer {\n private config: Required<SanitizationConfig>;\n\n constructor(config: SanitizationConfig = {}) {\n this.config = {\n redactKeys: config.redactKeys ?? DEFAULT_REDACT_KEYS,\n replacer: config.replacer ?? this.defaultReplacer.bind(this),\n redactedValue: config.redactedValue ?? '[REDACTED]',\n maxStackLines: config.maxStackLines ?? 10,\n };\n }\n\n /**\n * Default replacer function\n *\n * Redacts sensitive keys and truncates long strings\n */\n private defaultReplacer(key: string, value: unknown): unknown {\n // Check if key should be redacted\n const lowerKey = key.toLowerCase();\n if (\n this.config.redactKeys.some((k) => lowerKey.includes(k.toLowerCase()))\n ) {\n return this.config.redactedValue;\n }\n\n // Truncate very long strings (potential data dumps)\n if (typeof value === 'string' && value.length > 1000) {\n return `${value.substring(0, 1000)}... [TRUNCATED]`;\n }\n\n return value;\n }\n\n /**\n * Sanitize a value using the configured replacer\n */\n private sanitizeValue(value: unknown, seen = new WeakSet()): unknown {\n // Handle null/undefined\n if (value == null) {\n return value;\n }\n\n // Handle primitives\n if (typeof value !== 'object') {\n return value;\n }\n\n // Prevent circular reference infinite loops\n if (seen.has(value)) {\n return '[CIRCULAR]';\n }\n seen.add(value);\n\n // Handle arrays\n if (Array.isArray(value)) {\n return value.map((item) => this.sanitizeValue(item, seen));\n }\n\n // Handle Error objects specially\n if (value instanceof Error) {\n return {\n message: value.message,\n name: value.name,\n stack: value.stack\n ? value.stack\n .split('\\n')\n .slice(0, this.config.maxStackLines)\n .join('\\n')\n : undefined,\n };\n }\n\n // Handle regular objects\n const sanitized: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(value)) {\n const replacedValue = this.config.replacer(key, val);\n if (replacedValue !== undefined) {\n sanitized[key] = this.sanitizeValue(replacedValue, seen);\n }\n }\n\n return sanitized;\n }\n\n /**\n * Sanitize a signal payload\n *\n * @param signal - Signal to sanitize\n * @returns Sanitized signal (new object, doesn't mutate original)\n */\n sanitize(signal: Signal): Signal {\n return {\n id: signal.id,\n objectId: signal.objectId,\n className: signal.className,\n method: signal.method,\n type: signal.type,\n timestamp: signal.timestamp,\n ...(signal.step && { step: signal.step }),\n ...(signal.duration !== undefined && { duration: signal.duration }),\n // `signal.args` is an array, so `sanitizeValue` always returns an array\n // (the `Array.isArray` branch maps element-wise).\n ...(signal.args && {\n args: this.sanitizeValue(signal.args) as unknown[],\n }),\n ...(signal.result !== undefined\n ? { result: this.sanitizeValue(signal.result) }\n : {}),\n // `sanitizeValue` flattens an `Error` into an Error-shaped plain object\n // (`{ message, name, stack }`); the Signal contract still types it as\n // `Error`, so retain that surface here.\n ...(signal.error && {\n error: this.sanitizeValue(signal.error) as Error,\n }),\n // Sanitizing an object yields a `Record<string, unknown>`.\n ...(signal.metadata && {\n metadata: this.sanitizeValue(signal.metadata) as Record<\n string,\n unknown\n >,\n }),\n };\n }\n}\n"],"mappings":";;;;AAyCA,IAAM,sBAAsB;CAC1B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;AAQA,IAAa,kBAAb,MAA6B;CAC3B;CAEA,YAAY,SAA6B,CAAC,GAAG;EAC3C,KAAK,SAAS;GACZ,YAAY,OAAO,cAAc;GACjC,UAAU,OAAO,YAAY,KAAK,gBAAgB,KAAK,IAAI;GAC3D,eAAe,OAAO,iBAAiB;GACvC,eAAe,OAAO,iBAAiB;EACzC;CACF;;;;;;CAOA,gBAAwB,KAAa,OAAyB;EAE5D,MAAM,WAAW,IAAI,YAAY;EACjC,IACE,KAAK,OAAO,WAAW,MAAM,MAAM,SAAS,SAAS,EAAE,YAAY,CAAC,CAAC,GAErE,OAAO,KAAK,OAAO;EAIrB,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,KAC9C,OAAO,GAAG,MAAM,UAAU,GAAG,GAAI,EAAE;EAGrC,OAAO;CACT;;;;CAKA,cAAsB,OAAgB,uBAAO,IAAI,QAAQ,GAAY;EAEnE,IAAI,SAAS,MACX,OAAO;EAIT,IAAI,OAAO,UAAU,UACnB,OAAO;EAIT,IAAI,KAAK,IAAI,KAAK,GAChB,OAAO;EAET,KAAK,IAAI,KAAK;EAGd,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,KAAK,SAAS,KAAK,cAAc,MAAM,IAAI,CAAC;EAI3D,IAAI,iBAAiB,OACnB,OAAO;GACL,SAAS,MAAM;GACf,MAAM,MAAM;GACZ,OAAO,MAAM,QACT,MAAM,MACH,MAAM,IAAI,CAAC,CACX,MAAM,GAAG,KAAK,OAAO,aAAa,CAAC,CACnC,KAAK,IAAI,IACZ,KAAA;EACN;EAIF,MAAM,YAAqC,CAAC;EAC5C,KAAK,MAAM,CAAC,KAAK,QAAQ,OAAO,QAAQ,KAAK,GAAG;GAC9C,MAAM,gBAAgB,KAAK,OAAO,SAAS,KAAK,GAAG;GACnD,IAAI,kBAAkB,KAAA,GACpB,UAAU,OAAO,KAAK,cAAc,eAAe,IAAI;EAE3D;EAEA,OAAO;CACT;;;;;;;CAQA,SAAS,QAAwB;EAC/B,OAAO;GACL,IAAI,OAAO;GACX,UAAU,OAAO;GACjB,WAAW,OAAO;GAClB,QAAQ,OAAO;GACf,MAAM,OAAO;GACb,WAAW,OAAO;GAClB,GAAI,OAAO,QAAQ,EAAE,MAAM,OAAO,KAAK;GACvC,GAAI,OAAO,aAAa,KAAA,KAAa,EAAE,UAAU,OAAO,SAAS;GAGjE,GAAI,OAAO,QAAQ,EACjB,MAAM,KAAK,cAAc,OAAO,IAAI,EACtC;GACA,GAAI,OAAO,WAAW,KAAA,IAClB,EAAE,QAAQ,KAAK,cAAc,OAAO,MAAM,EAAE,IAC5C,CAAC;GAIL,GAAI,OAAO,SAAS,EAClB,OAAO,KAAK,cAAc,OAAO,KAAK,EACxC;GAEA,GAAI,OAAO,YAAY,EACrB,UAAU,KAAK,cAAc,OAAO,QAAQ,EAI9C;EACF;CACF;AACF"}
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-07-04T00:27:33.033Z",
3
+ "generatedAt": "2026-07-05T14:50:17.572Z",
4
4
  "packageName": "@happyvertical/smrt-core",
5
- "packageVersion": "0.38.2",
5
+ "packageVersion": "0.38.4",
6
6
  "sourceManifestPath": "dist/manifest.json",
7
7
  "agentDocPath": "AGENTS.md",
8
8
  "sourceHashes": {
9
- "manifest": "04a22b8c3aee1ce01dea3a19946ff914fc2d48967f76f90b2121782e07b914d3",
10
- "packageJson": "f20901240562e66fdc8cc2cd710c84155acfa8a0514f45ecac834211fd25f9de",
11
- "agents": "fbdf4b7a3061eae52b8473014cf9ec75c65d175714e113a365120e364b8e3901"
9
+ "manifest": "97274cbab5dfed317ed4f84d9c1520c93b27c0e7e4cafdcbe64a2ff17905d8f5",
10
+ "packageJson": "fc46b7062c2ee6a126ef794e3e2d042f060812706fde2787d10f59e29b0ea325",
11
+ "agents": "3d4ecc60503af2e37272cfc4ea8315344a84b2c74c10f5c6a83cac1dcd67e0ef"
12
12
  },
13
13
  "exports": [
14
14
  ".",
@@ -53,7 +53,7 @@
53
53
  "fast-glob": "3.3.3",
54
54
  "minimatch": "10.2.5",
55
55
  "pluralize": "^8.0.0",
56
- "tsx": "^4.22.4",
56
+ "tsx": "^4.23.0",
57
57
  "typescript": "^5.9.3",
58
58
  "yaml": "^2.9.0",
59
59
  "@faker-js/faker": "^10.5.0",
@@ -61,7 +61,7 @@
61
61
  "@types/node": "24.13.2",
62
62
  "@types/pluralize": "^0.0.33",
63
63
  "@xenova/transformers": "^2.17.2",
64
- "vite": "8.1.2",
64
+ "vite": "8.1.3",
65
65
  "vite-plugin-dts": "4.5.4",
66
66
  "vitest": "^4.1.9"
67
67
  },
@@ -323,5 +323,5 @@
323
323
  "polymorphicAssociations": 1,
324
324
  "uuidColumns": 3
325
325
  },
326
- "agentDoc": "# @happyvertical/smrt-core\n\nORM, code generation, AI integration, and the DispatchBus. Everything else builds on this.\n\n## Key Classes\n\n| Class | File | Purpose |\n|-------|------|---------|\n| SmrtObject | `src/object.ts` | Base persistent object — save, delete, is(), do(), loadFromId/Slug |\n| SmrtCollection | `src/collection.ts` | CRUD collection — list, get, create, delete, getOrUpsert |\n| ObjectRegistry | `src/registry.ts` | Global singleton (globalThis) — class metadata, fields, STI chains, manifests |\n| DispatchBus | `src/dispatch/bus.ts` | Inter-agent messaging — emit, subscribe (persistent), process |\n| GlobalInterceptors | `src/interceptors.ts` | Plugin system — beforeList/Get/Save/Delete hooks (used by tenancy) |\n\n## SmrtObject Lifecycle\n\n`constructor(options)` → `initialize()` → ready for `save()`/`delete()`/`loadFromId()`\n\n- `initialize()`: loads field initializers, applies option values (options override initializers), loads from DB if id/slug provided\n- `save()`: upsert with STI validation, interceptor execution, auto-embeddings. Persisted objects (`isPersisted` — set by DB hydration and successful saves) upsert on `['id']` so natural-key edits (e.g. slug renames) update in place; new objects upsert on the natural-key conflict columns for ingestion-style dedup (#1472)\n- `is(criteria)` / `do(instructions)` / `describe()`: AI operations via function calling. They inject the object's own `toPublicJSON()` (sensitive fields stripped) as a \"content body\" so the model reasons over the instance. Options: `includeData: false` skips injection (for callers that already curate the relevant fields into the instruction); `maxDataLength` overrides the truncation budget. Neither key is forwarded to `ai.message()`. (#1567)\n- `getSlug()`: auto-generates from name → title → label → id\n- `loadRelated(fieldName)`: lazy-loads relationships (cached in `_loadedRelationships` Map)\n\n## SmrtCollection Query\n\n```typescript\nawait collection.list({\n where: { status: 'active', price: { op: '>', value: 10 } },\n limit: 50, offset: 0, orderBy: 'created_at DESC'\n});\n```\n\n**WHERE operators**: `=`, `>`, `<`, `>=`, `<=`, `!=`, `in`, `not in`, `like`, `is null`, `is not null`. Arrays auto-detect `IN`. Dot notation for JSON paths: `metadata.userId`.\n\nSTI child collections auto-filter by `_meta_type`.\n\n## @smrt() Decorator Options\n\nKey options: `tableName`, `tableStrategy` ('cti'|'sti'), `conflictColumns`, `api`/`mcp`/`cli` (generation config), `ai` (callable methods), `hooks` (beforeSave/afterSave/beforeDelete/afterDelete), `embeddings` (auto-generate), `tenantScoped`, `agent`.\n\nRegistration sets `SMRT_TABLE_NAME` static property (survives minification).\n\n## Domain Knowledge Artifacts\n\n`smrtPlugin()` writes runtime manifests and agent/developer knowledge artifacts:\n\n- local dev/build: `.smrt/manifest.json` and `.smrt/smrt-knowledge.json`\n- package build: `dist/manifest.json` and `dist/smrt-knowledge.json`\n\nKeep `manifest.json` runtime-focused. `smrt-knowledge.json` is the deterministic\nagent contract for downstream review and architecture tools.\n\nConfig precedence for knowledge is defaults → top-level `knowledge` in\n`smrt.config.ts` → `packages[packageName].knowledge` → plugin option →\nobject-level `@smrt({ knowledge })`.\n\nObject-level `knowledge: false` excludes an object from authored context only;\nit must not change runtime manifest registration. Use\n`knowledge: { tags, summary, risks }` for review-sensitive domain objects.\n\nHTTP knowledge routes are disabled by default. If `knowledge.api.enabled` is\ntrue, generated SvelteKit routes must stay GET-only and guarded by dev mode or\nadmin auth.\n\n## DispatchBus\n\n- `emit(signalType, payload, metadata)` → creates persistent Dispatch record\n- `on(pattern, handler)` → in-memory handler (immediate)\n- `subscribe({ signalType, subscriber })` → persistent subscription (survives restarts)\n- `process(subscriberName, handler)` → process pending dispatches\n- Wildcards: `campaign.*` matches `campaign.completed` (single segment only)\n- Tables: `_smrt_dispatch`, `_smrt_dispatch_subscriptions`\n- Status: `pending → processing → completed` (or `failed`)\n\n## Change Feed (#1758)\n\nAdapter-agnostic change-observation spine (`src/change-feed.ts`) — the server half of the client/mobile sync contract (PRD #1755):\n\n- `_smrt_changes` system table: one append per framework save/delete via a GlobalInterceptors writer registered at framework init. Deletes are tombstones (`operation: 'delete'`). `_smrt_*` tables are skipped. Feed-append failures log and never fail the user's write. No dirty-check: a field-unchanged `.save()` appends a spurious `update` entry (diff-aware paths like `getOrUpsert`/sync-apply short-circuit before `save()` and append nothing); subscribers must tolerate spurious entries — they are convergent.\n- Sequences: allocated as `MAX(seq)+1` inside the INSERT with conflict retry — committed rows stay contiguous, so commit order == seq order on SQLite/Postgres/DuckDB (deliberately NOT identity/serial: those allocate before commit and break the cursor guarantee under concurrent writers).\n- `getChangesSince(db, { since, tables?, tenantId?, limit? }) → { changes, cursor, resyncRequired? }`: strictly monotonic cursor; polling with returned cursors misses no committed change and never repeats one. A cursor that cannot be served incrementally — pruned below the retained `[floor..horizon]` run, or foreign/ahead of the horizon — gets `resyncRequired: true` with empty `changes` and an unadvanced cursor; detection runs on the UNFILTERED log so `tables`/`tenantId` filters never trigger or mask it. `getTenantScopedChangesSince()` resolves tenant via the DispatchBus resolver hook (fail-closed: tenancy on + no context → global rows only; tenant `T` sees `T` + global rows, never another tenant).\n- `getTableVersion(db, table) → number`: the per-table change version (`MAX(seq)` for the table, replica-stable — no per-process divergence), the ETag source for zero-query conditional GETs (#1765). Advances on any framework write to the table (CRUD and sync-apply, which all `save()`/`delete()`). A table with no retained entry of its own falls back to the global horizon (never a resettable low value) so an all-pruned table cannot false-304 a stale client; only 0 when the feed is empty.\n- Generated `_changes` routes: REST (`GET {basePath}/_changes`, requires `authMiddleware`, otherwise 401 — per-model `api.public` does NOT apply) and SvelteKit (`{routesDir}/_changes/+server.ts`, requires an authenticated principal on `locals`; opt out via `sveltekit.changesRoute.enabled: false`). Query params: `since`, `tables` (comma-separated), `limit`. Responses stay HTTP 200 in the resync state — `resyncRequired` is protocol state, not an error.\n- Retention: `pruneChangeFeed(db, { maxAgeMs?, maxRows? })` — schedule it. Pruning deletes oldest-first and always retains the newest entry (a non-empty feed is never emptied), which is what makes pruned-cursor detection provable and keeps caught-up consumers polling normally. Raw-SQL writes are invisible to the feed (same documented gap as the #1499 cache); `bumpChangeFeed(db, { table, rowId? })` is the manual escape hatch.\n\n## Single Table Inheritance (STI)\n\n- Base: `@smrt({ tableStrategy: 'sti' })` — children inherit, share one table\n- Discriminator: `_meta_type` column with qualified names (`@happyvertical/smrt-content:Article`)\n- Child fields: `@meta()` decorator → stored in `_meta_data` JSONB (not as columns)\n- Polymorphic queries: collection loads `_meta_type`, creates correct subclass dynamically\n- Validation: fail-fast on save if `_meta_type` missing or mismatched\n\n## Code Generators\n\n| Generator | Location | Output |\n|-----------|----------|--------|\n| REST API | `src/generators/rest.ts` | OpenAPI-compliant CRUD endpoints |\n| CLI | `src/generators/cli.ts` | `objectname:action` admin commands — writable allowlist, exhaustive-include, `--from-file`, fail-closed tenant context |\n| MCP Server | `src/generators/mcp.ts` | Model Context Protocol tools |\n\nGenerated reads (`list`/`get`) on the REST and SvelteKit generators support conditional GET (helpers in `src/generators/conditional-get.ts`). ETag v2 (#1765): the validator is the table's change-feed version (`getTableVersion`) keyed by the request representation, so a **concrete** `If-None-Match` short-circuits into a 304 with an empty body **before** the collection query runs — an unchanged table revalidates with zero table scan. A wildcard `If-None-Match: *` is deferred until the payload builds (existence confirmed), so a missing item still returns 404, not a false 304. Tenant-scoped reads fold the active tenant into the representation (`resolveTenantEtagDiscriminator`) so one tenant's cached validator never satisfies another's read of the same URL. Routes whose GET renders via a **custom serializer** (which can load related tables the base-table version can't observe) keep the v1 body-hash ETag (`#1757`, query-first but correct); the default `toPublicJSON` path — all REST reads and non-serializer SvelteKit reads — uses v2. v2 is weakly consistent by design (the cost of not reading the data): a revalidation in the sub-statement window between a committed write and its feed append can return a stale 304 that self-heals on the next revalidation, and a deploy that changes the response shape without a table write leaves ETags unchanged until the next write (deploy-time ETag invalidation via the manifest hash is #1764's domain). Strong consistency requires the v1 body-hash path. Cache-Control policy (unchanged from #1757): `private, no-cache` by default; public models may opt into shared caching via `@smrt({ api: { public: true | 'read', cache: { sMaxage } } })` → `public, max-age=0, s-maxage=<n>`; non-public models never emit shared-cache headers. Tenant-scoped models (any mode) never emit them either — bodies vary with session-cookie tenant context that URL-keyed shared caches cannot see; `sMaxage` is neutralized to `private, no-cache` with a one-time warning.\n\n## Child Accessors (R10)\n\n`src/child-accessors.ts` installs a consistent `get<FieldName>()` instance method for every `@oneToMany` field at `@smrt()` registration time (e.g. `@oneToMany('OrderItem') items` → `order.getItems()`), delegating to `loadRelatedMany`. Two invariants:\n\n- **Additive** — never overwrites a hand-rolled method of the same name (checks the whole prototype chain). `Profile.getMetadata()` (key-value) and `ProfileRelationship.getTerms()` are preserved.\n- **Runtime-only** — attached to the prototype, invisible to the build-time manifest, so it never leaks into the REST/CLI/MCP surface.\n\nWhen the target declares multiple FKs back to the parent, annotate `@oneToMany(Target, { foreignKey: '<inverseField>' })`; `loadRelatedMany` and the eager `include:` loader both honor it (else first-match).\n\n## Vite Plugin\n\n```typescript\n// vite.config.ts — required for @smrt() decorators (Vite 8+, oxc transform)\nexport default defineConfig({\n oxc: {\n decorator: {\n legacy: true,\n emitDecoratorMetadata: true,\n },\n },\n});\n```\n\nUnder Vite 8 the oxc transform does not honor the pre-Vite-8 `esbuild.tsconfigRaw`\nrecipe (or tsconfig `experimentalDecorators` reached through SvelteKit's\n`extends \"./.svelte-kit/tsconfig.json\"` chain), so that recipe throws\n`SyntaxError: Invalid or unexpected token` on the first SSR request. Configure\ndecorators through `oxc.decorator` instead. Consumers still pinned on vite<8 need\nthe legacy `esbuild.tsconfigRaw` form with `experimentalDecorators: true,\nemitDecoratorMetadata: true`.\n\n## Gotchas\n\n- **Never override toJSON()** — handles STI discriminator + meta field extraction. Use `transformJSON()`\n- **Property init order**: TypeScript initializers run first, then `initialize()` applies option values (options win)\n- **No runtime schema creation**: application tables must be prepared explicitly via migrations/tooling; runtime only verifies and fails clearly\n- **Retry logic**: `db.get()` (3 retries, 250ms) and `db.upsert()` (3 retries, 500ms) have built-in retry\n- **Field caching**: `_cachedFields` populated during `Collection.create()` — eliminates async `getFields()` per query\n- **Smart cloning**: arrays/objects shallow-cloned in property init to prevent aliasing (Issue #22)\n- **Table verification cache**: `isTableVerified(dbUrl, tableName)` avoids redundant `tableExists()` calls\n- **Manifest required**: build-time AST scanning creates manifest. Without vitest plugin → \"No field metadata\"\n- **Vite plugin loads scanner from `dist/` first**: `src/vite-plugin/import-build-aware.ts` prefers `dist/` when it exists on disk; it only falls back to `src/` on fresh clones. So if you edit `src/scanner/*.ts` or `src/schema/generator.ts` and want those edits reflected in consumer manifest generation, you must rebuild (`pnpm build` or have `pnpm dev` / `pnpm build:watch` running in core). This is intentional — sniffing `.ts` vs `.js` via `import.meta.url` was non-deterministic under tsx and broke 12–13 publishes (#1139).\n"
326
+ "agentDoc": "# @happyvertical/smrt-core\n\nORM, code generation, AI integration, and the DispatchBus. Everything else builds on this.\n\n## Key Classes\n\n| Class | File | Purpose |\n|-------|------|---------|\n| SmrtObject | `src/object.ts` | Base persistent object — save, delete, is(), do(), loadFromId/Slug |\n| SmrtCollection | `src/collection.ts` | CRUD collection — list, get, create, delete, getOrUpsert |\n| ObjectRegistry | `src/registry.ts` | Global singleton (globalThis) — class metadata, fields, STI chains, manifests |\n| DispatchBus | `src/dispatch/bus.ts` | Inter-agent messaging — emit, subscribe (persistent), process |\n| GlobalInterceptors | `src/interceptors.ts` | Plugin system — beforeList/Get/Save/Delete hooks (used by tenancy) |\n\n## SmrtObject Lifecycle\n\n`constructor(options)` → `initialize()` → ready for `save()`/`delete()`/`loadFromId()`\n\n- `initialize()`: loads field initializers, applies option values (options override initializers), loads from DB if id/slug provided\n- `save()`: upsert with STI validation, interceptor execution, auto-embeddings. Persisted objects (`isPersisted` — set by DB hydration and successful saves) upsert on `['id']` so natural-key edits (e.g. slug renames) update in place; new objects upsert on the natural-key conflict columns for ingestion-style dedup (#1472)\n- `is(criteria)` / `do(instructions)` / `describe()`: AI operations via function calling. They inject the object's own `toPublicJSON()` (sensitive fields stripped) as a \"content body\" so the model reasons over the instance. Options: `includeData: false` skips injection (for callers that already curate the relevant fields into the instruction); `maxDataLength` overrides the truncation budget. Neither key is forwarded to `ai.message()`. (#1567)\n- `getSlug()`: auto-generates from name → title → label → id\n- `loadRelated(fieldName)`: lazy-loads relationships (cached in `_loadedRelationships` Map)\n\n## SmrtCollection Query\n\n```typescript\nawait collection.list({\n where: { status: 'active', price: { op: '>', value: 10 } },\n limit: 50, offset: 0, orderBy: 'created_at DESC'\n});\n```\n\n**WHERE operators**: `=`, `>`, `<`, `>=`, `<=`, `!=`, `in`, `not in`, `like`, `is null`, `is not null`. Arrays auto-detect `IN`. Dot notation for JSON paths: `metadata.userId`.\n\nSTI child collections auto-filter by `_meta_type`.\n\n## @smrt() Decorator Options\n\nKey options: `tableName`, `tableStrategy` ('cti'|'sti'), `conflictColumns`, `api`/`mcp`/`cli` (generation config), `ai` (callable methods), `hooks` (beforeSave/afterSave/beforeDelete/afterDelete), `embeddings` (auto-generate), `tenantScoped`, `agent`.\n\nRegistration sets `SMRT_TABLE_NAME` static property (survives minification).\n\n## Domain Knowledge Artifacts\n\n`smrtPlugin()` writes runtime manifests and agent/developer knowledge artifacts:\n\n- local dev/build: `.smrt/manifest.json` and `.smrt/smrt-knowledge.json`\n- package build: `dist/manifest.json` and `dist/smrt-knowledge.json`\n\nKeep `manifest.json` runtime-focused. `smrt-knowledge.json` is the deterministic\nagent contract for downstream review and architecture tools.\n\nConfig precedence for knowledge is defaults → top-level `knowledge` in\n`smrt.config.ts` → `packages[packageName].knowledge` → plugin option →\nobject-level `@smrt({ knowledge })`.\n\nObject-level `knowledge: false` excludes an object from authored context only;\nit must not change runtime manifest registration. Use\n`knowledge: { tags, summary, risks }` for review-sensitive domain objects.\n\nHTTP knowledge routes are disabled by default. If `knowledge.api.enabled` is\ntrue, generated SvelteKit routes must stay GET-only and guarded by dev mode or\nadmin auth.\n\n## DispatchBus\n\n- `emit(signalType, payload, metadata)` → creates persistent Dispatch record\n- `on(pattern, handler)` → in-memory handler (immediate)\n- `subscribe({ signalType, subscriber })` → persistent subscription (survives restarts)\n- `process(subscriberName, handler)` → process pending dispatches\n- Wildcards: `campaign.*` matches `campaign.completed` (single segment only)\n- Tables: `_smrt_dispatch`, `_smrt_dispatch_subscriptions`\n- Status: `pending → processing → completed` (or `failed`)\n\n## Change Feed (#1758)\n\nAdapter-agnostic change-observation spine (`src/change-feed.ts`) — the server half of the client/mobile sync contract (PRD #1755):\n\n- `_smrt_changes` system table: one append per framework save/delete via a GlobalInterceptors writer registered at framework init. Deletes are tombstones (`operation: 'delete'`). `_smrt_*` tables are skipped. Feed-append failures log and never fail the user's write. No dirty-check: a field-unchanged `.save()` appends a spurious `update` entry (diff-aware paths like `getOrUpsert`/sync-apply short-circuit before `save()` and append nothing); subscribers must tolerate spurious entries — they are convergent.\n- Sequences: allocated as `MAX(seq)+1` inside the INSERT with conflict retry — committed rows stay contiguous, so commit order == seq order on SQLite/Postgres/DuckDB (deliberately NOT identity/serial: those allocate before commit and break the cursor guarantee under concurrent writers).\n- `getChangesSince(db, { since, tables?, tenantId?, limit? }) → { changes, cursor, resyncRequired?, resyncCursor? }`: strictly monotonic cursor; polling with returned cursors misses no committed change and never repeats one. A cursor that cannot be served incrementally — pruned below the retained `[floor..horizon]` run, or foreign/ahead of the horizon — gets `resyncRequired: true` with empty `changes`, an unadvanced `cursor`, and `resyncCursor` set to the current horizon so clients can full-refetch then resume incrementally; detection runs on the UNFILTERED log so `tables`/`tenantId` filters never trigger or mask it. `getTenantScopedChangesSince()` resolves tenant via the DispatchBus resolver hook (fail-closed: tenancy on + no context → global rows only; tenant `T` sees `T` + global rows, never another tenant).\n- `getTableVersion(db, table) → number`: the per-table change version (`MAX(seq)` for the table, replica-stable — no per-process divergence), the ETag source for zero-query conditional GETs (#1765). Advances on any framework write to the table (CRUD and sync-apply, which all `save()`/`delete()`). A table with no retained entry of its own falls back to the global horizon (never a resettable low value) so an all-pruned table cannot false-304 a stale client; only 0 when the feed is empty.\n- Generated `_changes` routes: REST (`GET {basePath}/_changes`, requires `authMiddleware`, otherwise 401 — per-model `api.public` does NOT apply) and SvelteKit (`{routesDir}/_changes/+server.ts`, requires an authenticated principal on `locals`; opt out via `sveltekit.changesRoute.enabled: false`). Query params: `since`, `tables` (comma-separated), `limit`. Responses stay HTTP 200 in the resync state — `resyncRequired` is protocol state, not an error, and `resyncCursor` is the resume cursor after the client completes a full refetch.\n- Retention: `pruneChangeFeed(db, { maxAgeMs?, maxRows? })` — schedule it. Pruning deletes oldest-first and always retains the newest entry (a non-empty feed is never emptied), which is what makes pruned-cursor detection provable and keeps caught-up consumers polling normally. Raw-SQL writes are invisible to the feed (same documented gap as the #1499 cache); `bumpChangeFeed(db, { table, rowId? })` is the manual escape hatch.\n\n## Live Events / Change Signals (#1763, server half)\n\nThe push companion to the change feed (`src/change-signals.ts` + the generated `_events` SSE route) — the server half of live cache invalidation (PRD #1755). The client subscriber (two-client/reconnect/polling-fallback ACs) is a separate later slice.\n\n- **Change-signal bus** (`src/change-signals.ts`): every framework save/delete that appends a durable feed row also publishes a coarse `ChangeSignal` `{ table, operation, rowId, tenantId, seq }` — **never a row payload** (authorization stays on the read path). Structurally mirrors the collection cache's notify/listen path. `subscribeToChangeSignals(db, listener) → unsubscribe`; `publishChangeSignal`/`broadcastChangeSignal`/the listener loop stay internal. `appendChange` now returns the allocated `seq` (was `void`); the signal carries it as a coarse resume cursor (`bumpChangeFeed` ignores the return). The publish runs only after the append SUCCEEDS (no signal without a durable feed row) and in its own log-and-swallow try/catch, so a signal problem never fails the user's write. `_smrt_*` writes never signal (the writer skips them). Delivery is synchronous per-listener with per-listener try/catch (one throwing SSE controller never blocks others); no per-subscriber queue — backpressure rides the platform `ReadableStream`.\n- **In-process + cross-replica**: locally-published and peer-received signals go through the SAME `deliverLocally` path. Cross-replica fan-out rides the db adapter's optional notification capability (`db.notifications`, a NEW `smrt_change_signals` channel distinct from the cache channel) with echo-avoidance by `PROCESS_ID`. No capability → in-process only, warn-once, **never an error, never blocks the write** (subscribers on other replicas fall back to cursor polling).\n- **Generated `_events` SSE route**: REST (`GET {basePath}/_events`, requires `authMiddleware`, otherwise 401 — fail-closed, per-model `api.public` does NOT apply; 405 non-GET; 503 no db) and SvelteKit (`{routesDir}/_events/+server.ts`, requires an authenticated principal on `locals`; opt out via `sveltekit.eventsRoute.enabled: false`). Tenant scope is captured ONCE at connection open (`resolveDispatchTenantScope`) and filtered server-side per signal via `signalVisibleToTenant` (same rule as `getChangesSince`'s tenant filter) before any byte hits the wire — delivery runs outside any tenant ALS context, so it must use the captured value. The stream lifecycle lives in `buildChangeEventStream(db, { cursor, tenantScope, heartbeatMs? })` (exported; the SvelteKit route imports it so it stays thin): subscribe-before-catch-up (closes the gap window; overlap is deduped by the SSE `id:`/seq client-side), `retry: 3000`, cursor catch-up via `getChangesSince` filtered by the CAPTURED scope (not re-resolved from ALS, so it matches the live-signal filter exactly and can't replay another tenant's rows) (`Last-Event-ID` header beats `?since=`; default = live-forward only; `resyncRequired` → `event: resync`), heartbeat (`DEFAULT_EVENTS_HEARTBEAT_MS` = 15s), and `cancel()` teardown (clears heartbeat + unsubscribes). SSE frame: `id: <seq>\\nevent: change\\ndata: {table,operation,rowId,tenantId}\\n\\n` — seq is ONLY in the `id:` line, never the data JSON. **Same-origin only** (not CORS-wrapped): `EventSource` can't set headers and credentialed cross-origin needs Allow-Credentials the CORS helper doesn't emit — cross-origin SSE is a follow-up. Client disconnect through the Node `createServer` bridge now cancels the response reader (was a teardown leak) so `cancel()` fires and the subscription is released.\n- **Known gaps** (documented in the module): no max-connections cap (bound SSE connections at the edge); raw-SQL writes don't signal (same gap as the feed); live signals for caller-managed-transaction writes are best-effort (the append + signal fire pre-commit, so a rolled-back write may emit a signal and its freed seq is later reused) — the autocommit default path is exact, and clients reconcile via full catch-up/resync (inherits the change feed's transaction caveat).\n\n## Single Table Inheritance (STI)\n\n- Base: `@smrt({ tableStrategy: 'sti' })` — children inherit, share one table\n- Discriminator: `_meta_type` column with qualified names (`@happyvertical/smrt-content:Article`)\n- Child fields: `@meta()` decorator → stored in `_meta_data` JSONB (not as columns)\n- Polymorphic queries: collection loads `_meta_type`, creates correct subclass dynamically\n- Validation: fail-fast on save if `_meta_type` missing or mismatched\n\n## Code Generators\n\n| Generator | Location | Output |\n|-----------|----------|--------|\n| REST API | `src/generators/rest.ts` | OpenAPI-compliant CRUD endpoints |\n| CLI | `src/generators/cli.ts` | `objectname:action` admin commands — writable allowlist, exhaustive-include, `--from-file`, fail-closed tenant context |\n| MCP Server | `src/generators/mcp.ts` | Model Context Protocol tools |\n\nGenerated reads (`list`/`get`) on the REST and SvelteKit generators support conditional GET (helpers in `src/generators/conditional-get.ts`). ETag v2 (#1765): the validator is the table's change-feed version (`getTableVersion`) keyed by the request representation, so a **concrete** `If-None-Match` short-circuits into a 304 with an empty body **before** the collection query runs — an unchanged table revalidates with zero table scan. A wildcard `If-None-Match: *` is deferred until the payload builds (existence confirmed), so a missing item still returns 404, not a false 304. Tenant-scoped reads fold the active tenant into the representation (`resolveTenantEtagDiscriminator`) so one tenant's cached validator never satisfies another's read of the same URL. Routes whose GET renders via a **custom serializer** (which can load related tables the base-table version can't observe) keep the v1 body-hash ETag (`#1757`, query-first but correct); the default `toPublicJSON` path — all REST reads and non-serializer SvelteKit reads — uses v2. v2 is weakly consistent by design (the cost of not reading the data): a revalidation in the sub-statement window between a committed write and its feed append can return a stale 304 that self-heals on the next revalidation, and a deploy that changes the response shape without a table write leaves ETags unchanged until the next write (deploy-time ETag invalidation via the manifest hash is #1764's domain). Strong consistency requires the v1 body-hash path. Cache-Control policy (unchanged from #1757): `private, no-cache` by default; public models may opt into shared caching via `@smrt({ api: { public: true | 'read', cache: { sMaxage } } })` → `public, max-age=0, s-maxage=<n>`; non-public models never emit shared-cache headers. Tenant-scoped models (any mode) never emit them either — bodies vary with session-cookie tenant context that URL-keyed shared caches cannot see; `sMaxage` is neutralized to `private, no-cache` with a one-time warning.\n\n## Child Accessors (R10)\n\n`src/child-accessors.ts` installs a consistent `get<FieldName>()` instance method for every `@oneToMany` field at `@smrt()` registration time (e.g. `@oneToMany('OrderItem') items` → `order.getItems()`), delegating to `loadRelatedMany`. Two invariants:\n\n- **Additive** — never overwrites a hand-rolled method of the same name (checks the whole prototype chain). `Profile.getMetadata()` (key-value) and `ProfileRelationship.getTerms()` are preserved.\n- **Runtime-only** — attached to the prototype, invisible to the build-time manifest, so it never leaks into the REST/CLI/MCP surface.\n\nWhen the target declares multiple FKs back to the parent, annotate `@oneToMany(Target, { foreignKey: '<inverseField>' })`; `loadRelatedMany` and the eager `include:` loader both honor it (else first-match).\n\n## Vite Plugin\n\n```typescript\n// vite.config.ts — required for @smrt() decorators (Vite 8+, oxc transform)\nexport default defineConfig({\n oxc: {\n decorator: {\n legacy: true,\n emitDecoratorMetadata: true,\n },\n },\n});\n```\n\nUnder Vite 8 the oxc transform does not honor the pre-Vite-8 `esbuild.tsconfigRaw`\nrecipe (or tsconfig `experimentalDecorators` reached through SvelteKit's\n`extends \"./.svelte-kit/tsconfig.json\"` chain), so that recipe throws\n`SyntaxError: Invalid or unexpected token` on the first SSR request. Configure\ndecorators through `oxc.decorator` instead. Consumers still pinned on vite<8 need\nthe legacy `esbuild.tsconfigRaw` form with `experimentalDecorators: true,\nemitDecoratorMetadata: true`.\n\n## Gotchas\n\n- **Never override toJSON()** — handles STI discriminator + meta field extraction. Use `transformJSON()`\n- **Property init order**: TypeScript initializers run first, then `initialize()` applies option values (options win)\n- **No runtime schema creation**: application tables must be prepared explicitly via migrations/tooling; runtime only verifies and fails clearly\n- **Retry logic**: `db.get()` (3 retries, 250ms) and `db.upsert()` (3 retries, 500ms) have built-in retry\n- **Field caching**: `_cachedFields` populated during `Collection.create()` — eliminates async `getFields()` per query\n- **Smart cloning**: arrays/objects shallow-cloned in property init to prevent aliasing (Issue #22)\n- **Table verification cache**: `isTableVerified(dbUrl, tableName)` avoids redundant `tableExists()` calls\n- **Manifest required**: build-time AST scanning creates manifest. Without vitest plugin → \"No field metadata\"\n- **Vite plugin loads scanner from `dist/` first**: `src/vite-plugin/import-build-aware.ts` prefers `dist/` when it exists on disk; it only falls back to `src/` on fresh clones. So if you edit `src/scanner/*.ts` or `src/schema/generator.ts` and want those edits reflected in consumer manifest generation, you must rebuild (`pnpm build` or have `pnpm dev` / `pnpm build:watch` running in core). This is intentional — sniffing `.ts` vs `.js` via `import.meta.url` was non-deterministic under tsx and broke 12–13 publishes (#1139).\n"
327
327
  }
@@ -18,4 +18,5 @@
18
18
  * @packageDocumentation
19
19
  */
20
20
  export { getTestDatabase, type TestDatabaseOptions } from './database.js';
21
+ export { type StubNotification, type StubNotificationsHandle, stubNotifications, } from './notifications-stub.js';
21
22
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,eAAe,EAAE,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,eAAe,EAAE,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAC5B,iBAAiB,GAClB,MAAM,yBAAyB,CAAC"}
@@ -1,2 +1,3 @@
1
1
  import { getTestDatabase } from "./database.js";
2
- export { getTestDatabase };
2
+ import { stubNotifications } from "./notifications-stub.js";
3
+ export { getTestDatabase, stubNotifications };
@@ -0,0 +1,24 @@
1
+ import { DatabaseInterface } from '@happyvertical/sql';
2
+ /** One notification as delivered over the stubbed channel. */
3
+ export interface StubNotification {
4
+ channel: string;
5
+ payload: any;
6
+ }
7
+ /** Handle returned by {@link stubNotifications}. */
8
+ export interface StubNotificationsHandle {
9
+ /** Every broadcast recorded by `notify`, in call order. */
10
+ broadcasts: StubNotification[];
11
+ /** Deliver a notification to the active `listen` iterator (or queue it). */
12
+ push: (notification: StubNotification) => void;
13
+ }
14
+ /**
15
+ * Install a minimal `notifications` capability on `db` and return handles for
16
+ * asserting broadcasts and pushing inbound notifications.
17
+ *
18
+ * `notify` appends to `broadcasts` and resolves; `listen` returns a
19
+ * single-consumer async iterable whose `next()` resolves with the next pushed
20
+ * (or previously queued) notification, and whose `return()` completes the
21
+ * iterator so the background listener loop can be torn down.
22
+ */
23
+ export declare function stubNotifications(db: DatabaseInterface): StubNotificationsHandle;
24
+ //# sourceMappingURL=notifications-stub.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications-stub.d.ts","sourceRoot":"","sources":["../../src/testing/notifications-stub.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,8DAA8D;AAC9D,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAGhB,OAAO,EAAE,GAAG,CAAC;CACd;AAED,oDAAoD;AACpD,MAAM,WAAW,uBAAuB;IACtC,2DAA2D;IAC3D,UAAU,EAAE,gBAAgB,EAAE,CAAC;IAC/B,4EAA4E;IAC5E,IAAI,EAAE,CAAC,YAAY,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAChD;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,EAAE,EAAE,iBAAiB,GACpB,uBAAuB,CA2CzB"}
@@ -0,0 +1,62 @@
1
+ //#region src/testing/notifications-stub.ts
2
+ /**
3
+ * Install a minimal `notifications` capability on `db` and return handles for
4
+ * asserting broadcasts and pushing inbound notifications.
5
+ *
6
+ * `notify` appends to `broadcasts` and resolves; `listen` returns a
7
+ * single-consumer async iterable whose `next()` resolves with the next pushed
8
+ * (or previously queued) notification, and whose `return()` completes the
9
+ * iterator so the background listener loop can be torn down.
10
+ */
11
+ function stubNotifications(db) {
12
+ const broadcasts = [];
13
+ const waiters = [];
14
+ const queue = [];
15
+ const push = (notification) => {
16
+ const waiter = waiters.shift();
17
+ if (waiter) waiter(notification);
18
+ else queue.push(notification);
19
+ };
20
+ db.notifications = {
21
+ async notify(channel, payload) {
22
+ broadcasts.push({
23
+ channel,
24
+ payload
25
+ });
26
+ return 1;
27
+ },
28
+ listen(_channel) {
29
+ return { [Symbol.asyncIterator]() {
30
+ return {
31
+ next() {
32
+ const queued = queue.shift();
33
+ if (queued) return Promise.resolve({
34
+ value: queued,
35
+ done: false
36
+ });
37
+ return new Promise((resolve) => {
38
+ waiters.push((value) => resolve({
39
+ value,
40
+ done: false
41
+ }));
42
+ });
43
+ },
44
+ return() {
45
+ return Promise.resolve({
46
+ value: void 0,
47
+ done: true
48
+ });
49
+ }
50
+ };
51
+ } };
52
+ }
53
+ };
54
+ return {
55
+ broadcasts,
56
+ push
57
+ };
58
+ }
59
+ //#endregion
60
+ export { stubNotifications };
61
+
62
+ //# sourceMappingURL=notifications-stub.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications-stub.js","names":[],"sources":["../../src/testing/notifications-stub.ts"],"sourcesContent":["/**\n * In-process test double for the sql package's optional notification\n * capability (`db.notifications`, e.g. Postgres LISTEN/NOTIFY).\n *\n * Attaches a push-driven `notify`/`listen` pair to a database handle so tests\n * can exercise the cross-process / cross-replica paths of the collection cache\n * (#1498) and the change-signal bus (#1763) against a real in-memory SQLite\n * database that has no native notification support. `notify` records every\n * broadcast; `listen` exposes an async iterable that yields whatever `push`\n * delivers.\n *\n * Extracted from `__tests__/issue-1498-collection-cache.test.ts` so the same\n * double backs the change-signal bus tests without duplicating the stub.\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\n\n/** One notification as delivered over the stubbed channel. */\nexport interface StubNotification {\n channel: string;\n // Payloads are arbitrary JSON-ish structures; callers assert on their shape.\n // biome-ignore lint/suspicious/noExplicitAny: notification payloads are untyped by design\n payload: any;\n}\n\n/** Handle returned by {@link stubNotifications}. */\nexport interface StubNotificationsHandle {\n /** Every broadcast recorded by `notify`, in call order. */\n broadcasts: StubNotification[];\n /** Deliver a notification to the active `listen` iterator (or queue it). */\n push: (notification: StubNotification) => void;\n}\n\n/**\n * Install a minimal `notifications` capability on `db` and return handles for\n * asserting broadcasts and pushing inbound notifications.\n *\n * `notify` appends to `broadcasts` and resolves; `listen` returns a\n * single-consumer async iterable whose `next()` resolves with the next pushed\n * (or previously queued) notification, and whose `return()` completes the\n * iterator so the background listener loop can be torn down.\n */\nexport function stubNotifications(\n db: DatabaseInterface,\n): StubNotificationsHandle {\n const broadcasts: StubNotification[] = [];\n const waiters: Array<(value: StubNotification) => void> = [];\n const queue: StubNotification[] = [];\n\n const push = (notification: StubNotification): void => {\n const waiter = waiters.shift();\n if (waiter) waiter(notification);\n else queue.push(notification);\n };\n\n // biome-ignore lint/suspicious/noExplicitAny: attaching to the duck-typed capability slot\n (db as any).notifications = {\n async notify(channel: string, payload: unknown): Promise<number> {\n broadcasts.push({ channel, payload });\n return 1;\n },\n listen(_channel: string) {\n return {\n [Symbol.asyncIterator]() {\n return {\n next(): Promise<IteratorResult<StubNotification>> {\n const queued = queue.shift();\n if (queued) {\n return Promise.resolve({ value: queued, done: false });\n }\n return new Promise((resolve) => {\n waiters.push((value) => resolve({ value, done: false }));\n });\n },\n return(): Promise<IteratorResult<StubNotification>> {\n return Promise.resolve({\n value: undefined as unknown as StubNotification,\n done: true,\n });\n },\n };\n },\n };\n },\n };\n\n return { broadcasts, push };\n}\n"],"mappings":";;;;;;;;;;AA0CA,SAAgB,kBACd,IACyB;CACzB,MAAM,aAAiC,CAAC;CACxC,MAAM,UAAoD,CAAC;CAC3D,MAAM,QAA4B,CAAC;CAEnC,MAAM,QAAQ,iBAAyC;EACrD,MAAM,SAAS,QAAQ,MAAM;EAC7B,IAAI,QAAQ,OAAO,YAAY;OAC1B,MAAM,KAAK,YAAY;CAC9B;CAGA,GAAY,gBAAgB;EAC1B,MAAM,OAAO,SAAiB,SAAmC;GAC/D,WAAW,KAAK;IAAE;IAAS;GAAQ,CAAC;GACpC,OAAO;EACT;EACA,OAAO,UAAkB;GACvB,OAAO,EACL,CAAC,OAAO,iBAAiB;IACvB,OAAO;KACL,OAAkD;MAChD,MAAM,SAAS,MAAM,MAAM;MAC3B,IAAI,QACF,OAAO,QAAQ,QAAQ;OAAE,OAAO;OAAQ,MAAM;MAAM,CAAC;MAEvD,OAAO,IAAI,SAAS,YAAY;OAC9B,QAAQ,MAAM,UAAU,QAAQ;QAAE;QAAO,MAAM;OAAM,CAAC,CAAC;MACzD,CAAC;KACH;KACA,SAAoD;MAClD,OAAO,QAAQ,QAAQ;OACrB,OAAO,KAAA;OACP,MAAM;MACR,CAAC;KACH;IACF;GACF,EACF;EACF;CACF;CAEA,OAAO;EAAE;EAAY;CAAK;AAC5B"}
package/dist/testing.js CHANGED
@@ -1,3 +1,4 @@
1
1
  import { getTestDatabase } from "./testing/database.js";
2
+ import { stubNotifications } from "./testing/notifications-stub.js";
2
3
  import "./testing/index.js";
3
- export { getTestDatabase };
4
+ export { getTestDatabase, stubNotifications };
@@ -44,7 +44,7 @@ function convertTypeToJsonSchema(tsType) {
44
44
  * @returns True if method should be callable by AI
45
45
  */
46
46
  function shouldIncludeMethod(method, config) {
47
- if (!config || !config.callable) return false;
47
+ if (!config?.callable) return false;
48
48
  if (config.exclude?.includes(method.name)) return false;
49
49
  if (!method.isPublic) return false;
50
50
  if (method.isStatic) return false;
@@ -1 +1 @@
1
- {"version":3,"file":"tool-generator.js","names":[],"sources":["../../src/tools/tool-generator.ts"],"sourcesContent":["/**\n * Tool Manifest Generation for AI Function Calling\n *\n * This module converts TypeScript method definitions from the AST scanner\n * into AI tool format at build time for use with LLM function calling.\n */\n\nimport type { AITool } from '@happyvertical/ai';\nimport type { MethodDefinition } from '../scanner/types.js';\n\n/**\n * A JSON Schema fragment. The shape is open-ended (different keywords appear\n * for primitives, arrays, unions, etc.), so values are `unknown`; callers\n * narrow specific keys (e.g. `.default`) at the point of use.\n */\ntype JsonSchema = Record<string, unknown>;\n\n/**\n * The `parameters` object of a generated tool: a JSON Schema `object` with a\n * `properties` bag (each entry a nested schema) and a `required` name list.\n */\ninterface ToolParametersSchema {\n type: 'object';\n properties: Record<string, JsonSchema>;\n /**\n * Optional so the empty array can be `delete`d (omitting the key from the\n * emitted JSON Schema). It is always present while parameters are collected.\n */\n required?: string[];\n}\n\n/**\n * Configuration for AI-callable methods\n */\nexport interface AiConfig {\n /**\n * Methods that AI can call\n * - Array of method names, e.g., ['analyze', 'validate']\n * - 'public-async' to auto-include all public async methods\n * - 'all' to include all methods (not recommended)\n */\n callable?: string[] | 'public-async' | 'all';\n\n /**\n * Methods to exclude from AI calling (higher priority than callable)\n */\n exclude?: string[];\n\n /**\n * Additional tool descriptions to override method JSDoc\n */\n descriptions?: Record<string, string>;\n}\n\n/**\n * Converts a TypeScript type string to JSON Schema format\n *\n * @param tsType - TypeScript type string (e.g., 'string', 'number', '{ foo: string }')\n * @returns JSON Schema representation\n */\nexport function convertTypeToJsonSchema(tsType: string): JsonSchema {\n // Remove whitespace\n const cleanType = tsType.trim();\n\n // Primitive types\n if (cleanType === 'string') {\n return { type: 'string' };\n }\n if (cleanType === 'number') {\n return { type: 'number' };\n }\n if (cleanType === 'boolean') {\n return { type: 'boolean' };\n }\n if (cleanType === 'null') {\n return { type: 'null' };\n }\n if (cleanType === 'any' || cleanType === 'unknown') {\n return {}; // No type constraint\n }\n\n // Array types\n if (cleanType.endsWith('[]')) {\n const itemType = cleanType.slice(0, -2);\n return {\n type: 'array',\n items: convertTypeToJsonSchema(itemType),\n };\n }\n\n // Array<T> syntax\n const arrayMatch = cleanType.match(/^Array<(.+)>$/);\n if (arrayMatch) {\n return {\n type: 'array',\n items: convertTypeToJsonSchema(arrayMatch[1]),\n };\n }\n\n // Union types with literal values (e.g., 'shallow' | 'deep')\n if (cleanType.includes('|')) {\n const options = cleanType.split('|').map((s) => s.trim());\n\n // Check if all options are string literals\n if (options.every((opt) => opt.startsWith(\"'\") && opt.endsWith(\"'\"))) {\n return {\n type: 'string',\n enum: options.map((opt) => opt.slice(1, -1)), // Remove quotes\n };\n }\n\n // Mixed union - use oneOf\n return {\n oneOf: options.map(convertTypeToJsonSchema),\n };\n }\n\n // Object types - basic support\n if (cleanType.startsWith('{') && cleanType.endsWith('}')) {\n return { type: 'object' };\n }\n\n // Record<string, any> and similar\n if (cleanType.startsWith('Record<')) {\n return { type: 'object' };\n }\n\n // Default fallback\n return { type: 'string', description: `TypeScript type: ${cleanType}` };\n}\n\n/**\n * Determines if a method should be included as an AI-callable tool\n *\n * @param method - Method definition from AST scanner\n * @param config - AI configuration from @smrt decorator\n * @returns True if method should be callable by AI\n */\nexport function shouldIncludeMethod(\n method: MethodDefinition,\n config?: AiConfig,\n): boolean {\n // Skip if no AI config\n if (!config || !config.callable) {\n return false;\n }\n\n // Check exclusions first (higher priority)\n if (config.exclude?.includes(method.name)) {\n return false;\n }\n\n // Skip private methods always\n if (!method.isPublic) {\n return false;\n }\n\n // Skip static methods (tools operate on instances)\n if (method.isStatic) {\n return false;\n }\n\n // Handle 'all' mode\n if (config.callable === 'all') {\n return true;\n }\n\n // Handle 'public-async' mode\n if (config.callable === 'public-async') {\n return method.async;\n }\n\n // Handle explicit array of method names\n if (Array.isArray(config.callable)) {\n return config.callable.includes(method.name);\n }\n\n return false;\n}\n\n/**\n * Generates an AITool definition from a method definition\n *\n * @param method - Method definition from AST scanner\n * @param config - AI configuration for custom descriptions\n * @returns AITool definition for LLM function calling\n */\nexport function generateToolFromMethod(\n method: MethodDefinition,\n config?: AiConfig,\n): AITool {\n // Build parameters JSON Schema. `required` is a guaranteed-present local\n // array while collecting, then attached only when non-empty — avoids both\n // optional-chaining on an invariant and a post-hoc `delete`.\n const properties: ToolParametersSchema['properties'] = {};\n const required: string[] = [];\n\n for (const param of method.parameters) {\n // Convert parameter type to JSON Schema\n properties[param.name] = convertTypeToJsonSchema(param.type);\n\n // Add to required if not optional\n if (!param.optional) {\n required.push(param.name);\n }\n\n // Add default value if present\n if (param.default !== undefined) {\n properties[param.name].default = param.default;\n }\n }\n\n const parameters: ToolParametersSchema = {\n type: 'object',\n properties,\n ...(required.length > 0 ? { required } : {}),\n };\n\n // Get description (custom override or from JSDoc)\n const description =\n config?.descriptions?.[method.name] ||\n method.description ||\n `Call the ${method.name} method`;\n\n return {\n type: 'function',\n function: {\n name: method.name,\n description,\n parameters,\n },\n };\n}\n\n/**\n * Generates tool manifest from method definitions\n *\n * @param methods - Array of method definitions from AST scanner\n * @param config - AI configuration from @smrt decorator\n * @returns Array of AITool definitions for LLM function calling\n */\nexport function generateToolManifest(\n methods: MethodDefinition[],\n config?: AiConfig,\n): AITool[] {\n const tools: AITool[] = [];\n\n for (const method of methods) {\n if (shouldIncludeMethod(method, config)) {\n const tool = generateToolFromMethod(method, config);\n tools.push(tool);\n }\n }\n\n return tools;\n}\n"],"mappings":";;;;;;;AA4DA,SAAgB,wBAAwB,QAA4B;CAElE,MAAM,YAAY,OAAO,KAAK;CAG9B,IAAI,cAAc,UAChB,OAAO,EAAE,MAAM,SAAS;CAE1B,IAAI,cAAc,UAChB,OAAO,EAAE,MAAM,SAAS;CAE1B,IAAI,cAAc,WAChB,OAAO,EAAE,MAAM,UAAU;CAE3B,IAAI,cAAc,QAChB,OAAO,EAAE,MAAM,OAAO;CAExB,IAAI,cAAc,SAAS,cAAc,WACvC,OAAO,CAAC;CAIV,IAAI,UAAU,SAAS,IAAI,GAEzB,OAAO;EACL,MAAM;EACN,OAAO,wBAHQ,UAAU,MAAM,GAAG,EAGH,CAAQ;CACzC;CAIF,MAAM,aAAa,UAAU,MAAM,eAAe;CAClD,IAAI,YACF,OAAO;EACL,MAAM;EACN,OAAO,wBAAwB,WAAW,EAAE;CAC9C;CAIF,IAAI,UAAU,SAAS,GAAG,GAAG;EAC3B,MAAM,UAAU,UAAU,MAAM,GAAG,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK,CAAC;EAGxD,IAAI,QAAQ,OAAO,QAAQ,IAAI,WAAW,GAAG,KAAK,IAAI,SAAS,GAAG,CAAC,GACjE,OAAO;GACL,MAAM;GACN,MAAM,QAAQ,KAAK,QAAQ,IAAI,MAAM,GAAG,EAAE,CAAC;EAC7C;EAIF,OAAO,EACL,OAAO,QAAQ,IAAI,uBAAuB,EAC5C;CACF;CAGA,IAAI,UAAU,WAAW,GAAG,KAAK,UAAU,SAAS,GAAG,GACrD,OAAO,EAAE,MAAM,SAAS;CAI1B,IAAI,UAAU,WAAW,SAAS,GAChC,OAAO,EAAE,MAAM,SAAS;CAI1B,OAAO;EAAE,MAAM;EAAU,aAAa,oBAAoB;CAAY;AACxE;;;;;;;;AASA,SAAgB,oBACd,QACA,QACS;CAET,IAAI,CAAC,UAAU,CAAC,OAAO,UACrB,OAAO;CAIT,IAAI,OAAO,SAAS,SAAS,OAAO,IAAI,GACtC,OAAO;CAIT,IAAI,CAAC,OAAO,UACV,OAAO;CAIT,IAAI,OAAO,UACT,OAAO;CAIT,IAAI,OAAO,aAAa,OACtB,OAAO;CAIT,IAAI,OAAO,aAAa,gBACtB,OAAO,OAAO;CAIhB,IAAI,MAAM,QAAQ,OAAO,QAAQ,GAC/B,OAAO,OAAO,SAAS,SAAS,OAAO,IAAI;CAG7C,OAAO;AACT;;;;;;;;AASA,SAAgB,uBACd,QACA,QACQ;CAIR,MAAM,aAAiD,CAAC;CACxD,MAAM,WAAqB,CAAC;CAE5B,KAAK,MAAM,SAAS,OAAO,YAAY;EAErC,WAAW,MAAM,QAAQ,wBAAwB,MAAM,IAAI;EAG3D,IAAI,CAAC,MAAM,UACT,SAAS,KAAK,MAAM,IAAI;EAI1B,IAAI,MAAM,YAAY,KAAA,GACpB,WAAW,MAAM,KAAK,CAAC,UAAU,MAAM;CAE3C;CAEA,MAAM,aAAmC;EACvC,MAAM;EACN;EACA,GAAI,SAAS,SAAS,IAAI,EAAE,SAAS,IAAI,CAAC;CAC5C;CAGA,MAAM,cACJ,QAAQ,eAAe,OAAO,SAC9B,OAAO,eACP,YAAY,OAAO,KAAK;CAE1B,OAAO;EACL,MAAM;EACN,UAAU;GACR,MAAM,OAAO;GACb;GACA;EACF;CACF;AACF;;;;;;;;AASA,SAAgB,qBACd,SACA,QACU;CACV,MAAM,QAAkB,CAAC;CAEzB,KAAK,MAAM,UAAU,SACnB,IAAI,oBAAoB,QAAQ,MAAM,GAAG;EACvC,MAAM,OAAO,uBAAuB,QAAQ,MAAM;EAClD,MAAM,KAAK,IAAI;CACjB;CAGF,OAAO;AACT"}
1
+ {"version":3,"file":"tool-generator.js","names":[],"sources":["../../src/tools/tool-generator.ts"],"sourcesContent":["/**\n * Tool Manifest Generation for AI Function Calling\n *\n * This module converts TypeScript method definitions from the AST scanner\n * into AI tool format at build time for use with LLM function calling.\n */\n\nimport type { AITool } from '@happyvertical/ai';\nimport type { MethodDefinition } from '../scanner/types.js';\n\n/**\n * A JSON Schema fragment. The shape is open-ended (different keywords appear\n * for primitives, arrays, unions, etc.), so values are `unknown`; callers\n * narrow specific keys (e.g. `.default`) at the point of use.\n */\ntype JsonSchema = Record<string, unknown>;\n\n/**\n * The `parameters` object of a generated tool: a JSON Schema `object` with a\n * `properties` bag (each entry a nested schema) and a `required` name list.\n */\ninterface ToolParametersSchema {\n type: 'object';\n properties: Record<string, JsonSchema>;\n /**\n * Optional so the empty array can be `delete`d (omitting the key from the\n * emitted JSON Schema). It is always present while parameters are collected.\n */\n required?: string[];\n}\n\n/**\n * Configuration for AI-callable methods\n */\nexport interface AiConfig {\n /**\n * Methods that AI can call\n * - Array of method names, e.g., ['analyze', 'validate']\n * - 'public-async' to auto-include all public async methods\n * - 'all' to include all methods (not recommended)\n */\n callable?: string[] | 'public-async' | 'all';\n\n /**\n * Methods to exclude from AI calling (higher priority than callable)\n */\n exclude?: string[];\n\n /**\n * Additional tool descriptions to override method JSDoc\n */\n descriptions?: Record<string, string>;\n}\n\n/**\n * Converts a TypeScript type string to JSON Schema format\n *\n * @param tsType - TypeScript type string (e.g., 'string', 'number', '{ foo: string }')\n * @returns JSON Schema representation\n */\nexport function convertTypeToJsonSchema(tsType: string): JsonSchema {\n // Remove whitespace\n const cleanType = tsType.trim();\n\n // Primitive types\n if (cleanType === 'string') {\n return { type: 'string' };\n }\n if (cleanType === 'number') {\n return { type: 'number' };\n }\n if (cleanType === 'boolean') {\n return { type: 'boolean' };\n }\n if (cleanType === 'null') {\n return { type: 'null' };\n }\n if (cleanType === 'any' || cleanType === 'unknown') {\n return {}; // No type constraint\n }\n\n // Array types\n if (cleanType.endsWith('[]')) {\n const itemType = cleanType.slice(0, -2);\n return {\n type: 'array',\n items: convertTypeToJsonSchema(itemType),\n };\n }\n\n // Array<T> syntax\n const arrayMatch = cleanType.match(/^Array<(.+)>$/);\n if (arrayMatch) {\n return {\n type: 'array',\n items: convertTypeToJsonSchema(arrayMatch[1]),\n };\n }\n\n // Union types with literal values (e.g., 'shallow' | 'deep')\n if (cleanType.includes('|')) {\n const options = cleanType.split('|').map((s) => s.trim());\n\n // Check if all options are string literals\n if (options.every((opt) => opt.startsWith(\"'\") && opt.endsWith(\"'\"))) {\n return {\n type: 'string',\n enum: options.map((opt) => opt.slice(1, -1)), // Remove quotes\n };\n }\n\n // Mixed union - use oneOf\n return {\n oneOf: options.map(convertTypeToJsonSchema),\n };\n }\n\n // Object types - basic support\n if (cleanType.startsWith('{') && cleanType.endsWith('}')) {\n return { type: 'object' };\n }\n\n // Record<string, any> and similar\n if (cleanType.startsWith('Record<')) {\n return { type: 'object' };\n }\n\n // Default fallback\n return { type: 'string', description: `TypeScript type: ${cleanType}` };\n}\n\n/**\n * Determines if a method should be included as an AI-callable tool\n *\n * @param method - Method definition from AST scanner\n * @param config - AI configuration from @smrt decorator\n * @returns True if method should be callable by AI\n */\nexport function shouldIncludeMethod(\n method: MethodDefinition,\n config?: AiConfig,\n): boolean {\n // Skip if no AI config\n if (!config?.callable) {\n return false;\n }\n\n // Check exclusions first (higher priority)\n if (config.exclude?.includes(method.name)) {\n return false;\n }\n\n // Skip private methods always\n if (!method.isPublic) {\n return false;\n }\n\n // Skip static methods (tools operate on instances)\n if (method.isStatic) {\n return false;\n }\n\n // Handle 'all' mode\n if (config.callable === 'all') {\n return true;\n }\n\n // Handle 'public-async' mode\n if (config.callable === 'public-async') {\n return method.async;\n }\n\n // Handle explicit array of method names\n if (Array.isArray(config.callable)) {\n return config.callable.includes(method.name);\n }\n\n return false;\n}\n\n/**\n * Generates an AITool definition from a method definition\n *\n * @param method - Method definition from AST scanner\n * @param config - AI configuration for custom descriptions\n * @returns AITool definition for LLM function calling\n */\nexport function generateToolFromMethod(\n method: MethodDefinition,\n config?: AiConfig,\n): AITool {\n // Build parameters JSON Schema. `required` is a guaranteed-present local\n // array while collecting, then attached only when non-empty — avoids both\n // optional-chaining on an invariant and a post-hoc `delete`.\n const properties: ToolParametersSchema['properties'] = {};\n const required: string[] = [];\n\n for (const param of method.parameters) {\n // Convert parameter type to JSON Schema\n properties[param.name] = convertTypeToJsonSchema(param.type);\n\n // Add to required if not optional\n if (!param.optional) {\n required.push(param.name);\n }\n\n // Add default value if present\n if (param.default !== undefined) {\n properties[param.name].default = param.default;\n }\n }\n\n const parameters: ToolParametersSchema = {\n type: 'object',\n properties,\n ...(required.length > 0 ? { required } : {}),\n };\n\n // Get description (custom override or from JSDoc)\n const description =\n config?.descriptions?.[method.name] ||\n method.description ||\n `Call the ${method.name} method`;\n\n return {\n type: 'function',\n function: {\n name: method.name,\n description,\n parameters,\n },\n };\n}\n\n/**\n * Generates tool manifest from method definitions\n *\n * @param methods - Array of method definitions from AST scanner\n * @param config - AI configuration from @smrt decorator\n * @returns Array of AITool definitions for LLM function calling\n */\nexport function generateToolManifest(\n methods: MethodDefinition[],\n config?: AiConfig,\n): AITool[] {\n const tools: AITool[] = [];\n\n for (const method of methods) {\n if (shouldIncludeMethod(method, config)) {\n const tool = generateToolFromMethod(method, config);\n tools.push(tool);\n }\n }\n\n return tools;\n}\n"],"mappings":";;;;;;;AA4DA,SAAgB,wBAAwB,QAA4B;CAElE,MAAM,YAAY,OAAO,KAAK;CAG9B,IAAI,cAAc,UAChB,OAAO,EAAE,MAAM,SAAS;CAE1B,IAAI,cAAc,UAChB,OAAO,EAAE,MAAM,SAAS;CAE1B,IAAI,cAAc,WAChB,OAAO,EAAE,MAAM,UAAU;CAE3B,IAAI,cAAc,QAChB,OAAO,EAAE,MAAM,OAAO;CAExB,IAAI,cAAc,SAAS,cAAc,WACvC,OAAO,CAAC;CAIV,IAAI,UAAU,SAAS,IAAI,GAEzB,OAAO;EACL,MAAM;EACN,OAAO,wBAHQ,UAAU,MAAM,GAAG,EAGH,CAAQ;CACzC;CAIF,MAAM,aAAa,UAAU,MAAM,eAAe;CAClD,IAAI,YACF,OAAO;EACL,MAAM;EACN,OAAO,wBAAwB,WAAW,EAAE;CAC9C;CAIF,IAAI,UAAU,SAAS,GAAG,GAAG;EAC3B,MAAM,UAAU,UAAU,MAAM,GAAG,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK,CAAC;EAGxD,IAAI,QAAQ,OAAO,QAAQ,IAAI,WAAW,GAAG,KAAK,IAAI,SAAS,GAAG,CAAC,GACjE,OAAO;GACL,MAAM;GACN,MAAM,QAAQ,KAAK,QAAQ,IAAI,MAAM,GAAG,EAAE,CAAC;EAC7C;EAIF,OAAO,EACL,OAAO,QAAQ,IAAI,uBAAuB,EAC5C;CACF;CAGA,IAAI,UAAU,WAAW,GAAG,KAAK,UAAU,SAAS,GAAG,GACrD,OAAO,EAAE,MAAM,SAAS;CAI1B,IAAI,UAAU,WAAW,SAAS,GAChC,OAAO,EAAE,MAAM,SAAS;CAI1B,OAAO;EAAE,MAAM;EAAU,aAAa,oBAAoB;CAAY;AACxE;;;;;;;;AASA,SAAgB,oBACd,QACA,QACS;CAET,IAAI,CAAC,QAAQ,UACX,OAAO;CAIT,IAAI,OAAO,SAAS,SAAS,OAAO,IAAI,GACtC,OAAO;CAIT,IAAI,CAAC,OAAO,UACV,OAAO;CAIT,IAAI,OAAO,UACT,OAAO;CAIT,IAAI,OAAO,aAAa,OACtB,OAAO;CAIT,IAAI,OAAO,aAAa,gBACtB,OAAO,OAAO;CAIhB,IAAI,MAAM,QAAQ,OAAO,QAAQ,GAC/B,OAAO,OAAO,SAAS,SAAS,OAAO,IAAI;CAG7C,OAAO;AACT;;;;;;;;AASA,SAAgB,uBACd,QACA,QACQ;CAIR,MAAM,aAAiD,CAAC;CACxD,MAAM,WAAqB,CAAC;CAE5B,KAAK,MAAM,SAAS,OAAO,YAAY;EAErC,WAAW,MAAM,QAAQ,wBAAwB,MAAM,IAAI;EAG3D,IAAI,CAAC,MAAM,UACT,SAAS,KAAK,MAAM,IAAI;EAI1B,IAAI,MAAM,YAAY,KAAA,GACpB,WAAW,MAAM,KAAK,CAAC,UAAU,MAAM;CAE3C;CAEA,MAAM,aAAmC;EACvC,MAAM;EACN;EACA,GAAI,SAAS,SAAS,IAAI,EAAE,SAAS,IAAI,CAAC;CAC5C;CAGA,MAAM,cACJ,QAAQ,eAAe,OAAO,SAC9B,OAAO,eACP,YAAY,OAAO,KAAK;CAE1B,OAAO;EACL,MAAM;EACN,UAAU;GACR,MAAM,OAAO;GACb;GACA;EACF;CACF;AACF;;;;;;;;AASA,SAAgB,qBACd,SACA,QACU;CACV,MAAM,QAAkB,CAAC;CAEzB,KAAK,MAAM,UAAU,SACnB,IAAI,oBAAoB,QAAQ,MAAM,GAAG;EACvC,MAAM,OAAO,uBAAuB,QAAQ,MAAM;EAClD,MAAM,KAAK,IAAI;CACjB;CAGF,OAAO;AACT"}
@@ -1,5 +1,19 @@
1
1
  import { SmartObjectManifest } from '../scanner/types';
2
2
  import { SvelteKitOptions } from './sveltekit-generator.js';
3
+ /**
4
+ * Pick the class the route resolves its database through: the first
5
+ * non-collection object by sorted manifest key, for determinism across
6
+ * builds.
7
+ *
8
+ * Returns the manifest **registry key** verbatim (which may be
9
+ * package-qualified, e.g. `@happyvertical/smrt-ledgers:Account`) — the emitted
10
+ * route passes it straight to `getCollection()`, exactly as the generated CRUD
11
+ * routes do. Collapsing it to a simple class name would resolve ambiguously
12
+ * when two loaded packages declare the same simple name (mirrors the #1778
13
+ * verbatim-key fix and the sync-apply route's `registryKey`).
14
+ */
15
+ export declare function resolveAnchorClassName(manifest: SmartObjectManifest): string | null;
16
+ export declare function manifestHasTenantScopedObject(manifest: SmartObjectManifest): boolean;
3
17
  /**
4
18
  * Generate the `_changes/+server.ts` route. Returns true when a route was
5
19
  * written. Disabled with `sveltekit: { changesRoute: { enabled: false } }`;
@@ -1 +1 @@
1
- {"version":3,"file":"changes-route.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/changes-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,OAAO,KAAK,EAEV,mBAAmB,EACpB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAyCjE;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CA0BT"}
1
+ {"version":3,"file":"changes-route.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/changes-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,OAAO,KAAK,EAEV,mBAAmB,EACpB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAWjE;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,mBAAmB,GAC5B,MAAM,GAAG,IAAI,CASf;AAED,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,mBAAmB,GAC5B,OAAO,CAKT;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CA0BT"}
@@ -90,7 +90,7 @@ function generateChangesRouteTemplate(anchorClassName, tenantScoped) {
90
90
  // (comma-separated), limit. A response with resyncRequired: true (still
91
91
  // HTTP 200 — protocol state, not an error) means the cursor cannot be
92
92
  // served incrementally (pruned or foreign) and the client must re-fetch
93
- // in full before resuming polling.
93
+ // in full before resuming polling from resyncCursor.
94
94
 
95
95
  import { error, json } from '@sveltejs/kit';
96
96
  import { getTenantScopedChangesSince } from '@happyvertical/smrt-core';
@@ -169,6 +169,6 @@ export const GET: RequestHandler = async ({ locals, url }) => {
169
169
  `;
170
170
  }
171
171
  //#endregion
172
- export { generateChangesRoute };
172
+ export { generateChangesRoute, manifestHasTenantScopedObject, resolveAnchorClassName };
173
173
 
174
174
  //# sourceMappingURL=changes-route.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"changes-route.js","names":[],"sources":["../../src/vite-plugin/changes-route.ts"],"sourcesContent":["/**\n * SvelteKit `_changes` route generation for the change feed (issue #1758).\n *\n * Emits `{routesDir}/_changes/+server.ts` — an auth-guarded, tenant-scoped\n * GET endpoint over the `_smrt_changes` log, part of the client/mobile sync\n * contract (PRD #1755). Kept in its own module so `sveltekit-generator.ts`\n * only carries a one-line registration.\n *\n * Design notes:\n * - **Fail-closed auth** (#1540 posture): the handler requires an\n * authenticated principal on `locals`. The feed spans every table, so\n * per-model `api: { public }` opt-outs deliberately do not apply.\n * - **Tenant scoping**: when the project has tenant-scoped objects, the\n * route establishes tenant context from `locals` exactly like generated\n * collection routes, then reads through\n * `getTenantScopedChangesSince()` — a tenant only ever sees its own\n * changes plus global rows.\n * - **Database resolution**: the route anchors on the project's first\n * generated collection (alphabetical) via the consumer's existing\n * `getCollection()` helper, inheriting its configuration, request-scoped\n * database support and system-table bootstrap. Multi-database projects\n * (per-object `db` overrides) see the anchor collection's feed.\n * - Cleanup rides the existing generated-route sweep: the emitted file\n * starts with {@link AUTO_GENERATED_ROUTE_HEADER}.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type {\n SmartObjectDefinition,\n SmartObjectManifest,\n} from '../scanner/types';\nimport { AUTO_GENERATED_ROUTE_HEADER } from './route-header.js';\nimport type { SvelteKitOptions } from './sveltekit-generator.js';\n\n/**\n * Mirrors `isCollectionClass` in `sveltekit-generator.ts` (module-private\n * there): collection classes share route paths with their item class and\n * never anchor routes themselves.\n */\nfunction isCollectionDefinition(objectDef: SmartObjectDefinition): boolean {\n return objectDef.extends === 'SmrtCollection' || !!objectDef.extendsTypeArg;\n}\n\n/**\n * Pick the class the route resolves its database through: the first\n * non-collection object by sorted manifest key, for determinism across\n * builds.\n *\n * Returns the manifest **registry key** verbatim (which may be\n * package-qualified, e.g. `@happyvertical/smrt-ledgers:Account`) — the emitted\n * route passes it straight to `getCollection()`, exactly as the generated CRUD\n * routes do. Collapsing it to a simple class name would resolve ambiguously\n * when two loaded packages declare the same simple name (mirrors the #1778\n * verbatim-key fix and the sync-apply route's `registryKey`).\n */\nfunction resolveAnchorClassName(manifest: SmartObjectManifest): string | null {\n const sortedNames = Object.keys(manifest.objects).sort();\n for (const name of sortedNames) {\n const def = manifest.objects[name];\n if (def && !isCollectionDefinition(def)) {\n return name;\n }\n }\n return null;\n}\n\nfunction manifestHasTenantScopedObject(manifest: SmartObjectManifest): boolean {\n return Object.values(manifest.objects).some(\n (def) =>\n !isCollectionDefinition(def) && !!def.decoratorConfig?.tenantScoped,\n );\n}\n\n/**\n * Generate the `_changes/+server.ts` route. Returns true when a route was\n * written. Disabled with `sveltekit: { changesRoute: { enabled: false } }`;\n * skipped (with a log line) when the manifest has no objects to anchor the\n * database on.\n */\nexport function generateChangesRoute(\n projectRoot: string,\n manifest: SmartObjectManifest,\n options: SvelteKitOptions,\n): boolean {\n if (options.changesRoute?.enabled === false) {\n return false;\n }\n\n const anchorClassName = resolveAnchorClassName(manifest);\n if (!anchorClassName) {\n console.log(\n '[smrt] Skipping _changes route - no SMRT objects to anchor the database on',\n );\n return false;\n }\n\n const routeDir = join(projectRoot, options.routesDir, '_changes');\n const content = generateChangesRouteTemplate(\n anchorClassName,\n manifestHasTenantScopedObject(manifest),\n );\n\n if (!existsSync(routeDir)) {\n mkdirSync(routeDir, { recursive: true });\n }\n const filePath = join(routeDir, '+server.ts');\n writeFileSync(filePath, content, 'utf-8');\n console.log(`[smrt] Generated: ${filePath}`);\n return true;\n}\n\nfunction generateChangesRouteTemplate(\n anchorClassName: string,\n tenantScoped: boolean,\n): string {\n const tenantHelper = tenantScoped\n ? `\nimport { enterTenantContext, hasTenantContext } from '@happyvertical/smrt-tenancy';\n\nfunction establishTenantContext(locals: unknown): void {\n if (hasTenantContext()) return;\n if (!locals || typeof locals !== 'object') return;\n const l = locals as Record<string, unknown>;\n const user = l.user as Record<string, unknown> | undefined;\n const session = l.session as Record<string, unknown> | undefined;\n const tenantId = l.tenantId ?? user?.tenantId ?? session?.tenantId;\n if (typeof tenantId === 'string' && tenantId) {\n enterTenantContext({ tenantId });\n }\n}\n`\n : '';\n const tenantCall = tenantScoped ? '\\n establishTenantContext(locals);' : '';\n\n return `${AUTO_GENERATED_ROUTE_HEADER}\n// DO NOT EDIT - changes will be overwritten\n//\n// GET /_changes — cursor read over the _smrt_changes change feed (#1758).\n// Part of the client/mobile sync contract: poll with the returned cursor to\n// observe every committed framework save/delete (deletes are tombstones)\n// exactly once. Query params: since (cursor, default 0), tables\n// (comma-separated), limit. A response with resyncRequired: true (still\n// HTTP 200 — protocol state, not an error) means the cursor cannot be\n// served incrementally (pruned or foreign) and the client must re-fetch\n// in full before resuming polling.\n\nimport { error, json } from '@sveltejs/kit';\nimport { getTenantScopedChangesSince } from '@happyvertical/smrt-core';\nimport { getCollection } from '$lib/server/smrt';\nimport type { RequestHandler } from './$types';\n\n// Fail-closed authorization (#1540): the change feed spans every table, so\n// it is never public — an authenticated principal on \\`locals\\` is required.\nfunction hasAuthenticatedPrincipal(locals: unknown): boolean {\n if (!locals || typeof locals !== 'object') return false;\n const l = locals as Record<string, unknown>;\n const isResolvedPrincipal = (v: unknown) =>\n typeof v === 'object' && v !== null;\n return (\n isResolvedPrincipal(l.user) ||\n isResolvedPrincipal(l.session) ||\n l.smrtAuth === true\n );\n}\n\nfunction requireRouteAuth(locals: unknown): void {\n if (!hasAuthenticatedPrincipal(locals)) {\n throw error(401, 'Authentication required');\n }\n}\n${tenantHelper}\nexport const GET: RequestHandler = async ({ locals, url }) => {\n requireRouteAuth(locals);${tenantCall}\n\n const since = Number(url.searchParams.get('since') ?? '0');\n if (!Number.isFinite(since) || since < 0) {\n throw error(400, \"'since' must be a non-negative number\");\n }\n\n let limit: number | undefined;\n const limitParam = url.searchParams.get('limit');\n if (limitParam !== null) {\n limit = Number(limitParam);\n if (!Number.isFinite(limit) || limit < 1) {\n throw error(400, \"'limit' must be a positive number\");\n }\n }\n\n const tablesParam = url.searchParams.get('tables');\n const tables = tablesParam\n ? tablesParam\n .split(',')\n .map((table) => table.trim())\n .filter(Boolean)\n : undefined;\n\n // The feed lives in the project's database; anchor on the\n // ${anchorClassName} collection to reuse its configured connection.\n const collection = await getCollection('${anchorClassName}');\n const page = await getTenantScopedChangesSince(collection.db, {\n since,\n tables,\n limit,\n });\n return json(page);\n};\n`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,SAAS,uBAAuB,WAA2C;CACzE,OAAO,UAAU,YAAY,oBAAoB,CAAC,CAAC,UAAU;AAC/D;;;;;;;;;;;;;AAcA,SAAS,uBAAuB,UAA8C;CAC5E,MAAM,cAAc,OAAO,KAAK,SAAS,OAAO,CAAC,CAAC,KAAK;CACvD,KAAK,MAAM,QAAQ,aAAa;EAC9B,MAAM,MAAM,SAAS,QAAQ;EAC7B,IAAI,OAAO,CAAC,uBAAuB,GAAG,GACpC,OAAO;CAEX;CACA,OAAO;AACT;AAEA,SAAS,8BAA8B,UAAwC;CAC7E,OAAO,OAAO,OAAO,SAAS,OAAO,CAAC,CAAC,MACpC,QACC,CAAC,uBAAuB,GAAG,KAAK,CAAC,CAAC,IAAI,iBAAiB,YAC3D;AACF;;;;;;;AAQA,SAAgB,qBACd,aACA,UACA,SACS;CACT,IAAI,QAAQ,cAAc,YAAY,OACpC,OAAO;CAGT,MAAM,kBAAkB,uBAAuB,QAAQ;CACvD,IAAI,CAAC,iBAAiB;EACpB,QAAQ,IACN,4EACF;EACA,OAAO;CACT;CAEA,MAAM,WAAW,KAAK,aAAa,QAAQ,WAAW,UAAU;CAChE,MAAM,UAAU,6BACd,iBACA,8BAA8B,QAAQ,CACxC;CAEA,IAAI,CAAC,WAAW,QAAQ,GACtB,UAAU,UAAU,EAAE,WAAW,KAAK,CAAC;CAEzC,MAAM,WAAW,KAAK,UAAU,YAAY;CAC5C,cAAc,UAAU,SAAS,OAAO;CACxC,QAAQ,IAAI,qBAAqB,UAAU;CAC3C,OAAO;AACT;AAEA,SAAS,6BACP,iBACA,cACQ;CAoBR,OAAO,GAAG,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAnBjB,eACjB;;;;;;;;;;;;;;IAeA,GAuCS;;6BAtCM,eAAe,wCAAwC,GAwCpC;;;;;;;;;;;;;;;;;;;;;;;;;OAyBjC,gBAAgB;4CACqB,gBAAgB;;;;;;;;;AAS5D"}
1
+ {"version":3,"file":"changes-route.js","names":[],"sources":["../../src/vite-plugin/changes-route.ts"],"sourcesContent":["/**\n * SvelteKit `_changes` route generation for the change feed (issue #1758).\n *\n * Emits `{routesDir}/_changes/+server.ts` — an auth-guarded, tenant-scoped\n * GET endpoint over the `_smrt_changes` log, part of the client/mobile sync\n * contract (PRD #1755). Kept in its own module so `sveltekit-generator.ts`\n * only carries a one-line registration.\n *\n * Design notes:\n * - **Fail-closed auth** (#1540 posture): the handler requires an\n * authenticated principal on `locals`. The feed spans every table, so\n * per-model `api: { public }` opt-outs deliberately do not apply.\n * - **Tenant scoping**: when the project has tenant-scoped objects, the\n * route establishes tenant context from `locals` exactly like generated\n * collection routes, then reads through\n * `getTenantScopedChangesSince()` — a tenant only ever sees its own\n * changes plus global rows.\n * - **Database resolution**: the route anchors on the project's first\n * generated collection (alphabetical) via the consumer's existing\n * `getCollection()` helper, inheriting its configuration, request-scoped\n * database support and system-table bootstrap. Multi-database projects\n * (per-object `db` overrides) see the anchor collection's feed.\n * - Cleanup rides the existing generated-route sweep: the emitted file\n * starts with {@link AUTO_GENERATED_ROUTE_HEADER}.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type {\n SmartObjectDefinition,\n SmartObjectManifest,\n} from '../scanner/types';\nimport { AUTO_GENERATED_ROUTE_HEADER } from './route-header.js';\nimport type { SvelteKitOptions } from './sveltekit-generator.js';\n\n/**\n * Mirrors `isCollectionClass` in `sveltekit-generator.ts` (module-private\n * there): collection classes share route paths with their item class and\n * never anchor routes themselves.\n */\nfunction isCollectionDefinition(objectDef: SmartObjectDefinition): boolean {\n return objectDef.extends === 'SmrtCollection' || !!objectDef.extendsTypeArg;\n}\n\n/**\n * Pick the class the route resolves its database through: the first\n * non-collection object by sorted manifest key, for determinism across\n * builds.\n *\n * Returns the manifest **registry key** verbatim (which may be\n * package-qualified, e.g. `@happyvertical/smrt-ledgers:Account`) — the emitted\n * route passes it straight to `getCollection()`, exactly as the generated CRUD\n * routes do. Collapsing it to a simple class name would resolve ambiguously\n * when two loaded packages declare the same simple name (mirrors the #1778\n * verbatim-key fix and the sync-apply route's `registryKey`).\n */\nexport function resolveAnchorClassName(\n manifest: SmartObjectManifest,\n): string | null {\n const sortedNames = Object.keys(manifest.objects).sort();\n for (const name of sortedNames) {\n const def = manifest.objects[name];\n if (def && !isCollectionDefinition(def)) {\n return name;\n }\n }\n return null;\n}\n\nexport function manifestHasTenantScopedObject(\n manifest: SmartObjectManifest,\n): boolean {\n return Object.values(manifest.objects).some(\n (def) =>\n !isCollectionDefinition(def) && !!def.decoratorConfig?.tenantScoped,\n );\n}\n\n/**\n * Generate the `_changes/+server.ts` route. Returns true when a route was\n * written. Disabled with `sveltekit: { changesRoute: { enabled: false } }`;\n * skipped (with a log line) when the manifest has no objects to anchor the\n * database on.\n */\nexport function generateChangesRoute(\n projectRoot: string,\n manifest: SmartObjectManifest,\n options: SvelteKitOptions,\n): boolean {\n if (options.changesRoute?.enabled === false) {\n return false;\n }\n\n const anchorClassName = resolveAnchorClassName(manifest);\n if (!anchorClassName) {\n console.log(\n '[smrt] Skipping _changes route - no SMRT objects to anchor the database on',\n );\n return false;\n }\n\n const routeDir = join(projectRoot, options.routesDir, '_changes');\n const content = generateChangesRouteTemplate(\n anchorClassName,\n manifestHasTenantScopedObject(manifest),\n );\n\n if (!existsSync(routeDir)) {\n mkdirSync(routeDir, { recursive: true });\n }\n const filePath = join(routeDir, '+server.ts');\n writeFileSync(filePath, content, 'utf-8');\n console.log(`[smrt] Generated: ${filePath}`);\n return true;\n}\n\nfunction generateChangesRouteTemplate(\n anchorClassName: string,\n tenantScoped: boolean,\n): string {\n const tenantHelper = tenantScoped\n ? `\nimport { enterTenantContext, hasTenantContext } from '@happyvertical/smrt-tenancy';\n\nfunction establishTenantContext(locals: unknown): void {\n if (hasTenantContext()) return;\n if (!locals || typeof locals !== 'object') return;\n const l = locals as Record<string, unknown>;\n const user = l.user as Record<string, unknown> | undefined;\n const session = l.session as Record<string, unknown> | undefined;\n const tenantId = l.tenantId ?? user?.tenantId ?? session?.tenantId;\n if (typeof tenantId === 'string' && tenantId) {\n enterTenantContext({ tenantId });\n }\n}\n`\n : '';\n const tenantCall = tenantScoped ? '\\n establishTenantContext(locals);' : '';\n\n return `${AUTO_GENERATED_ROUTE_HEADER}\n// DO NOT EDIT - changes will be overwritten\n//\n// GET /_changes — cursor read over the _smrt_changes change feed (#1758).\n// Part of the client/mobile sync contract: poll with the returned cursor to\n// observe every committed framework save/delete (deletes are tombstones)\n// exactly once. Query params: since (cursor, default 0), tables\n// (comma-separated), limit. A response with resyncRequired: true (still\n// HTTP 200 — protocol state, not an error) means the cursor cannot be\n// served incrementally (pruned or foreign) and the client must re-fetch\n// in full before resuming polling from resyncCursor.\n\nimport { error, json } from '@sveltejs/kit';\nimport { getTenantScopedChangesSince } from '@happyvertical/smrt-core';\nimport { getCollection } from '$lib/server/smrt';\nimport type { RequestHandler } from './$types';\n\n// Fail-closed authorization (#1540): the change feed spans every table, so\n// it is never public — an authenticated principal on \\`locals\\` is required.\nfunction hasAuthenticatedPrincipal(locals: unknown): boolean {\n if (!locals || typeof locals !== 'object') return false;\n const l = locals as Record<string, unknown>;\n const isResolvedPrincipal = (v: unknown) =>\n typeof v === 'object' && v !== null;\n return (\n isResolvedPrincipal(l.user) ||\n isResolvedPrincipal(l.session) ||\n l.smrtAuth === true\n );\n}\n\nfunction requireRouteAuth(locals: unknown): void {\n if (!hasAuthenticatedPrincipal(locals)) {\n throw error(401, 'Authentication required');\n }\n}\n${tenantHelper}\nexport const GET: RequestHandler = async ({ locals, url }) => {\n requireRouteAuth(locals);${tenantCall}\n\n const since = Number(url.searchParams.get('since') ?? '0');\n if (!Number.isFinite(since) || since < 0) {\n throw error(400, \"'since' must be a non-negative number\");\n }\n\n let limit: number | undefined;\n const limitParam = url.searchParams.get('limit');\n if (limitParam !== null) {\n limit = Number(limitParam);\n if (!Number.isFinite(limit) || limit < 1) {\n throw error(400, \"'limit' must be a positive number\");\n }\n }\n\n const tablesParam = url.searchParams.get('tables');\n const tables = tablesParam\n ? tablesParam\n .split(',')\n .map((table) => table.trim())\n .filter(Boolean)\n : undefined;\n\n // The feed lives in the project's database; anchor on the\n // ${anchorClassName} collection to reuse its configured connection.\n const collection = await getCollection('${anchorClassName}');\n const page = await getTenantScopedChangesSince(collection.db, {\n since,\n tables,\n limit,\n });\n return json(page);\n};\n`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,SAAS,uBAAuB,WAA2C;CACzE,OAAO,UAAU,YAAY,oBAAoB,CAAC,CAAC,UAAU;AAC/D;;;;;;;;;;;;;AAcA,SAAgB,uBACd,UACe;CACf,MAAM,cAAc,OAAO,KAAK,SAAS,OAAO,CAAC,CAAC,KAAK;CACvD,KAAK,MAAM,QAAQ,aAAa;EAC9B,MAAM,MAAM,SAAS,QAAQ;EAC7B,IAAI,OAAO,CAAC,uBAAuB,GAAG,GACpC,OAAO;CAEX;CACA,OAAO;AACT;AAEA,SAAgB,8BACd,UACS;CACT,OAAO,OAAO,OAAO,SAAS,OAAO,CAAC,CAAC,MACpC,QACC,CAAC,uBAAuB,GAAG,KAAK,CAAC,CAAC,IAAI,iBAAiB,YAC3D;AACF;;;;;;;AAQA,SAAgB,qBACd,aACA,UACA,SACS;CACT,IAAI,QAAQ,cAAc,YAAY,OACpC,OAAO;CAGT,MAAM,kBAAkB,uBAAuB,QAAQ;CACvD,IAAI,CAAC,iBAAiB;EACpB,QAAQ,IACN,4EACF;EACA,OAAO;CACT;CAEA,MAAM,WAAW,KAAK,aAAa,QAAQ,WAAW,UAAU;CAChE,MAAM,UAAU,6BACd,iBACA,8BAA8B,QAAQ,CACxC;CAEA,IAAI,CAAC,WAAW,QAAQ,GACtB,UAAU,UAAU,EAAE,WAAW,KAAK,CAAC;CAEzC,MAAM,WAAW,KAAK,UAAU,YAAY;CAC5C,cAAc,UAAU,SAAS,OAAO;CACxC,QAAQ,IAAI,qBAAqB,UAAU;CAC3C,OAAO;AACT;AAEA,SAAS,6BACP,iBACA,cACQ;CAoBR,OAAO,GAAG,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAnBjB,eACjB;;;;;;;;;;;;;;IAeA,GAuCS;;6BAtCM,eAAe,wCAAwC,GAwCpC;;;;;;;;;;;;;;;;;;;;;;;;;OAyBjC,gBAAgB;4CACqB,gBAAgB;;;;;;;;;AAS5D"}
@@ -0,0 +1,9 @@
1
+ import { SmartObjectManifest } from '../scanner/types';
2
+ import { SvelteKitOptions } from './sveltekit-generator.js';
3
+ /**
4
+ * Generate the `_events/+server.ts` route. Returns true when a route was
5
+ * written. Disabled with `sveltekit: { eventsRoute: { enabled: false } }`;
6
+ * skipped (with a log line) when the manifest has no objects to anchor on.
7
+ */
8
+ export declare function generateEventsRoute(projectRoot: string, manifest: SmartObjectManifest, options: SvelteKitOptions): boolean;
9
+ //# sourceMappingURL=events-route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events-route.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/events-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAIH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAM5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEjE;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CA0BT"}