@happyvertical/smrt-core 0.40.3 → 0.40.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.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "version": "1.0.0",
3
- "timestamp": 1784104737462,
3
+ "timestamp": 1784132777670,
4
4
  "packageName": "@happyvertical/smrt-core",
5
- "packageVersion": "0.40.3",
5
+ "packageVersion": "0.40.4",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-core:SmrtClass": {
8
8
  "name": "smrtclass",
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/prebuild/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAG5D,MAAM,WAAW,eAAe;IAC9B,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAAC;IACvC,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,sCAAsC;IACtC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,IAAI,CAAC,CAkCf;AAiVD;;GAEG;AACH,wBAAsB,2BAA2B,CAC/C,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,IAAI,CAAC,CAkBf"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/prebuild/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAI5D,MAAM,WAAW,eAAe;IAC9B,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAAC;IACvC,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,sCAAsC;IACtC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,IAAI,CAAC,CAkCf;AAyUD;;GAEG;AACH,wBAAsB,2BAA2B,CAC/C,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,IAAI,CAAC,CAkBf"}
@@ -1,4 +1,5 @@
1
1
  import { selectWebCollectionEntries } from "../vite-plugin/web-collections.js";
2
+ import { selectApiClientEntries } from "../vite-plugin/api-client-entries.js";
2
3
  import * as fs from "node:fs";
3
4
  import * as path from "node:path";
4
5
  //#region src/prebuild/index.ts
@@ -126,9 +127,8 @@ declare module '@smrt/client' {
126
127
  }
127
128
 
128
129
  export interface ApiClient {
129
- ${[...new Set(Object.values(manifest.objects).map((obj) => obj.collection))].map((collection) => {
130
- const dataType = Object.entries(manifest.objects).find(([, obj]) => obj.collection === collection)?.[1].className;
131
- return ` ${collection}: CrudOperations<${dataType ? `${dataType}Data` : "any"}>;`;
130
+ ${selectApiClientEntries(manifest).map(({ clientKey, dataInterfaceName }) => {
131
+ return ` ${clientKey}: CrudOperations<${dataInterfaceName}>;`;
132
132
  }).join("\n")}
133
133
  }
134
134
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/prebuild/index.ts"],"sourcesContent":["/**\n * Pre-build utilities for generating TypeScript declarations\n * Solves virtual module resolution by creating physical .d.ts files\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport type { SmartObjectManifest } from '../scanner/types';\nimport { selectWebCollectionEntries } from '../vite-plugin/web-collections.js';\n\nexport interface PrebuildOptions {\n /** Path to manifest file or manifest object */\n manifest: string | SmartObjectManifest;\n /** Output directory for generated types */\n outDir: string;\n /** Include virtual module declarations */\n includeVirtualModules?: boolean;\n /** Include object type definitions */\n includeObjectTypes?: boolean;\n /** Project root path for resolving relative paths */\n projectRoot?: string;\n}\n\n/**\n * Generate TypeScript declaration files from SMRT manifest\n */\nexport async function generateDeclarations(\n options: PrebuildOptions,\n): Promise<void> {\n const {\n manifest: manifestInput,\n outDir,\n includeVirtualModules = true,\n includeObjectTypes = true,\n projectRoot = process.cwd(),\n } = options;\n\n // Load manifest\n const manifest: SmartObjectManifest =\n typeof manifestInput === 'string'\n ? JSON.parse(fs.readFileSync(manifestInput, 'utf-8'))\n : manifestInput;\n\n // Ensure output directory exists\n const fullOutDir = path.isAbsolute(outDir)\n ? outDir\n : path.join(projectRoot, outDir);\n fs.mkdirSync(fullOutDir, { recursive: true });\n\n console.log(`[smrt:prebuild] Generating declarations to ${fullOutDir}`);\n\n if (includeObjectTypes) {\n await generateObjectTypeDeclarations(manifest, fullOutDir);\n }\n\n if (includeVirtualModules) {\n await generateVirtualModuleDeclarations(manifest, fullOutDir);\n }\n\n console.log(\n `[smrt:prebuild] Generated declarations for ${Object.keys(manifest.objects).length} SMRT objects`,\n );\n}\n\n/**\n * Generate TypeScript interfaces for SMRT objects\n */\nasync function generateObjectTypeDeclarations(\n manifest: SmartObjectManifest,\n outDir: string,\n): Promise<void> {\n const interfaces: string[] = [];\n\n // Generate interfaces for each discovered SMRT object\n for (const [_objectName, objectMeta] of Object.entries(manifest.objects)) {\n const fields = objectMeta.fields || {};\n const propertyLines: string[] = [];\n\n // Add standard SmrtObject properties\n propertyLines.push(' id?: string;');\n propertyLines.push(' created_at?: string;');\n propertyLines.push(' updated_at?: string;');\n\n // Add object-specific properties\n for (const [fieldName, fieldDef] of Object.entries(fields)) {\n const type = mapFieldTypeToTypeScript(fieldDef.type);\n const optional = !fieldDef.required ? '?' : '';\n propertyLines.push(` ${fieldName}${optional}: ${type};`);\n }\n\n const interfaceDef = `export interface ${objectMeta.className}Data {\n${propertyLines.join('\\n')}\n}`;\n interfaces.push(interfaceDef);\n }\n\n // Write object types file\n const objectTypesContent = `/**\n * Auto-generated TypeScript interfaces for SMRT objects\n * Generated at build time from @smrt() decorated classes\n *\n * DO NOT EDIT THIS FILE MANUALLY\n */\n\n${interfaces.join('\\n\\n')}\n`;\n\n fs.writeFileSync(path.join(outDir, 'smrt-objects.d.ts'), objectTypesContent);\n}\n\n/**\n * Generate virtual module declarations\n */\nasync function generateVirtualModuleDeclarations(\n manifest: SmartObjectManifest,\n outDir: string,\n): Promise<void> {\n // Generate manifest module declaration\n const manifestDeclaration = `/**\n * Auto-generated manifest module declaration\n */\ndeclare module '@smrt/manifest' {\n export interface SmrtObjectField {\n type: string;\n required?: boolean;\n default?: any;\n }\n\n export interface SmrtObjectMethod {\n name: string;\n parameters: Array<{\n name: string;\n type: string;\n optional?: boolean;\n default?: any;\n }>;\n returnType: string;\n async: boolean;\n isStatic: boolean;\n isPublic: boolean;\n }\n\n export interface SmrtObjectDefinition {\n name: string;\n className: string;\n collection: string;\n filePath: string;\n fields: Record<string, SmrtObjectField>;\n methods: Record<string, SmrtObjectMethod>;\n decoratorConfig: any;\n extends?: string;\n }\n\n export interface SmrtManifest {\n version: string;\n timestamp: number;\n objects: Record<string, SmrtObjectDefinition>;\n }\n\n export const manifest: SmrtManifest;\n export default manifest;\n}`;\n\n // Generate client module declaration\n const collectionNames = [\n ...new Set(Object.values(manifest.objects).map((obj) => obj.collection)),\n ];\n\n const apiClientInterface = collectionNames\n .map((collection) => {\n const dataType = Object.entries(manifest.objects).find(\n ([, obj]) => obj.collection === collection,\n )?.[1].className;\n const interfaceName = dataType ? `${dataType}Data` : 'any';\n return ` ${collection}: CrudOperations<${interfaceName}>;`;\n })\n .join('\\n');\n\n // Wire-shape policy (#1797): the server returns BARE JSON — a bare array for\n // list/search, a bare object for get/create/update — with snake_case field\n // names (created_at, updated_at). These declarations match that shape (no\n // envelope, no camelCase) and are byte-identical to the vite-plugin client\n // declaration. Fetchers reject on non-2xx with a SmrtClientError (#1796).\n const clientDeclaration = `/**\n * Auto-generated API client module declaration\n */\ndeclare module '@smrt/client' {\n /** Shape of a JSON error body carried by a rejected request (SmrtClientError.body). */\n export interface ApiError {\n error?: string;\n message?: string;\n }\n\n /** Typed error thrown by every fetcher on a non-2xx response (#1796). */\n export interface SmrtClientError extends Error {\n name: 'SmrtClientError';\n status: number;\n body?: ApiError | string;\n }\n\n export interface CrudOperations<T = any> {\n list(params?: Record<string, any>): Promise<T[]>;\n get(id: string): Promise<T>;\n create(data: Partial<T>): Promise<T>;\n update(id: string, data: Partial<T>): Promise<T>;\n delete(id: string): Promise<boolean>;\n search(query: string): Promise<T[]>;\n }\n\n export interface ApiClient {\n${apiClientInterface}\n }\n\n export function createClient(basePath?: string): ApiClient;\n export default createClient;\n}`;\n\n // Generate routes module declaration\n const routesDeclaration = `/**\n * Auto-generated routes module declaration\n */\ndeclare module '@smrt/routes' {\n export interface RouteApp {\n get(path: string, handler: (req: any, res: any) => void): void;\n post(path: string, handler: (req: any, res: any) => void): void;\n put(path: string, handler: (req: any, res: any) => void): void;\n delete(path: string, handler: (req: any, res: any) => void): void;\n }\n\n export function setupRoutes(app: RouteApp): void;\n export default setupRoutes;\n}`;\n\n // Generate MCP module declaration\n const mcpDeclaration = `/**\n * Auto-generated MCP module declaration\n */\ndeclare module '@smrt/mcp' {\n export interface McpTool {\n name: string;\n description: string;\n inputSchema: {\n type: string;\n properties: Record<string, any>;\n required?: string[];\n };\n }\n\n export const tools: McpTool[];\n export function createMCPServer(): {\n name: string;\n version: string;\n tools: McpTool[]\n };\n export default createMCPServer;\n}`;\n\n // Generate types module declaration with object imports\n const objectImports = Object.values(manifest.objects)\n .map(\n (obj) =>\n ` export type ${obj.className}Data = import('./smrt-objects').${obj.className}Data;`,\n )\n .join('\\n');\n\n const typesDeclaration = `/**\n * Auto-generated types module declaration\n */\ndeclare module '@smrt/types' {\n${objectImports}\n\n export interface Request {\n params: Record<string, string>;\n query: Record<string, any>;\n json(): Promise<any>;\n }\n\n export interface Response {\n json(data: any, init?: { status?: number }): Response;\n status(code: number): Response;\n }\n}`;\n\n // Generate web collection-definition module declaration (#1761). Selection\n // shares selectWebCollectionEntries with the vite-plugin runtime module and\n // its virt-web d.ts, so this `tsc`-only consumer declaration cannot drift\n // from the emitted values.\n const webCollectionEntries = selectWebCollectionEntries(manifest)\n .map(\n ({ collection, obj }) =>\n ` ${collection}: SmrtWebCollectionDefinition<import('./smrt-objects').${obj.className}Data>;`,\n )\n .join('\\n');\n\n const webDeclaration = `/**\n * Auto-generated web collection-definition module declaration (#1761)\n */\ndeclare module '@smrt/web' {\n export interface SmrtWebFieldDefinition {\n type: string;\n required?: boolean;\n default?: unknown;\n }\n\n export type SmrtWebRelationshipKind =\n | 'foreignKey'\n | 'crossPackageRef'\n | 'oneToMany'\n | 'manyToMany';\n\n /**\n * A manifest-derived edge to a sibling REST collection. Mutating this\n * collection invalidates the caches of the collections named by these edges\n * (#1761 relationship-derived invalidation).\n */\n export interface SmrtWebRelationship {\n field: string;\n kind: SmrtWebRelationshipKind;\n relatedCollection: string;\n }\n\n /** A WebMCP/MCP tool descriptor for one collection action (#1812). */\n export interface WebToolDescriptor {\n action: string;\n name: string;\n description: string;\n inputSchema: Record<string, unknown>;\n readOnly: boolean;\n }\n\n export interface SmrtWebCollectionDefinition<TData = Record<string, unknown>> {\n name: string;\n className: string;\n endpoint: string;\n idField: string;\n actions: string[];\n fields: Record<string, SmrtWebFieldDefinition>;\n /** Manifest-derived relationship edges to sibling REST collections. */\n relationships: SmrtWebRelationship[];\n /** WebMCP/MCP tool descriptors for the exposed actions (#1812). */\n toolDescriptors: WebToolDescriptor[];\n /** Phantom row-type carrier for inference — never present at runtime. */\n _row?: TData;\n }\n\n export interface SmrtWebCollectionDefinitions {\n${webCollectionEntries}\n }\n\n export const collectionDefinitions: SmrtWebCollectionDefinitions;\n export function getCollectionDefinition<\n K extends keyof SmrtWebCollectionDefinitions,\n >(name: K): SmrtWebCollectionDefinitions[K];\n /**\n * Build-time web-collection shape digest (#1764). A deterministic,\n * replica-stable hash of the emitted collection definitions; a change means\n * old persisted client rows may mis-hydrate. Consumers fold it into the\n * durable persistence namespace and the version-awareness updateAvailable\n * contract signal.\n */\n export const manifestHash: string;\n export default collectionDefinitions;\n}`;\n\n // Write all virtual module declarations\n fs.writeFileSync(\n path.join(outDir, 'smrt-manifest.d.ts'),\n manifestDeclaration,\n );\n fs.writeFileSync(path.join(outDir, 'smrt-client.d.ts'), clientDeclaration);\n fs.writeFileSync(path.join(outDir, 'smrt-routes.d.ts'), routesDeclaration);\n fs.writeFileSync(path.join(outDir, 'smrt-mcp.d.ts'), mcpDeclaration);\n fs.writeFileSync(path.join(outDir, 'smrt-types.d.ts'), typesDeclaration);\n fs.writeFileSync(path.join(outDir, 'smrt-web.d.ts'), webDeclaration);\n}\n\n/**\n * Map SMRT field types to TypeScript types\n */\nfunction mapFieldTypeToTypeScript(smrtType: string): string {\n switch (smrtType) {\n case 'text':\n return 'string';\n case 'decimal':\n case 'integer':\n return 'number';\n case 'boolean':\n return 'boolean';\n case 'datetime':\n return 'string | Date';\n case 'json':\n return 'any';\n case 'foreignKey':\n return 'string';\n default:\n return 'any';\n }\n}\n\n/**\n * CLI command for generating declarations\n */\nexport async function generateDeclarationsFromCLI(\n args: string[],\n): Promise<void> {\n const manifestPath = args[0];\n const outDir = args[1] || 'src/types/generated';\n\n if (!manifestPath) {\n console.error('Usage: generate-declarations <manifest-path> [output-dir]');\n process.exit(1);\n }\n\n if (!fs.existsSync(manifestPath)) {\n console.error(`Manifest file not found: ${manifestPath}`);\n process.exit(1);\n }\n\n await generateDeclarations({\n manifest: manifestPath,\n outDir,\n });\n}\n"],"mappings":";;;;;;;;;;;AA0BA,eAAsB,qBACpB,SACe;CACf,MAAM,EACJ,UAAU,eACV,QACA,wBAAwB,MACxB,qBAAqB,MACrB,cAAc,QAAQ,IAAI,MACxB;CAGJ,MAAM,WACJ,OAAO,kBAAkB,WACrB,KAAK,MAAM,GAAG,aAAa,eAAe,OAAO,CAAC,IAClD;CAGN,MAAM,aAAa,KAAK,WAAW,MAAM,IACrC,SACA,KAAK,KAAK,aAAa,MAAM;CACjC,GAAG,UAAU,YAAY,EAAE,WAAW,KAAK,CAAC;CAE5C,QAAQ,IAAI,8CAA8C,YAAY;CAEtE,IAAI,oBACF,MAAM,+BAA+B,UAAU,UAAU;CAG3D,IAAI,uBACF,MAAM,kCAAkC,UAAU,UAAU;CAG9D,QAAQ,IACN,8CAA8C,OAAO,KAAK,SAAS,OAAO,CAAC,CAAC,OAAO,cACrF;AACF;;;;AAKA,eAAe,+BACb,UACA,QACe;CACf,MAAM,aAAuB,CAAC;CAG9B,KAAK,MAAM,CAAC,aAAa,eAAe,OAAO,QAAQ,SAAS,OAAO,GAAG;EACxE,MAAM,SAAS,WAAW,UAAU,CAAC;EACrC,MAAM,gBAA0B,CAAC;EAGjC,cAAc,KAAK,gBAAgB;EACnC,cAAc,KAAK,wBAAwB;EAC3C,cAAc,KAAK,wBAAwB;EAG3C,KAAK,MAAM,CAAC,WAAW,aAAa,OAAO,QAAQ,MAAM,GAAG;GAC1D,MAAM,OAAO,yBAAyB,SAAS,IAAI;GACnD,MAAM,WAAW,CAAC,SAAS,WAAW,MAAM;GAC5C,cAAc,KAAK,KAAK,YAAY,SAAS,IAAI,KAAK,EAAE;EAC1D;EAEA,MAAM,eAAe,oBAAoB,WAAW,UAAU;EAChE,cAAc,KAAK,IAAI,EAAE;;EAEvB,WAAW,KAAK,YAAY;CAC9B;CAGA,MAAM,qBAAqB;;;;;;;EAO3B,WAAW,KAAK,MAAM,EAAE;;CAGxB,GAAG,cAAc,KAAK,KAAK,QAAQ,mBAAmB,GAAG,kBAAkB;AAC7E;;;;AAKA,eAAe,kCACb,UACA,QACe;CAEf,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiE5B,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;EAfC,CAHzB,GAAG,IAAI,IAAI,OAAO,OAAO,SAAS,OAAO,CAAC,CAAC,KAAK,QAAQ,IAAI,UAAU,CAAC,CAG9C,CAAA,CACxB,KAAK,eAAe;EACnB,MAAM,WAAW,OAAO,QAAQ,SAAS,OAAO,CAAC,CAAC,MAC/C,GAAG,SAAS,IAAI,eAAe,UAClC,CAAC,GAAG,EAAE,CAAC;EAEP,OAAO,OAAO,WAAW,mBADH,WAAW,GAAG,SAAS,QAAQ,MACK;CAC5D,CAAC,CAAC,CACD,KAAK,IAkCR,EAAmB;;;;;;CAQnB,MAAM,oBAAoB;;;;;;;;;;;;;;CAgB1B,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;CA+BvB,MAAM,mBAAmB;;;;EAPH,OAAO,OAAO,SAAS,OAAO,CAAC,CAClD,KACE,QACC,iBAAiB,IAAI,UAAU,kCAAkC,IAAI,UAAU,MACnF,CAAC,CACA,KAAK,IAMR,EAAc;;;;;;;;;;;;;CAyBd,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAPM,2BAA2B,QAAQ,CAAC,CAC9D,KACE,EAAE,YAAY,UACb,OAAO,WAAW,yDAAyD,IAAI,UAAU,OAC7F,CAAC,CACA,KAAK,IAsDR,EAAqB;;;;;;;;;;;;;;;;;CAmBrB,GAAG,cACD,KAAK,KAAK,QAAQ,oBAAoB,GACtC,mBACF;CACA,GAAG,cAAc,KAAK,KAAK,QAAQ,kBAAkB,GAAG,iBAAiB;CACzE,GAAG,cAAc,KAAK,KAAK,QAAQ,kBAAkB,GAAG,iBAAiB;CACzE,GAAG,cAAc,KAAK,KAAK,QAAQ,eAAe,GAAG,cAAc;CACnE,GAAG,cAAc,KAAK,KAAK,QAAQ,iBAAiB,GAAG,gBAAgB;CACvE,GAAG,cAAc,KAAK,KAAK,QAAQ,eAAe,GAAG,cAAc;AACrE;;;;AAKA,SAAS,yBAAyB,UAA0B;CAC1D,QAAQ,UAAR;EACE,KAAK,QACH,OAAO;EACT,KAAK;EACL,KAAK,WACH,OAAO;EACT,KAAK,WACH,OAAO;EACT,KAAK,YACH,OAAO;EACT,KAAK,QACH,OAAO;EACT,KAAK,cACH,OAAO;EACT,SACE,OAAO;CACX;AACF;;;;AAKA,eAAsB,4BACpB,MACe;CACf,MAAM,eAAe,KAAK;CAC1B,MAAM,SAAS,KAAK,MAAM;CAE1B,IAAI,CAAC,cAAc;EACjB,QAAQ,MAAM,2DAA2D;EACzE,QAAQ,KAAK,CAAC;CAChB;CAEA,IAAI,CAAC,GAAG,WAAW,YAAY,GAAG;EAChC,QAAQ,MAAM,4BAA4B,cAAc;EACxD,QAAQ,KAAK,CAAC;CAChB;CAEA,MAAM,qBAAqB;EACzB,UAAU;EACV;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/prebuild/index.ts"],"sourcesContent":["/**\n * Pre-build utilities for generating TypeScript declarations\n * Solves virtual module resolution by creating physical .d.ts files\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport type { SmartObjectManifest } from '../scanner/types';\nimport { selectApiClientEntries } from '../vite-plugin/api-client-entries.js';\nimport { selectWebCollectionEntries } from '../vite-plugin/web-collections.js';\n\nexport interface PrebuildOptions {\n /** Path to manifest file or manifest object */\n manifest: string | SmartObjectManifest;\n /** Output directory for generated types */\n outDir: string;\n /** Include virtual module declarations */\n includeVirtualModules?: boolean;\n /** Include object type definitions */\n includeObjectTypes?: boolean;\n /** Project root path for resolving relative paths */\n projectRoot?: string;\n}\n\n/**\n * Generate TypeScript declaration files from SMRT manifest\n */\nexport async function generateDeclarations(\n options: PrebuildOptions,\n): Promise<void> {\n const {\n manifest: manifestInput,\n outDir,\n includeVirtualModules = true,\n includeObjectTypes = true,\n projectRoot = process.cwd(),\n } = options;\n\n // Load manifest\n const manifest: SmartObjectManifest =\n typeof manifestInput === 'string'\n ? JSON.parse(fs.readFileSync(manifestInput, 'utf-8'))\n : manifestInput;\n\n // Ensure output directory exists\n const fullOutDir = path.isAbsolute(outDir)\n ? outDir\n : path.join(projectRoot, outDir);\n fs.mkdirSync(fullOutDir, { recursive: true });\n\n console.log(`[smrt:prebuild] Generating declarations to ${fullOutDir}`);\n\n if (includeObjectTypes) {\n await generateObjectTypeDeclarations(manifest, fullOutDir);\n }\n\n if (includeVirtualModules) {\n await generateVirtualModuleDeclarations(manifest, fullOutDir);\n }\n\n console.log(\n `[smrt:prebuild] Generated declarations for ${Object.keys(manifest.objects).length} SMRT objects`,\n );\n}\n\n/**\n * Generate TypeScript interfaces for SMRT objects\n */\nasync function generateObjectTypeDeclarations(\n manifest: SmartObjectManifest,\n outDir: string,\n): Promise<void> {\n const interfaces: string[] = [];\n\n // Generate interfaces for each discovered SMRT object\n for (const [_objectName, objectMeta] of Object.entries(manifest.objects)) {\n const fields = objectMeta.fields || {};\n const propertyLines: string[] = [];\n\n // Add standard SmrtObject properties\n propertyLines.push(' id?: string;');\n propertyLines.push(' created_at?: string;');\n propertyLines.push(' updated_at?: string;');\n\n // Add object-specific properties\n for (const [fieldName, fieldDef] of Object.entries(fields)) {\n const type = mapFieldTypeToTypeScript(fieldDef.type);\n const optional = !fieldDef.required ? '?' : '';\n propertyLines.push(` ${fieldName}${optional}: ${type};`);\n }\n\n const interfaceDef = `export interface ${objectMeta.className}Data {\n${propertyLines.join('\\n')}\n}`;\n interfaces.push(interfaceDef);\n }\n\n // Write object types file\n const objectTypesContent = `/**\n * Auto-generated TypeScript interfaces for SMRT objects\n * Generated at build time from @smrt() decorated classes\n *\n * DO NOT EDIT THIS FILE MANUALLY\n */\n\n${interfaces.join('\\n\\n')}\n`;\n\n fs.writeFileSync(path.join(outDir, 'smrt-objects.d.ts'), objectTypesContent);\n}\n\n/**\n * Generate virtual module declarations\n */\nasync function generateVirtualModuleDeclarations(\n manifest: SmartObjectManifest,\n outDir: string,\n): Promise<void> {\n // Generate manifest module declaration\n const manifestDeclaration = `/**\n * Auto-generated manifest module declaration\n */\ndeclare module '@smrt/manifest' {\n export interface SmrtObjectField {\n type: string;\n required?: boolean;\n default?: any;\n }\n\n export interface SmrtObjectMethod {\n name: string;\n parameters: Array<{\n name: string;\n type: string;\n optional?: boolean;\n default?: any;\n }>;\n returnType: string;\n async: boolean;\n isStatic: boolean;\n isPublic: boolean;\n }\n\n export interface SmrtObjectDefinition {\n name: string;\n className: string;\n collection: string;\n filePath: string;\n fields: Record<string, SmrtObjectField>;\n methods: Record<string, SmrtObjectMethod>;\n decoratorConfig: any;\n extends?: string;\n }\n\n export interface SmrtManifest {\n version: string;\n timestamp: number;\n objects: Record<string, SmrtObjectDefinition>;\n }\n\n export const manifest: SmrtManifest;\n export default manifest;\n}`;\n\n // Generate client module declaration\n const apiClientInterface = selectApiClientEntries(manifest)\n .map(({ clientKey, dataInterfaceName }) => {\n return ` ${clientKey}: CrudOperations<${dataInterfaceName}>;`;\n })\n .join('\\n');\n\n // Wire-shape policy (#1797): the server returns BARE JSON — a bare array for\n // list/search, a bare object for get/create/update — with snake_case field\n // names (created_at, updated_at). These declarations match that shape (no\n // envelope, no camelCase) and are byte-identical to the vite-plugin client\n // declaration. Fetchers reject on non-2xx with a SmrtClientError (#1796).\n const clientDeclaration = `/**\n * Auto-generated API client module declaration\n */\ndeclare module '@smrt/client' {\n /** Shape of a JSON error body carried by a rejected request (SmrtClientError.body). */\n export interface ApiError {\n error?: string;\n message?: string;\n }\n\n /** Typed error thrown by every fetcher on a non-2xx response (#1796). */\n export interface SmrtClientError extends Error {\n name: 'SmrtClientError';\n status: number;\n body?: ApiError | string;\n }\n\n export interface CrudOperations<T = any> {\n list(params?: Record<string, any>): Promise<T[]>;\n get(id: string): Promise<T>;\n create(data: Partial<T>): Promise<T>;\n update(id: string, data: Partial<T>): Promise<T>;\n delete(id: string): Promise<boolean>;\n search(query: string): Promise<T[]>;\n }\n\n export interface ApiClient {\n${apiClientInterface}\n }\n\n export function createClient(basePath?: string): ApiClient;\n export default createClient;\n}`;\n\n // Generate routes module declaration\n const routesDeclaration = `/**\n * Auto-generated routes module declaration\n */\ndeclare module '@smrt/routes' {\n export interface RouteApp {\n get(path: string, handler: (req: any, res: any) => void): void;\n post(path: string, handler: (req: any, res: any) => void): void;\n put(path: string, handler: (req: any, res: any) => void): void;\n delete(path: string, handler: (req: any, res: any) => void): void;\n }\n\n export function setupRoutes(app: RouteApp): void;\n export default setupRoutes;\n}`;\n\n // Generate MCP module declaration\n const mcpDeclaration = `/**\n * Auto-generated MCP module declaration\n */\ndeclare module '@smrt/mcp' {\n export interface McpTool {\n name: string;\n description: string;\n inputSchema: {\n type: string;\n properties: Record<string, any>;\n required?: string[];\n };\n }\n\n export const tools: McpTool[];\n export function createMCPServer(): {\n name: string;\n version: string;\n tools: McpTool[]\n };\n export default createMCPServer;\n}`;\n\n // Generate types module declaration with object imports\n const objectImports = Object.values(manifest.objects)\n .map(\n (obj) =>\n ` export type ${obj.className}Data = import('./smrt-objects').${obj.className}Data;`,\n )\n .join('\\n');\n\n const typesDeclaration = `/**\n * Auto-generated types module declaration\n */\ndeclare module '@smrt/types' {\n${objectImports}\n\n export interface Request {\n params: Record<string, string>;\n query: Record<string, any>;\n json(): Promise<any>;\n }\n\n export interface Response {\n json(data: any, init?: { status?: number }): Response;\n status(code: number): Response;\n }\n}`;\n\n // Generate web collection-definition module declaration (#1761). Selection\n // shares selectWebCollectionEntries with the vite-plugin runtime module and\n // its virt-web d.ts, so this `tsc`-only consumer declaration cannot drift\n // from the emitted values.\n const webCollectionEntries = selectWebCollectionEntries(manifest)\n .map(\n ({ collection, obj }) =>\n ` ${collection}: SmrtWebCollectionDefinition<import('./smrt-objects').${obj.className}Data>;`,\n )\n .join('\\n');\n\n const webDeclaration = `/**\n * Auto-generated web collection-definition module declaration (#1761)\n */\ndeclare module '@smrt/web' {\n export interface SmrtWebFieldDefinition {\n type: string;\n required?: boolean;\n default?: unknown;\n }\n\n export type SmrtWebRelationshipKind =\n | 'foreignKey'\n | 'crossPackageRef'\n | 'oneToMany'\n | 'manyToMany';\n\n /**\n * A manifest-derived edge to a sibling REST collection. Mutating this\n * collection invalidates the caches of the collections named by these edges\n * (#1761 relationship-derived invalidation).\n */\n export interface SmrtWebRelationship {\n field: string;\n kind: SmrtWebRelationshipKind;\n relatedCollection: string;\n }\n\n /** A WebMCP/MCP tool descriptor for one collection action (#1812). */\n export interface WebToolDescriptor {\n action: string;\n name: string;\n description: string;\n inputSchema: Record<string, unknown>;\n readOnly: boolean;\n }\n\n export interface SmrtWebCollectionDefinition<TData = Record<string, unknown>> {\n name: string;\n className: string;\n endpoint: string;\n idField: string;\n actions: string[];\n fields: Record<string, SmrtWebFieldDefinition>;\n /** Manifest-derived relationship edges to sibling REST collections. */\n relationships: SmrtWebRelationship[];\n /** WebMCP/MCP tool descriptors for the exposed actions (#1812). */\n toolDescriptors: WebToolDescriptor[];\n /** Phantom row-type carrier for inference — never present at runtime. */\n _row?: TData;\n }\n\n export interface SmrtWebCollectionDefinitions {\n${webCollectionEntries}\n }\n\n export const collectionDefinitions: SmrtWebCollectionDefinitions;\n export function getCollectionDefinition<\n K extends keyof SmrtWebCollectionDefinitions,\n >(name: K): SmrtWebCollectionDefinitions[K];\n /**\n * Build-time web-collection shape digest (#1764). A deterministic,\n * replica-stable hash of the emitted collection definitions; a change means\n * old persisted client rows may mis-hydrate. Consumers fold it into the\n * durable persistence namespace and the version-awareness updateAvailable\n * contract signal.\n */\n export const manifestHash: string;\n export default collectionDefinitions;\n}`;\n\n // Write all virtual module declarations\n fs.writeFileSync(\n path.join(outDir, 'smrt-manifest.d.ts'),\n manifestDeclaration,\n );\n fs.writeFileSync(path.join(outDir, 'smrt-client.d.ts'), clientDeclaration);\n fs.writeFileSync(path.join(outDir, 'smrt-routes.d.ts'), routesDeclaration);\n fs.writeFileSync(path.join(outDir, 'smrt-mcp.d.ts'), mcpDeclaration);\n fs.writeFileSync(path.join(outDir, 'smrt-types.d.ts'), typesDeclaration);\n fs.writeFileSync(path.join(outDir, 'smrt-web.d.ts'), webDeclaration);\n}\n\n/**\n * Map SMRT field types to TypeScript types\n */\nfunction mapFieldTypeToTypeScript(smrtType: string): string {\n switch (smrtType) {\n case 'text':\n return 'string';\n case 'decimal':\n case 'integer':\n return 'number';\n case 'boolean':\n return 'boolean';\n case 'datetime':\n return 'string | Date';\n case 'json':\n return 'any';\n case 'foreignKey':\n return 'string';\n default:\n return 'any';\n }\n}\n\n/**\n * CLI command for generating declarations\n */\nexport async function generateDeclarationsFromCLI(\n args: string[],\n): Promise<void> {\n const manifestPath = args[0];\n const outDir = args[1] || 'src/types/generated';\n\n if (!manifestPath) {\n console.error('Usage: generate-declarations <manifest-path> [output-dir]');\n process.exit(1);\n }\n\n if (!fs.existsSync(manifestPath)) {\n console.error(`Manifest file not found: ${manifestPath}`);\n process.exit(1);\n }\n\n await generateDeclarations({\n manifest: manifestPath,\n outDir,\n });\n}\n"],"mappings":"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"}
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-07-15T08:38:55.574Z",
3
+ "generatedAt": "2026-07-15T16:26:15.777Z",
4
4
  "packageName": "@happyvertical/smrt-core",
5
- "packageVersion": "0.40.3",
5
+ "packageVersion": "0.40.4",
6
6
  "sourceManifestPath": "dist/manifest.json",
7
7
  "agentDocPath": "AGENTS.md",
8
8
  "sourceHashes": {
9
- "manifest": "3919dca6cf49e2242be6b42d8cbce4cacea6be132131415729bb4a42772cf05f",
10
- "packageJson": "f76a47c808cc3af4e145f295790470c09171f96a9e8cd980f43cfece6602ff80",
11
- "agents": "46a9a399123c7c5700076fa94b650df11e28123af899806243bc2fcb6e7295e4"
9
+ "manifest": "cb64635541c7423f6e38e342cba4481bf0940b496be4bdeea7170e35149f4413",
10
+ "packageJson": "73db43e49822916f3c853cb7d871cfac34a577e1cf5eac109fdef83a00b8a619",
11
+ "agents": "e8267bf639a5eec0e6465970b6bb163cfdfbea1d1c7f18f81e640aa9de61bd35"
12
12
  },
13
13
  "exports": [
14
14
  ".",
@@ -324,5 +324,5 @@
324
324
  "polymorphicAssociations": 1,
325
325
  "uuidColumns": 3
326
326
  },
327
- "agentDoc": "# @happyvertical/smrt-core\n\nORM, code generation, AI integration, and the DispatchBus. Everything else builds on this.\n\n## Key Classes\n\n| Class | File | Purpose |\n|-------|------|---------|\n| SmrtObject | `src/object.ts` | Base persistent object — save, delete, is(), do(), loadFromId/Slug |\n| SmrtCollection | `src/collection.ts` | CRUD collection — list, get, create, delete, getOrUpsert |\n| ObjectRegistry | `src/registry.ts` | Global singleton (globalThis) — class metadata, fields, STI chains, manifests |\n| DispatchBus | `src/dispatch/bus.ts` | Inter-agent messaging — emit, subscribe (persistent), process |\n| GlobalInterceptors | `src/interceptors.ts` | Plugin system — beforeList/Get/Save/Delete hooks (used by tenancy) |\n| LearningMemory | `src/learning/memory.ts` | Confidence-scored recall/capture over `_smrt_contexts` + embeddings (#1886) |\n\n## SmrtObject Lifecycle\n\n`constructor(options)` → `initialize()` → ready for `save()`/`delete()`/`loadFromId()`\n\n- `initialize()`: loads field initializers, applies option values (options override initializers), loads from DB if id/slug provided\n- `save()`: upsert with STI validation, interceptor execution, auto-embeddings. Persisted objects (`isPersisted` — set by DB hydration and successful saves) upsert on `['id']` so natural-key edits (e.g. slug renames) update in place; new objects upsert on the natural-key conflict columns for ingestion-style dedup (#1472)\n- `is(criteria)` / `do(instructions)` / `describe()`: AI operations via function calling. They inject the object's own `toPublicJSON()` (sensitive fields stripped) as a \"content body\" so the model reasons over the instance. Options: `includeData: false` skips injection (for callers that already curate the relevant fields into the instruction); `maxDataLength` overrides the truncation budget. Neither key is forwarded to `ai.message()`. (#1567)\n- `getSlug()`: auto-generates from name → title → label → id\n- `loadRelated(fieldName)`: lazy-loads relationships (cached in `_loadedRelationships` Map)\n\n## LearningMemory (#1886)\n\nConfidence-scored, self-correcting memory over the existing `_smrt_contexts` (keyed recall) and `_smrt_embeddings` (semantic recall) substrate. Wires the reinforcement columns that ship on `_smrt_contexts` but were never written (`success_count`, `failure_count`, and a `last_used_at` that recall now refreshes). This is L1 of the tenant-learning-agents epic; the opt-in `Learning` trait in `@happyvertical/smrt-agents` composes it into the agent lifecycle.\n\n```typescript\nconst memory = new LearningMemory({ db: obj.systemDb, ownerClass: 'InvoiceAgent', ownerId: obj.id, tenantId });\n\n// recall — union of keyed-context lookup + (optional) semantic search, confidence-filtered\nconst [hit] = await memory.recall('parser/acme', { key: docUrl }); // or { query } with a wired semanticSearch\nconst strategy = hit?.value ?? (await generate());\n\n// capture — reinforce the outcome\nawait memory.capture({ scope: 'parser/acme', key: docUrl, value: strategy }, { success: ok });\n```\n\n- **`capture(episode, outcome)`**: success strengthens `confidence` toward 1.0 + increments `success_count`; failure decays toward `failureConfidence` (default 0.3) + increments `failure_count`. Defaults (`minConfidence` 0.7, `successConfidence` 0.9, `reinforcement` 0.5) mean a single failure drops a confident memory below the reuse floor. Seeds a new row when none exists and the episode carries a `value` (a failed first attempt is retained at low confidence for self-correction).\n- **`recall(scope, opts)`**: owner-scoped keyed lookup (thus tenant-isolated) filtered by the confidence floor, expiry, and optional time-decay, with hierarchical scope fallback; unions an injected `semanticSearch` arm when a `query` is given (tenant-scoped via its `where`). Refreshes `last_used_at` on returned rows.\n- Injected `semanticSearch` matches `SmrtCollection.semanticSearch`, so `LearningMemory` never reaches into a collection's internals.\n\n## SmrtCollection Query\n\n```typescript\nawait collection.list({\n where: { status: 'active', price: { op: '>', value: 10 } },\n limit: 50, offset: 0, orderBy: 'created_at DESC'\n});\n```\n\nProjection primitive (#1902): pass `select: ['id', 'title', 'tenantId']` to\n`list()` when an admin/list workflow needs compact rows. `select` uses SMRT\nfield names, maps them to DB columns internally, and returns plain objects keyed\nby the same SMRT field names without hydrating `SmrtObject` instances. It\ncomposes with `where`, `orderBy`, `limit`, and `offset`; `beforeList`\ninterceptors still run. It is for column-backed fields only and cannot combine\nwith `include`/relationship eager loading.\n\n**WHERE operators**: `=`, `>`, `<`, `>=`, `<=`, `!=`, `in`, `not in`, `like`, `is null`, `is not null`. Arrays auto-detect `IN`. Dot notation for JSON paths: `metadata.userId`.\n\nSTI child collections auto-filter by `_meta_type`.\n\n## Object Memory & Semantic Search\n\nTwo persistence primitives every `SmrtObject`/`SmrtCollection` inherits — load-bearing for learning agents, usable by any object. Full guide: `docs/content/core.md` → \"Context Memory System\".\n\n- **Context memory** (`remember`/`recall`/`recallAll`/`forget`/`forgetScope`, table `_smrt_contexts`): stores any JSON value keyed by `(owner_class, owner_id, scope, key, version)` with a `confidence` score (0–1) and a stored `expiresAt` (metadata — `recall()` does **not** filter expired rows; expiry is caller-managed). `recall()` returns the highest-confidence match with an optional `minConfidence` floor and **opt-in** hierarchical scope fallback (`includeAncestors: true` → `'a/b/c' → 'a/b' → 'a' → 'global'`; default off); `recallAll()` returns a `Map`. Typical use: cache a learned strategy (e.g. a working selector per host) and reuse it across sessions. `success_count`/`failure_count` columns exist for outcome-weighting but are not auto-updated by the framework.\n- **Semantic search** (on `SmrtCollection`, table `_smrt_embeddings`): `semanticSearch(query)`, `findSimilar(object)`, `findSimilarToEmbedding(vector)` — cosine ranking over embeddings of the fields declared in `@smrt({ embeddings })`. Native pgvector/HNSW when configured, in-memory `CosineSimilarity` fallback otherwise; default local model `Xenova/bge-base-en-v1.5` (768-dim) or AI `text-embedding-3-small`. Hits hydrate via `list({ 'id in': … })`, so `@TenantScoped` isolation applies to results.\n\n## @smrt() Decorator Options\n\nKey options: `tableName`, `tableStrategy` ('cti'|'sti'), `conflictColumns`, `api`/`mcp`/`cli` (generation config), `ai` (callable methods), `hooks` (beforeSave/afterSave/beforeDelete/afterDelete), `embeddings` (auto-generate), `tenantScoped`, `agent`.\n\nRegistration sets `SMRT_TABLE_NAME` static property (survives minification).\n\n## Domain Knowledge Artifacts\n\n`smrtPlugin()` writes runtime manifests and agent/developer knowledge artifacts:\n\n- local dev/build: `.smrt/manifest.json` and `.smrt/smrt-knowledge.json`\n- package build: `dist/manifest.json` and `dist/smrt-knowledge.json`\n\nKeep `manifest.json` runtime-focused. `smrt-knowledge.json` is the deterministic\nagent contract for downstream review and architecture tools.\n\nConfig precedence for knowledge is defaults → top-level `knowledge` in\n`smrt.config.ts` → `packages[packageName].knowledge` → plugin option →\nobject-level `@smrt({ knowledge })`.\n\nObject-level `knowledge: false` excludes an object from authored context only;\nit must not change runtime manifest registration. Use\n`knowledge: { tags, summary, risks }` for review-sensitive domain objects.\n\nHTTP knowledge routes are disabled by default. If `knowledge.api.enabled` is\ntrue, generated SvelteKit routes must stay GET-only and guarded by dev mode or\nadmin auth.\n\n## DispatchBus\n\n- `emit(signalType, payload, metadata)` → creates persistent Dispatch record\n- `on(pattern, handler)` → in-memory handler (immediate)\n- `subscribe({ signalType, subscriber })` → persistent subscription (survives restarts)\n- `process(subscriberName, handler)` → process pending dispatches\n- Wildcards: `campaign.*` matches `campaign.completed` (single segment only)\n- Tables: `_smrt_dispatch`, `_smrt_dispatch_subscriptions`\n- Status: `pending → processing → completed` (or `failed`)\n\n## Change Feed (#1758)\n\nAdapter-agnostic change-observation spine (`src/change-feed.ts`) — the server half of the client/mobile sync contract (PRD #1755):\n\n- `_smrt_changes` system table: one append per framework save/delete via a GlobalInterceptors writer registered at framework init. Deletes are tombstones (`operation: 'delete'`). `_smrt_*` tables are skipped. Feed-append failures log and never fail the user's write. No dirty-check: a field-unchanged `.save()` appends a spurious `update` entry (diff-aware paths like `getOrUpsert`/sync-apply short-circuit before `save()` and append nothing); subscribers must tolerate spurious entries — they are convergent.\n- Sequences: allocated as `MAX(seq)+1` inside the INSERT with conflict retry — committed rows stay contiguous, so commit order == seq order on SQLite/Postgres/DuckDB (deliberately NOT identity/serial: those allocate before commit and break the cursor guarantee under concurrent writers).\n- `getChangesSince(db, { since, tables?, tenantId?, limit? }) → { changes, cursor, resyncRequired?, resyncCursor? }`: strictly monotonic cursor; polling with returned cursors misses no committed change and never repeats one. A cursor that cannot be served incrementally — pruned below the retained `[floor..horizon]` run, or foreign/ahead of the horizon — gets `resyncRequired: true` with empty `changes`, an unadvanced `cursor`, and `resyncCursor` set to the current horizon so clients can full-refetch then resume incrementally; detection runs on the UNFILTERED log so `tables`/`tenantId` filters never trigger or mask it. `getTenantScopedChangesSince()` resolves tenant via the DispatchBus resolver hook (fail-closed: tenancy on + no context → global rows only; tenant `T` sees `T` + global rows, never another tenant).\n- `getTableVersion(db, table) → number`: the per-table change version (`MAX(seq)` for the table, replica-stable — no per-process divergence), the ETag source for zero-query conditional GETs (#1765). Advances on any framework write to the table (CRUD and sync-apply, which all `save()`/`delete()`). A table with no retained entry of its own falls back to the global horizon (never a resettable low value) so an all-pruned table cannot false-304 a stale client; only 0 when the feed is empty.\n- Generated `_changes` routes: REST (`GET {basePath}/_changes`, requires `authMiddleware`, otherwise 401 — per-model `api.public` does NOT apply) and SvelteKit (`{routesDir}/_changes/+server.ts`, requires an authenticated principal on `locals`; opt out via `sveltekit.changesRoute.enabled: false`). Query params: `since`, `tables` (comma-separated), `limit`. Responses stay HTTP 200 in the resync state — `resyncRequired` is protocol state, not an error, and `resyncCursor` is the resume cursor after the client completes a full refetch.\n- Retention: `pruneChangeFeed(db, { maxAgeMs?, maxRows? })` — schedule it. Pruning deletes oldest-first and always retains the newest entry (a non-empty feed is never emptied), which is what makes pruned-cursor detection provable and keeps caught-up consumers polling normally. Raw-SQL writes are invisible to the feed (same documented gap as the #1499 cache); `bumpChangeFeed(db, { table, rowId? })` is the manual escape hatch.\n\n## Live Events / Change Signals (#1763, server half)\n\nThe push companion to the change feed (`src/change-signals.ts` + the generated `_events` SSE route) — the server half of live cache invalidation (PRD #1755). The client subscriber (two-client/reconnect/polling-fallback ACs) is a separate later slice.\n\n- **Change-signal bus** (`src/change-signals.ts`): every framework save/delete that appends a durable feed row also publishes a coarse `ChangeSignal` `{ table, operation, rowId, tenantId, seq }` — **never a row payload** (authorization stays on the read path). Structurally mirrors the collection cache's notify/listen path. `subscribeToChangeSignals(db, listener) → unsubscribe`; `publishChangeSignal`/`broadcastChangeSignal`/the listener loop stay internal. `appendChange` now returns the allocated `seq` (was `void`); the signal carries it as a coarse resume cursor (`bumpChangeFeed` ignores the return). The publish runs only after the append SUCCEEDS (no signal without a durable feed row) and in its own log-and-swallow try/catch, so a signal problem never fails the user's write. `_smrt_*` writes never signal (the writer skips them). Delivery is synchronous per-listener with per-listener try/catch (one throwing SSE controller never blocks others); no per-subscriber queue — backpressure rides the platform `ReadableStream`.\n- **In-process + cross-replica**: locally-published and peer-received signals go through the SAME `deliverLocally` path. Cross-replica fan-out rides the db adapter's optional notification capability (`db.notifications`, a NEW `smrt_change_signals` channel distinct from the cache channel) with echo-avoidance by `PROCESS_ID`. No capability → in-process only, warn-once, **never an error, never blocks the write** (subscribers on other replicas fall back to cursor polling).\n- **Generated `_events` SSE route**: REST (`GET {basePath}/_events`, requires `authMiddleware`, otherwise 401 — fail-closed, per-model `api.public` does NOT apply; 405 non-GET; 503 no db or subscriber capacity reached) and SvelteKit (`{routesDir}/_events/+server.ts`, requires an authenticated principal on `locals`; opt out via `sveltekit.eventsRoute.enabled: false`; cap via `sveltekit.eventsRoute.maxSubscribers`, where `0` means unlimited). Tenant scope is captured ONCE at connection open (`resolveDispatchTenantScope`) and filtered server-side per signal via `signalVisibleToTenant` (same rule as `getChangesSince`'s tenant filter) before any byte hits the wire — delivery runs outside any tenant ALS context, so it must use the captured value. The stream lifecycle lives in `buildChangeEventStream(db, { cursor, tenantScope, heartbeatMs?, manifestHash? })` (exported; the SvelteKit route imports it so it stays thin): subscribe-before-catch-up (closes the gap window; overlap is deduped by the SSE `id:`/seq client-side), `retry: 3000`, optional connection-open `event: manifest` carrying `{ manifestHash }` for live contract detection, cursor catch-up via `getChangesSince` filtered by the CAPTURED scope (not re-resolved from ALS, so it matches the live-signal filter exactly and can't replay another tenant's rows) (`Last-Event-ID` header beats `?since=`; default = live-forward only; `resyncRequired` → `event: resync`), heartbeat (`DEFAULT_EVENTS_HEARTBEAT_MS` = 15s), and `cancel()` teardown (clears heartbeat + unsubscribes). SSE change frame: `id: <seq>\\nevent: change\\ndata: {table,operation,rowId,tenantId}\\n\\n` — seq is ONLY in the `id:` line, never the data JSON. Subscriber cap default is `DEFAULT_EVENTS_MAX_SUBSCRIBERS` = 1000; over-cap connections return retryable 503 + `Retry-After`, and existing subscribers are unaffected. **Cross-origin is opt-in and fail-closed (#1861)**: same-origin only by default. REST wraps `_events` in its CORS layer — set `enableCors` + an explicit `allowedOrigins` allowlist + `allowCredentials: true` and an allow-listed browser can subscribe with a credentialed `EventSource` (`withCredentials: true`); the response echoes the specific `Origin` (never `*`) plus `Access-Control-Allow-Credentials: true`. SvelteKit mirrors this via `sveltekit.eventsRoute.allowedOrigins` + `allowCredentials` (the generated route bakes the allowlist into a `Set`, echoes only a member origin, and answers a credentialed `OPTIONS` preflight). CORS never authorizes — the fail-closed auth guard and captured tenant scope are unchanged, so the read posture holds identically across origins; it only lets an allow-listed browser's cookies reach the guard. Client disconnect through the Node `createServer` bridge now cancels the response reader (was a teardown leak) so `cancel()` fires and the subscription is released.\n- **Known gaps** (documented in the module): raw-SQL writes don't signal (same gap as the feed); live signals for caller-managed-transaction writes are best-effort (the append + signal fire pre-commit, so a rolled-back write may emit a signal and its freed seq is later reused) — the autocommit default path is exact, and clients reconcile via full catch-up/resync (inherits the change feed's transaction caveat).\n\n## Single Table Inheritance (STI)\n\n- Base: `@smrt({ tableStrategy: 'sti' })` — children inherit, share one table\n- Discriminator: `_meta_type` column with qualified names (`@happyvertical/smrt-content:Article`)\n- Child fields: `@meta()` decorator → stored in `_meta_data` JSONB (not as columns)\n- Polymorphic queries: collection loads `_meta_type`, creates correct subclass dynamically\n- Validation: fail-fast on save if `_meta_type` missing or mismatched\n\n## Code Generators\n\n| Generator | Location | Output |\n|-----------|----------|--------|\n| REST API | `src/generators/rest.ts` | OpenAPI-compliant CRUD endpoints |\n| CLI | `src/generators/cli.ts` | `objectname:action` admin commands — writable allowlist, exhaustive-include, `--from-file`, fail-closed tenant context |\n| MCP Server | `src/generators/mcp.ts` | Model Context Protocol tools |\n| Web collections | `src/vite-plugin/web-collections.ts` (selectors) + `generateWebModule` | `@happyvertical/smrt-virt-web` — one typed collection definition per API-exposed REST collection (#1761), consumed by `@happyvertical/smrt-web` |\n\nThe web module also emits a build-time **`manifestHash`** constant (#1764): `computeWebManifestHash(manifest)` is a deterministic, replica-stable digest of the emitted web-collection SHAPE (name/className/endpoint/idField/actions/fields/relationships), canonicalized (recursive key sort) before `sha256 → base64url`, truncated to 16 chars — so the same schema always hashes the same, and a field add/remove/type-change/edge-change changes it. A change means old persisted client rows may mis-hydrate, so smrt-web keys its durable persistence namespace on it and its `updateAvailable` contract signal compares against it. Three emission sites must not drift: the runtime value (`generateWebModule`), the `@happyvertical/smrt-virt-web` ambient d.ts (`vite-plugin/index.ts`), and the physical `@smrt/web` d.ts (`prebuild/index.ts`).\n\nGenerated reads (`list`/`get`) on the REST and SvelteKit generators support conditional GET (helpers in `src/generators/conditional-get.ts`). ETag v2 (#1765): the validator is the table's change-feed version (`getTableVersion`) keyed by the request representation, so a **concrete** `If-None-Match` short-circuits into a 304 with an empty body **before** the collection query runs — an unchanged table revalidates with zero table scan. A wildcard `If-None-Match: *` is deferred until the payload builds (existence confirmed), so a missing item still returns 404, not a false 304. Tenant-scoped reads fold the active tenant into the representation (`resolveTenantEtagDiscriminator`) so one tenant's cached validator never satisfies another's read of the same URL. Routes whose GET renders via a **custom serializer** (which can load related tables the base-table version can't observe) keep the v1 body-hash ETag (`#1757`, query-first but correct); the default `toPublicJSON` path — all REST reads and non-serializer SvelteKit reads — uses v2. v2 is weakly consistent by design (the cost of not reading the data): a revalidation in the sub-statement window between a committed write and its feed append can return a stale 304 that self-heals on the next revalidation. The other v2 window — a deploy that changes the response shape WITHOUT a table write — is closed by the **#1764 ETag salt**: `computeTableVersionEtag(version, representation, manifestHash?)` folds the build's web-collection shape digest into the digest, so a shape-only redeploy busts every read validator (`undefined` reproduces the pre-#1764 unsalted value byte-for-byte for direct helper callers). The generated SvelteKit route bakes the digest in as a `MANIFEST_HASH` constant (via `generateConditionalGetRouteHelper`'s `manifestHash` option, sourced from `computeWebManifestHash(manifest)`) — automatic for the SvelteKit transport. The runtime `APIGenerator` auto-populates the same salt from the runtime registry with `computeRuntimeWebManifestHash()` when `APIConfig.manifestHash` is omitted; explicit `APIConfig.manifestHash` still wins for custom setups. The digest scope is get-OR-list (`selectWebEtagSaltEntries`), so **get-only** routes are salted too. Strong consistency still requires the v1 body-hash path. Cache-Control policy (unchanged from #1757): `private, no-cache` by default; public models may opt into shared caching via `@smrt({ api: { public: true | 'read', cache: { sMaxage } } })` → `public, max-age=0, s-maxage=<n>`; non-public models never emit shared-cache headers. Tenant-scoped models (any mode) never emit them either — bodies vary with session-cookie tenant context that URL-keyed shared caches cannot see; `sMaxage` is neutralized to `private, no-cache` with a one-time warning.\n\n## Child Accessors (R10)\n\n`src/child-accessors.ts` installs a consistent `get<FieldName>()` instance method for every `@oneToMany` field at `@smrt()` registration time (e.g. `@oneToMany('OrderItem') items` → `order.getItems()`), delegating to `loadRelatedMany`. Two invariants:\n\n- **Additive** — never overwrites a hand-rolled method of the same name (checks the whole prototype chain). `Profile.getMetadata()` (key-value) and `ProfileRelationship.getTerms()` are preserved.\n- **Runtime-only** — attached to the prototype, invisible to the build-time manifest, so it never leaks into the REST/CLI/MCP surface.\n\nWhen the target declares multiple FKs back to the parent, annotate `@oneToMany(Target, { foreignKey: '<inverseField>' })`; `loadRelatedMany` and the eager `include:` loader both honor it (else first-match).\n\n## Vite Plugin\n\n```typescript\n// vite.config.ts — required for @smrt() decorators (Vite 8+, oxc transform)\nexport default defineConfig({\n oxc: {\n decorator: {\n legacy: true,\n emitDecoratorMetadata: true,\n },\n },\n});\n```\n\nUnder Vite 8 the oxc transform does not honor the pre-Vite-8 `esbuild.tsconfigRaw`\nrecipe (or tsconfig `experimentalDecorators` reached through SvelteKit's\n`extends \"./.svelte-kit/tsconfig.json\"` chain), so that recipe throws\n`SyntaxError: Invalid or unexpected token` on the first SSR request. Configure\ndecorators through `oxc.decorator` instead. Consumers still pinned on vite<8 need\nthe legacy `esbuild.tsconfigRaw` form with `experimentalDecorators: true,\nemitDecoratorMetadata: true`.\n\n## Gotchas\n\n- **Filesystem support is a lazy boundary (#1979)**: `SmrtClass` acquires `options.fs` adapters via `createFilesystemAdapter()` (`src/filesystem-loader.ts`), never a static `@happyvertical/files` import — the files SDK statically pulls @aws-sdk/client-s3 and reaches googleapis, and a static edge here would land it in every downstream SSR bundle. Node/tsx/vite-dev runtimes resolve it on first use; fully-bundled deployments import `@happyvertical/smrt-core/filesystem` at startup. Use `importOptionalDependency()` (`src/lazy-external.ts`) for any similar optional heavyweight dependency.\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
+ "agentDoc": "# @happyvertical/smrt-core\n\nORM, code generation, AI integration, and the DispatchBus. Everything else builds on this.\n\n## Key Classes\n\n| Class | File | Purpose |\n|-------|------|---------|\n| SmrtObject | `src/object.ts` | Base persistent object — save, delete, is(), do(), loadFromId/Slug |\n| SmrtCollection | `src/collection.ts` | CRUD collection — list, get, create, delete, getOrUpsert |\n| ObjectRegistry | `src/registry.ts` | Global singleton (globalThis) — class metadata, fields, STI chains, manifests |\n| DispatchBus | `src/dispatch/bus.ts` | Inter-agent messaging — emit, subscribe (persistent), process |\n| GlobalInterceptors | `src/interceptors.ts` | Plugin system — beforeList/Get/Save/Delete hooks (used by tenancy) |\n| LearningMemory | `src/learning/memory.ts` | Confidence-scored recall/capture over `_smrt_contexts` + embeddings (#1886) |\n\n## SmrtObject Lifecycle\n\n`constructor(options)` → `initialize()` → ready for `save()`/`delete()`/`loadFromId()`\n\n- `initialize()`: loads field initializers, applies option values (options override initializers), loads from DB if id/slug provided\n- `save()`: upsert with STI validation, interceptor execution, auto-embeddings. Persisted objects (`isPersisted` — set by DB hydration and successful saves) upsert on `['id']` so natural-key edits (e.g. slug renames) update in place; new objects upsert on the natural-key conflict columns for ingestion-style dedup (#1472)\n- `is(criteria)` / `do(instructions)` / `describe()`: AI operations via function calling. They inject the object's own `toPublicJSON()` (sensitive fields stripped) as a \"content body\" so the model reasons over the instance. Options: `includeData: false` skips injection (for callers that already curate the relevant fields into the instruction); `maxDataLength` overrides the truncation budget. Neither key is forwarded to `ai.message()`. (#1567)\n- `getSlug()`: auto-generates from name → title → label → id\n- `loadRelated(fieldName)`: lazy-loads relationships (cached in `_loadedRelationships` Map)\n\n## LearningMemory (#1886)\n\nConfidence-scored, self-correcting memory over the existing `_smrt_contexts` (keyed recall) and `_smrt_embeddings` (semantic recall) substrate. Wires the reinforcement columns that ship on `_smrt_contexts` but were never written (`success_count`, `failure_count`, and a `last_used_at` that recall now refreshes). This is L1 of the tenant-learning-agents epic; the opt-in `Learning` trait in `@happyvertical/smrt-agents` composes it into the agent lifecycle.\n\n```typescript\nconst memory = new LearningMemory({ db: obj.systemDb, ownerClass: 'InvoiceAgent', ownerId: obj.id, tenantId });\n\n// recall — union of keyed-context lookup + (optional) semantic search, confidence-filtered\nconst [hit] = await memory.recall('parser/acme', { key: docUrl }); // or { query } with a wired semanticSearch\nconst strategy = hit?.value ?? (await generate());\n\n// capture — reinforce the outcome\nawait memory.capture({ scope: 'parser/acme', key: docUrl, value: strategy }, { success: ok });\n```\n\n- **`capture(episode, outcome)`**: success strengthens `confidence` toward 1.0 + increments `success_count`; failure decays toward `failureConfidence` (default 0.3) + increments `failure_count`. Defaults (`minConfidence` 0.7, `successConfidence` 0.9, `reinforcement` 0.5) mean a single failure drops a confident memory below the reuse floor. Seeds a new row when none exists and the episode carries a `value` (a failed first attempt is retained at low confidence for self-correction).\n- **`recall(scope, opts)`**: owner-scoped keyed lookup (thus tenant-isolated) filtered by the confidence floor, expiry, and optional time-decay, with hierarchical scope fallback; unions an injected `semanticSearch` arm when a `query` is given (tenant-scoped via its `where`). Refreshes `last_used_at` on returned rows.\n- Injected `semanticSearch` matches `SmrtCollection.semanticSearch`, so `LearningMemory` never reaches into a collection's internals.\n\n## SmrtCollection Query\n\n```typescript\nawait collection.list({\n where: { status: 'active', price: { op: '>', value: 10 } },\n limit: 50, offset: 0, orderBy: 'created_at DESC'\n});\n```\n\nProjection primitive (#1902): pass `select: ['id', 'title', 'tenantId']` to\n`list()` when an admin/list workflow needs compact rows. `select` uses SMRT\nfield names, maps them to DB columns internally, and returns plain objects keyed\nby the same SMRT field names without hydrating `SmrtObject` instances. It\ncomposes with `where`, `orderBy`, `limit`, and `offset`; `beforeList`\ninterceptors still run. It is for column-backed fields only and cannot combine\nwith `include`/relationship eager loading.\n\n**WHERE operators**: `=`, `>`, `<`, `>=`, `<=`, `!=`, `in`, `not in`, `like`, `is null`, `is not null`. Arrays auto-detect `IN`. Dot notation for JSON paths: `metadata.userId`.\n\nSTI child collections auto-filter by `_meta_type`.\n\n## Object Memory & Semantic Search\n\nTwo persistence primitives every `SmrtObject`/`SmrtCollection` inherits — load-bearing for learning agents, usable by any object. Full guide: `docs/content/core.md` → \"Context Memory System\".\n\n- **Context memory** (`remember`/`recall`/`recallAll`/`forget`/`forgetScope`, table `_smrt_contexts`): stores any JSON value keyed by `(owner_class, owner_id, scope, key, version)` with a `confidence` score (0–1) and a stored `expiresAt` (metadata — `recall()` does **not** filter expired rows; expiry is caller-managed). `recall()` returns the highest-confidence match with an optional `minConfidence` floor and **opt-in** hierarchical scope fallback (`includeAncestors: true` → `'a/b/c' → 'a/b' → 'a' → 'global'`; default off); `recallAll()` returns a `Map`. Typical use: cache a learned strategy (e.g. a working selector per host) and reuse it across sessions. `success_count`/`failure_count` columns exist for outcome-weighting but are not auto-updated by the framework.\n- **Semantic search** (on `SmrtCollection`, table `_smrt_embeddings`): `semanticSearch(query)`, `findSimilar(object)`, `findSimilarToEmbedding(vector)` — cosine ranking over embeddings of the fields declared in `@smrt({ embeddings })`. Native pgvector/HNSW when configured, in-memory `CosineSimilarity` fallback otherwise; default local model `Xenova/bge-base-en-v1.5` (768-dim) or AI `text-embedding-3-small`. Hits hydrate via `list({ 'id in': … })`, so `@TenantScoped` isolation applies to results.\n\n## @smrt() Decorator Options\n\nKey options: `tableName`, `tableStrategy` ('cti'|'sti'), `conflictColumns`, `api`/`mcp`/`cli` (generation config), `ai` (callable methods), `hooks` (beforeSave/afterSave/beforeDelete/afterDelete), `embeddings` (auto-generate), `tenantScoped`, `agent`.\n\nRegistration sets `SMRT_TABLE_NAME` static property (survives minification).\n\n## Domain Knowledge Artifacts\n\n`smrtPlugin()` writes runtime manifests and agent/developer knowledge artifacts:\n\n- local dev/build: `.smrt/manifest.json` and `.smrt/smrt-knowledge.json`\n- package build: `dist/manifest.json` and `dist/smrt-knowledge.json`\n\nKeep `manifest.json` runtime-focused. `smrt-knowledge.json` is the deterministic\nagent contract for downstream review and architecture tools.\n\nConfig precedence for knowledge is defaults → top-level `knowledge` in\n`smrt.config.ts` → `packages[packageName].knowledge` → plugin option →\nobject-level `@smrt({ knowledge })`.\n\nObject-level `knowledge: false` excludes an object from authored context only;\nit must not change runtime manifest registration. Use\n`knowledge: { tags, summary, risks }` for review-sensitive domain objects.\n\nHTTP knowledge routes are disabled by default. If `knowledge.api.enabled` is\ntrue, generated SvelteKit routes must stay GET-only and guarded by dev mode or\nadmin auth.\n\n## DispatchBus\n\n- `emit(signalType, payload, metadata)` → creates persistent Dispatch record\n- `on(pattern, handler)` → in-memory handler (immediate)\n- `subscribe({ signalType, subscriber })` → persistent subscription (survives restarts)\n- `process(subscriberName, handler)` → process pending dispatches\n- Wildcards: `campaign.*` matches `campaign.completed` (single segment only)\n- Tables: `_smrt_dispatch`, `_smrt_dispatch_subscriptions`\n- Status: `pending → processing → completed` (or `failed`)\n\n## Change Feed (#1758)\n\nAdapter-agnostic change-observation spine (`src/change-feed.ts`) — the server half of the client/mobile sync contract (PRD #1755):\n\n- `_smrt_changes` system table: one append per framework save/delete via a GlobalInterceptors writer registered at framework init. Deletes are tombstones (`operation: 'delete'`). `_smrt_*` tables are skipped. Feed-append failures log and never fail the user's write. No dirty-check: a field-unchanged `.save()` appends a spurious `update` entry (diff-aware paths like `getOrUpsert`/sync-apply short-circuit before `save()` and append nothing); subscribers must tolerate spurious entries — they are convergent.\n- Sequences: allocated as `MAX(seq)+1` inside the INSERT with conflict retry — committed rows stay contiguous, so commit order == seq order on SQLite/Postgres/DuckDB (deliberately NOT identity/serial: those allocate before commit and break the cursor guarantee under concurrent writers).\n- `getChangesSince(db, { since, tables?, tenantId?, limit? }) → { changes, cursor, resyncRequired?, resyncCursor? }`: strictly monotonic cursor; polling with returned cursors misses no committed change and never repeats one. A cursor that cannot be served incrementally — pruned below the retained `[floor..horizon]` run, or foreign/ahead of the horizon — gets `resyncRequired: true` with empty `changes`, an unadvanced `cursor`, and `resyncCursor` set to the current horizon so clients can full-refetch then resume incrementally; detection runs on the UNFILTERED log so `tables`/`tenantId` filters never trigger or mask it. `getTenantScopedChangesSince()` resolves tenant via the DispatchBus resolver hook (fail-closed: tenancy on + no context → global rows only; tenant `T` sees `T` + global rows, never another tenant).\n- `getTableVersion(db, table) → number`: the per-table change version (`MAX(seq)` for the table, replica-stable — no per-process divergence), the ETag source for zero-query conditional GETs (#1765). Advances on any framework write to the table (CRUD and sync-apply, which all `save()`/`delete()`). A table with no retained entry of its own falls back to the global horizon (never a resettable low value) so an all-pruned table cannot false-304 a stale client; only 0 when the feed is empty.\n- Generated `_changes` routes: REST (`GET {basePath}/_changes`, requires `authMiddleware`, otherwise 401 — per-model `api.public` does NOT apply) and SvelteKit (`{routesDir}/_changes/+server.ts`, requires an authenticated principal on `locals`; opt out via `sveltekit.changesRoute.enabled: false`). Query params: `since`, `tables` (comma-separated), `limit`. Responses stay HTTP 200 in the resync state — `resyncRequired` is protocol state, not an error, and `resyncCursor` is the resume cursor after the client completes a full refetch.\n- Retention: `pruneChangeFeed(db, { maxAgeMs?, maxRows? })` — schedule it. Pruning deletes oldest-first and always retains the newest entry (a non-empty feed is never emptied), which is what makes pruned-cursor detection provable and keeps caught-up consumers polling normally. Raw-SQL writes are invisible to the feed (same documented gap as the #1499 cache); `bumpChangeFeed(db, { table, rowId? })` is the manual escape hatch.\n\n## Live Events / Change Signals (#1763, server half)\n\nThe push companion to the change feed (`src/change-signals.ts` + the generated `_events` SSE route) — the server half of live cache invalidation (PRD #1755). The client subscriber (two-client/reconnect/polling-fallback ACs) is a separate later slice.\n\n- **Change-signal bus** (`src/change-signals.ts`): every framework save/delete that appends a durable feed row also publishes a coarse `ChangeSignal` `{ table, operation, rowId, tenantId, seq }` — **never a row payload** (authorization stays on the read path). Structurally mirrors the collection cache's notify/listen path. `subscribeToChangeSignals(db, listener) → unsubscribe`; `publishChangeSignal`/`broadcastChangeSignal`/the listener loop stay internal. `appendChange` now returns the allocated `seq` (was `void`); the signal carries it as a coarse resume cursor (`bumpChangeFeed` ignores the return). The publish runs only after the append SUCCEEDS (no signal without a durable feed row) and in its own log-and-swallow try/catch, so a signal problem never fails the user's write. `_smrt_*` writes never signal (the writer skips them). Delivery is synchronous per-listener with per-listener try/catch (one throwing SSE controller never blocks others); no per-subscriber queue — backpressure rides the platform `ReadableStream`.\n- **In-process + cross-replica**: locally-published and peer-received signals go through the SAME `deliverLocally` path. Cross-replica fan-out rides the db adapter's optional notification capability (`db.notifications`, a NEW `smrt_change_signals` channel distinct from the cache channel) with echo-avoidance by `PROCESS_ID`. No capability → in-process only, warn-once, **never an error, never blocks the write** (subscribers on other replicas fall back to cursor polling).\n- **Generated `_events` SSE route**: REST (`GET {basePath}/_events`, requires `authMiddleware`, otherwise 401 — fail-closed, per-model `api.public` does NOT apply; 405 non-GET; 503 no db or subscriber capacity reached) and SvelteKit (`{routesDir}/_events/+server.ts`, requires an authenticated principal on `locals`; opt out via `sveltekit.eventsRoute.enabled: false`; cap via `sveltekit.eventsRoute.maxSubscribers`, where `0` means unlimited). Tenant scope is captured ONCE at connection open (`resolveDispatchTenantScope`) and filtered server-side per signal via `signalVisibleToTenant` (same rule as `getChangesSince`'s tenant filter) before any byte hits the wire — delivery runs outside any tenant ALS context, so it must use the captured value. The stream lifecycle lives in `buildChangeEventStream(db, { cursor, tenantScope, heartbeatMs?, manifestHash? })` (exported; the SvelteKit route imports it so it stays thin): subscribe-before-catch-up (closes the gap window; overlap is deduped by the SSE `id:`/seq client-side), `retry: 3000`, optional connection-open `event: manifest` carrying `{ manifestHash }` for live contract detection, cursor catch-up via `getChangesSince` filtered by the CAPTURED scope (not re-resolved from ALS, so it matches the live-signal filter exactly and can't replay another tenant's rows) (`Last-Event-ID` header beats `?since=`; default = live-forward only; `resyncRequired` → `event: resync`), heartbeat (`DEFAULT_EVENTS_HEARTBEAT_MS` = 15s), and `cancel()` teardown (clears heartbeat + unsubscribes). SSE change frame: `id: <seq>\\nevent: change\\ndata: {table,operation,rowId,tenantId}\\n\\n` — seq is ONLY in the `id:` line, never the data JSON. Subscriber cap default is `DEFAULT_EVENTS_MAX_SUBSCRIBERS` = 1000; over-cap connections return retryable 503 + `Retry-After`, and existing subscribers are unaffected. **Cross-origin is opt-in and fail-closed (#1861)**: same-origin only by default. REST wraps `_events` in its CORS layer — set `enableCors` + an explicit `allowedOrigins` allowlist + `allowCredentials: true` and an allow-listed browser can subscribe with a credentialed `EventSource` (`withCredentials: true`); the response echoes the specific `Origin` (never `*`) plus `Access-Control-Allow-Credentials: true`. SvelteKit mirrors this via `sveltekit.eventsRoute.allowedOrigins` + `allowCredentials` (the generated route bakes the allowlist into a `Set`, echoes only a member origin, and answers a credentialed `OPTIONS` preflight). CORS never authorizes — the fail-closed auth guard and captured tenant scope are unchanged, so the read posture holds identically across origins; it only lets an allow-listed browser's cookies reach the guard. Client disconnect through the Node `createServer` bridge now cancels the response reader (was a teardown leak) so `cancel()` fires and the subscription is released.\n- **Known gaps** (documented in the module): raw-SQL writes don't signal (same gap as the feed); live signals for caller-managed-transaction writes are best-effort (the append + signal fire pre-commit, so a rolled-back write may emit a signal and its freed seq is later reused) — the autocommit default path is exact, and clients reconcile via full catch-up/resync (inherits the change feed's transaction caveat).\n\n## Single Table Inheritance (STI)\n\n- Base: `@smrt({ tableStrategy: 'sti' })` — children inherit, share one table\n- Discriminator: `_meta_type` column with qualified names (`@happyvertical/smrt-content:Article`)\n- Child fields: `@meta()` decorator → stored in `_meta_data` JSONB (not as columns)\n- Polymorphic queries: collection loads `_meta_type`, creates correct subclass dynamically\n- Validation: fail-fast on save if `_meta_type` missing or mismatched\n\n## Code Generators\n\n| Generator | Location | Output |\n|-----------|----------|--------|\n| REST API | `src/generators/rest.ts` | OpenAPI-compliant CRUD endpoints |\n| CLI | `src/generators/cli.ts` | `objectname:action` admin commands — writable allowlist, exhaustive-include, `--from-file`, fail-closed tenant context |\n| MCP Server | `src/generators/mcp.ts` | Model Context Protocol tools |\n| Web collections | `src/vite-plugin/web-collections.ts` (selectors) + `generateWebModule` | `@happyvertical/smrt-virt-web` — one typed collection definition per API-exposed REST collection (#1761), consumed by `@happyvertical/smrt-web` |\n\nGenerated API clients share `selectApiClientEntries()` across the runtime Vite\nmodule, its ambient declaration, and physical prebuild declarations. When a\ncollection class and its populated model share an endpoint, the model owns the\ncanonical collection key and row payload schema; the collection class remains\navailable under a deterministic class-derived secondary key. Selection and\ncollision suffixes must not depend on manifest insertion order (#2027).\nFor aggregated manifests, inheritance and item-type references resolve exact\nqualified names first, then package-local simple names, then a stable identity\nfallback so duplicate class names across packages cannot reintroduce ordering.\n\nThe web module also emits a build-time **`manifestHash`** constant (#1764): `computeWebManifestHash(manifest)` is a deterministic, replica-stable digest of the emitted web-collection SHAPE (name/className/endpoint/idField/actions/fields/relationships), canonicalized (recursive key sort) before `sha256 → base64url`, truncated to 16 chars — so the same schema always hashes the same, and a field add/remove/type-change/edge-change changes it. A change means old persisted client rows may mis-hydrate, so smrt-web keys its durable persistence namespace on it and its `updateAvailable` contract signal compares against it. Three emission sites must not drift: the runtime value (`generateWebModule`), the `@happyvertical/smrt-virt-web` ambient d.ts (`vite-plugin/index.ts`), and the physical `@smrt/web` d.ts (`prebuild/index.ts`).\n\nGenerated reads (`list`/`get`) on the REST and SvelteKit generators support conditional GET (helpers in `src/generators/conditional-get.ts`). ETag v2 (#1765): the validator is the table's change-feed version (`getTableVersion`) keyed by the request representation, so a **concrete** `If-None-Match` short-circuits into a 304 with an empty body **before** the collection query runs — an unchanged table revalidates with zero table scan. A wildcard `If-None-Match: *` is deferred until the payload builds (existence confirmed), so a missing item still returns 404, not a false 304. Tenant-scoped reads fold the active tenant into the representation (`resolveTenantEtagDiscriminator`) so one tenant's cached validator never satisfies another's read of the same URL. Routes whose GET renders via a **custom serializer** (which can load related tables the base-table version can't observe) keep the v1 body-hash ETag (`#1757`, query-first but correct); the default `toPublicJSON` path — all REST reads and non-serializer SvelteKit reads — uses v2. v2 is weakly consistent by design (the cost of not reading the data): a revalidation in the sub-statement window between a committed write and its feed append can return a stale 304 that self-heals on the next revalidation. The other v2 window — a deploy that changes the response shape WITHOUT a table write — is closed by the **#1764 ETag salt**: `computeTableVersionEtag(version, representation, manifestHash?)` folds the build's web-collection shape digest into the digest, so a shape-only redeploy busts every read validator (`undefined` reproduces the pre-#1764 unsalted value byte-for-byte for direct helper callers). The generated SvelteKit route bakes the digest in as a `MANIFEST_HASH` constant (via `generateConditionalGetRouteHelper`'s `manifestHash` option, sourced from `computeWebManifestHash(manifest)`) — automatic for the SvelteKit transport. The runtime `APIGenerator` auto-populates the same salt from the runtime registry with `computeRuntimeWebManifestHash()` when `APIConfig.manifestHash` is omitted; explicit `APIConfig.manifestHash` still wins for custom setups. The digest scope is get-OR-list (`selectWebEtagSaltEntries`), so **get-only** routes are salted too. Strong consistency still requires the v1 body-hash path. Cache-Control policy (unchanged from #1757): `private, no-cache` by default; public models may opt into shared caching via `@smrt({ api: { public: true | 'read', cache: { sMaxage } } })` → `public, max-age=0, s-maxage=<n>`; non-public models never emit shared-cache headers. Tenant-scoped models (any mode) never emit them either — bodies vary with session-cookie tenant context that URL-keyed shared caches cannot see; `sMaxage` is neutralized to `private, no-cache` with a one-time warning.\n\n## Child Accessors (R10)\n\n`src/child-accessors.ts` installs a consistent `get<FieldName>()` instance method for every `@oneToMany` field at `@smrt()` registration time (e.g. `@oneToMany('OrderItem') items` → `order.getItems()`), delegating to `loadRelatedMany`. Two invariants:\n\n- **Additive** — never overwrites a hand-rolled method of the same name (checks the whole prototype chain). `Profile.getMetadata()` (key-value) and `ProfileRelationship.getTerms()` are preserved.\n- **Runtime-only** — attached to the prototype, invisible to the build-time manifest, so it never leaks into the REST/CLI/MCP surface.\n\nWhen the target declares multiple FKs back to the parent, annotate `@oneToMany(Target, { foreignKey: '<inverseField>' })`; `loadRelatedMany` and the eager `include:` loader both honor it (else first-match).\n\n## Vite Plugin\n\n```typescript\n// vite.config.ts — required for @smrt() decorators (Vite 8+, oxc transform)\nexport default defineConfig({\n oxc: {\n decorator: {\n legacy: true,\n emitDecoratorMetadata: true,\n },\n },\n});\n```\n\nUnder Vite 8 the oxc transform does not honor the pre-Vite-8 `esbuild.tsconfigRaw`\nrecipe (or tsconfig `experimentalDecorators` reached through SvelteKit's\n`extends \"./.svelte-kit/tsconfig.json\"` chain), so that recipe throws\n`SyntaxError: Invalid or unexpected token` on the first SSR request. Configure\ndecorators through `oxc.decorator` instead. Consumers still pinned on vite<8 need\nthe legacy `esbuild.tsconfigRaw` form with `experimentalDecorators: true,\nemitDecoratorMetadata: true`.\n\n## Gotchas\n\n- **Filesystem support is a lazy boundary (#1979)**: `SmrtClass` acquires `options.fs` adapters via `createFilesystemAdapter()` (`src/filesystem-loader.ts`), never a static `@happyvertical/files` import — the files SDK statically pulls @aws-sdk/client-s3 and reaches googleapis, and a static edge here would land it in every downstream SSR bundle. Node/tsx/vite-dev runtimes resolve it on first use; fully-bundled deployments import `@happyvertical/smrt-core/filesystem` at startup. Use `importOptionalDependency()` (`src/lazy-external.ts`) for any similar optional heavyweight dependency.\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"
328
328
  }
@@ -0,0 +1,18 @@
1
+ import { SmartObjectDefinition, SmartObjectManifest } from '../scanner/types.js';
2
+ export interface ApiClientEntry {
3
+ /** Original key in manifest.objects. */
4
+ objectName: string;
5
+ /** Object whose routes and custom methods this client entry exposes. */
6
+ obj: SmartObjectDefinition;
7
+ /** Canonical collection key or deterministic class-derived secondary key. */
8
+ clientKey: string;
9
+ /** Row-payload interface used by CRUD methods for this entry. */
10
+ dataInterfaceName: string;
11
+ }
12
+ /**
13
+ * Select every generated client entry with deterministic endpoint keys and row
14
+ * payload types. Exactly one entry owns each canonical collection key; other
15
+ * API objects remain available under stable class-derived secondary keys.
16
+ */
17
+ export declare function selectApiClientEntries(manifest: SmartObjectManifest): ApiClientEntry[];
18
+ //# sourceMappingURL=api-client-entries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client-entries.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/api-client-entries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,qBAAqB,CAAC;AAM7B,MAAM,WAAW,cAAc;IAC7B,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,GAAG,EAAE,qBAAqB,CAAC;IAC3B,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAuKD;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,mBAAmB,GAC5B,cAAc,EAAE,CAmElB"}
@@ -0,0 +1,146 @@
1
+ import { findManifestObjectByName, isCollectionManifestClass } from "./web-collections.js";
2
+ //#region src/vite-plugin/api-client-entries.ts
3
+ function compareText(left, right) {
4
+ if (left < right) return -1;
5
+ if (left > right) return 1;
6
+ return 0;
7
+ }
8
+ function candidateIdentity(candidate) {
9
+ return [
10
+ candidate.obj.qualifiedName ?? "",
11
+ candidate.obj.className,
12
+ candidate.objectName
13
+ ].join("\0");
14
+ }
15
+ function compareCandidateIdentity(left, right) {
16
+ return compareText(candidateIdentity(left), candidateIdentity(right));
17
+ }
18
+ function lowerFirst(value) {
19
+ return value ? value[0].toLowerCase() + value.slice(1) : value;
20
+ }
21
+ function isAncestorOf(manifest, ancestor, descendant) {
22
+ const seen = /* @__PURE__ */ new Set();
23
+ let child = descendant;
24
+ let parentName = child.extendsQualified || child.extends;
25
+ while (parentName && !seen.has(parentName)) {
26
+ seen.add(parentName);
27
+ const parent = findManifestObjectByName(manifest, parentName, child);
28
+ if (!parent) return false;
29
+ if (parent === ancestor) return true;
30
+ child = parent;
31
+ parentName = parent.extendsQualified || parent.extends;
32
+ }
33
+ return false;
34
+ }
35
+ function resolveCollectionItemObject(obj, manifest) {
36
+ const seen = /* @__PURE__ */ new Set();
37
+ let candidate = obj;
38
+ while (candidate) {
39
+ if (candidate.extendsTypeArg) {
40
+ const itemObject = findManifestObjectByName(manifest, candidate.extendsTypeArg, candidate);
41
+ if (itemObject) return itemObject;
42
+ }
43
+ if (candidate.className.endsWith("Collection")) {
44
+ const conventionalItem = findManifestObjectByName(manifest, candidate.className.slice(0, -10), candidate);
45
+ if (conventionalItem) return conventionalItem;
46
+ }
47
+ const parentName = candidate.extendsQualified || candidate.extends;
48
+ if (!parentName || seen.has(parentName)) return void 0;
49
+ seen.add(parentName);
50
+ candidate = findManifestObjectByName(manifest, parentName, candidate);
51
+ }
52
+ }
53
+ function resolveDataInterfaceName(obj, manifest) {
54
+ if (!isCollectionManifestClass(manifest, obj)) return `${obj.className}Data`;
55
+ return `${resolveCollectionItemObject(obj, manifest)?.className || obj.className}Data`;
56
+ }
57
+ function sharedCollectionModelDepth(manifest, obj) {
58
+ const seen = /* @__PURE__ */ new Set();
59
+ let depth = 0;
60
+ let child = obj;
61
+ let parentName = child.extendsQualified || child.extends;
62
+ while (parentName && !seen.has(parentName)) {
63
+ seen.add(parentName);
64
+ const parent = findManifestObjectByName(manifest, parentName, child);
65
+ if (!parent || parent.collection !== obj.collection) break;
66
+ depth += 1;
67
+ child = parent;
68
+ parentName = parent.extendsQualified || parent.extends;
69
+ }
70
+ return depth;
71
+ }
72
+ /**
73
+ * Select the owner of one canonical collection endpoint with a transitive rank.
74
+ *
75
+ * Models beat collection classes because CRUD payloads are rows. Among models,
76
+ * shallower same-collection ancestry wins, then a root with more descendants
77
+ * wins (the STI base over an unrelated endpoint collision), then qualified
78
+ * identity resolves any remaining ambiguity. Numeric/lexical tuple ranks stay
79
+ * transitive, unlike pairwise ancestry overrides inside Array.sort().
80
+ */
81
+ function selectCanonicalOwner(manifest, group) {
82
+ const models = group.filter((candidate) => !isCollectionManifestClass(manifest, candidate.obj));
83
+ if (models.length > 0) return [...models].map((candidate) => ({
84
+ candidate,
85
+ depth: sharedCollectionModelDepth(manifest, candidate.obj),
86
+ descendantCount: models.filter((other) => other !== candidate && isAncestorOf(manifest, candidate.obj, other.obj)).length
87
+ })).sort((left, right) => left.depth - right.depth || right.descendantCount - left.descendantCount || compareCandidateIdentity(left.candidate, right.candidate))[0]?.candidate;
88
+ return [...group].sort((left, right) => {
89
+ const leftHasItem = resolveCollectionItemObject(left.obj, manifest);
90
+ const rightHasItem = resolveCollectionItemObject(right.obj, manifest);
91
+ if (Boolean(leftHasItem) !== Boolean(rightHasItem)) return leftHasItem ? -1 : 1;
92
+ return compareCandidateIdentity(left, right);
93
+ })[0];
94
+ }
95
+ /**
96
+ * Select every generated client entry with deterministic endpoint keys and row
97
+ * payload types. Exactly one entry owns each canonical collection key; other
98
+ * API objects remain available under stable class-derived secondary keys.
99
+ */
100
+ function selectApiClientEntries(manifest) {
101
+ const candidates = Object.entries(manifest.objects).map(([objectName, obj]) => ({
102
+ objectName,
103
+ obj
104
+ }));
105
+ const candidatesByCollection = /* @__PURE__ */ new Map();
106
+ for (const candidate of candidates) {
107
+ const group = candidatesByCollection.get(candidate.obj.collection) ?? [];
108
+ group.push(candidate);
109
+ candidatesByCollection.set(candidate.obj.collection, group);
110
+ }
111
+ const canonicalOwners = /* @__PURE__ */ new Map();
112
+ for (const [collection, group] of candidatesByCollection) {
113
+ const canonicalOwner = selectCanonicalOwner(manifest, group);
114
+ if (canonicalOwner) canonicalOwners.set(collection, canonicalOwner);
115
+ }
116
+ const orderedCandidates = [...candidates].sort((left, right) => {
117
+ const collectionOrder = compareText(left.obj.collection, right.obj.collection);
118
+ if (collectionOrder !== 0) return collectionOrder;
119
+ const leftIsCanonical = canonicalOwners.get(left.obj.collection) === left;
120
+ if (leftIsCanonical !== (canonicalOwners.get(right.obj.collection) === right)) return leftIsCanonical ? -1 : 1;
121
+ return compareCandidateIdentity(left, right);
122
+ });
123
+ const reservedCanonicalKeys = new Set(canonicalOwners.keys());
124
+ const usedKeys = /* @__PURE__ */ new Set();
125
+ return orderedCandidates.map(({ objectName, obj }) => {
126
+ const isCanonicalOwner = canonicalOwners.get(obj.collection)?.objectName === objectName;
127
+ let clientKey = isCanonicalOwner ? obj.collection : lowerFirst(obj.className);
128
+ const baseClientKey = clientKey;
129
+ let suffix = 2;
130
+ while (usedKeys.has(clientKey) || !isCanonicalOwner && reservedCanonicalKeys.has(clientKey)) {
131
+ clientKey = `${baseClientKey}${suffix}`;
132
+ suffix += 1;
133
+ }
134
+ usedKeys.add(clientKey);
135
+ return {
136
+ objectName,
137
+ obj,
138
+ clientKey,
139
+ dataInterfaceName: resolveDataInterfaceName(obj, manifest)
140
+ };
141
+ });
142
+ }
143
+ //#endregion
144
+ export { selectApiClientEntries };
145
+
146
+ //# sourceMappingURL=api-client-entries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client-entries.js","names":[],"sources":["../../src/vite-plugin/api-client-entries.ts"],"sourcesContent":["/**\n * Manifest -> generated API-client entry selection.\n *\n * Runtime client values, Vite ambient declarations, and physical prebuild\n * declarations must agree on which manifest object owns a collection's\n * canonical endpoint. Collection classes describe access methods, not row\n * payloads, so a populated model always wins the shared collection key.\n */\n\nimport type {\n SmartObjectDefinition,\n SmartObjectManifest,\n} from '../scanner/types.js';\nimport {\n findManifestObjectByName,\n isCollectionManifestClass,\n} from './web-collections.js';\n\nexport interface ApiClientEntry {\n /** Original key in manifest.objects. */\n objectName: string;\n /** Object whose routes and custom methods this client entry exposes. */\n obj: SmartObjectDefinition;\n /** Canonical collection key or deterministic class-derived secondary key. */\n clientKey: string;\n /** Row-payload interface used by CRUD methods for this entry. */\n dataInterfaceName: string;\n}\n\ninterface ManifestCandidate {\n objectName: string;\n obj: SmartObjectDefinition;\n}\n\nfunction compareText(left: string, right: string): number {\n if (left < right) return -1;\n if (left > right) return 1;\n return 0;\n}\n\nfunction candidateIdentity(candidate: ManifestCandidate): string {\n return [\n candidate.obj.qualifiedName ?? '',\n candidate.obj.className,\n candidate.objectName,\n ].join('\\0');\n}\n\nfunction compareCandidateIdentity(\n left: ManifestCandidate,\n right: ManifestCandidate,\n): number {\n return compareText(candidateIdentity(left), candidateIdentity(right));\n}\n\nfunction lowerFirst(value: string): string {\n return value ? value[0].toLowerCase() + value.slice(1) : value;\n}\n\nfunction isAncestorOf(\n manifest: SmartObjectManifest,\n ancestor: SmartObjectDefinition,\n descendant: SmartObjectDefinition,\n): boolean {\n const seen = new Set<string>();\n let child = descendant;\n let parentName = child.extendsQualified || child.extends;\n\n while (parentName && !seen.has(parentName)) {\n seen.add(parentName);\n const parent = findManifestObjectByName(manifest, parentName, child);\n if (!parent) return false;\n if (parent === ancestor) return true;\n child = parent;\n parentName = parent.extendsQualified || parent.extends;\n }\n\n return false;\n}\n\nfunction resolveCollectionItemObject(\n obj: SmartObjectDefinition,\n manifest: SmartObjectManifest,\n): SmartObjectDefinition | undefined {\n const seen = new Set<string>();\n let candidate: SmartObjectDefinition | undefined = obj;\n\n while (candidate) {\n if (candidate.extendsTypeArg) {\n const itemObject = findManifestObjectByName(\n manifest,\n candidate.extendsTypeArg,\n candidate,\n );\n if (itemObject) return itemObject;\n }\n\n if (candidate.className.endsWith('Collection')) {\n const conventionalItem = findManifestObjectByName(\n manifest,\n candidate.className.slice(0, -'Collection'.length),\n candidate,\n );\n if (conventionalItem) return conventionalItem;\n }\n\n const parentName = candidate.extendsQualified || candidate.extends;\n if (!parentName || seen.has(parentName)) return undefined;\n seen.add(parentName);\n candidate = findManifestObjectByName(manifest, parentName, candidate);\n }\n\n return undefined;\n}\n\nfunction resolveDataInterfaceName(\n obj: SmartObjectDefinition,\n manifest: SmartObjectManifest,\n): string {\n if (!isCollectionManifestClass(manifest, obj)) {\n return `${obj.className}Data`;\n }\n\n const itemObject = resolveCollectionItemObject(obj, manifest);\n return `${itemObject?.className || obj.className}Data`;\n}\n\nfunction sharedCollectionModelDepth(\n manifest: SmartObjectManifest,\n obj: SmartObjectDefinition,\n): number {\n const seen = new Set<string>();\n let depth = 0;\n let child = obj;\n let parentName = child.extendsQualified || child.extends;\n\n while (parentName && !seen.has(parentName)) {\n seen.add(parentName);\n const parent = findManifestObjectByName(manifest, parentName, child);\n if (!parent || parent.collection !== obj.collection) break;\n depth += 1;\n child = parent;\n parentName = parent.extendsQualified || parent.extends;\n }\n\n return depth;\n}\n\n/**\n * Select the owner of one canonical collection endpoint with a transitive rank.\n *\n * Models beat collection classes because CRUD payloads are rows. Among models,\n * shallower same-collection ancestry wins, then a root with more descendants\n * wins (the STI base over an unrelated endpoint collision), then qualified\n * identity resolves any remaining ambiguity. Numeric/lexical tuple ranks stay\n * transitive, unlike pairwise ancestry overrides inside Array.sort().\n */\nfunction selectCanonicalOwner(\n manifest: SmartObjectManifest,\n group: ManifestCandidate[],\n): ManifestCandidate | undefined {\n const models = group.filter(\n (candidate) => !isCollectionManifestClass(manifest, candidate.obj),\n );\n\n if (models.length > 0) {\n return [...models]\n .map((candidate) => ({\n candidate,\n depth: sharedCollectionModelDepth(manifest, candidate.obj),\n descendantCount: models.filter(\n (other) =>\n other !== candidate &&\n isAncestorOf(manifest, candidate.obj, other.obj),\n ).length,\n }))\n .sort(\n (left, right) =>\n left.depth - right.depth ||\n right.descendantCount - left.descendantCount ||\n compareCandidateIdentity(left.candidate, right.candidate),\n )[0]?.candidate;\n }\n\n return [...group].sort((left, right) => {\n const leftHasItem = resolveCollectionItemObject(left.obj, manifest);\n const rightHasItem = resolveCollectionItemObject(right.obj, manifest);\n if (Boolean(leftHasItem) !== Boolean(rightHasItem)) {\n return leftHasItem ? -1 : 1;\n }\n return compareCandidateIdentity(left, right);\n })[0];\n}\n\n/**\n * Select every generated client entry with deterministic endpoint keys and row\n * payload types. Exactly one entry owns each canonical collection key; other\n * API objects remain available under stable class-derived secondary keys.\n */\nexport function selectApiClientEntries(\n manifest: SmartObjectManifest,\n): ApiClientEntry[] {\n const candidates = Object.entries(manifest.objects).map(\n ([objectName, obj]): ManifestCandidate => ({ objectName, obj }),\n );\n const candidatesByCollection = new Map<string, ManifestCandidate[]>();\n\n for (const candidate of candidates) {\n const group = candidatesByCollection.get(candidate.obj.collection) ?? [];\n group.push(candidate);\n candidatesByCollection.set(candidate.obj.collection, group);\n }\n\n const canonicalOwners = new Map<string, ManifestCandidate>();\n for (const [collection, group] of candidatesByCollection) {\n const canonicalOwner = selectCanonicalOwner(manifest, group);\n if (canonicalOwner) canonicalOwners.set(collection, canonicalOwner);\n }\n\n // Canonical owners are emitted first within each collection. Sorting all\n // remaining candidates by identity makes numeric suffixes stable too.\n const orderedCandidates = [...candidates].sort((left, right) => {\n const collectionOrder = compareText(\n left.obj.collection,\n right.obj.collection,\n );\n if (collectionOrder !== 0) return collectionOrder;\n\n const leftIsCanonical = canonicalOwners.get(left.obj.collection) === left;\n const rightIsCanonical =\n canonicalOwners.get(right.obj.collection) === right;\n if (leftIsCanonical !== rightIsCanonical) {\n return leftIsCanonical ? -1 : 1;\n }\n\n return compareCandidateIdentity(left, right);\n });\n\n // A secondary key must never shadow any canonical endpoint, including a\n // later collection whose canonical owner has not been emitted yet.\n const reservedCanonicalKeys = new Set(canonicalOwners.keys());\n const usedKeys = new Set<string>();\n\n return orderedCandidates.map(({ objectName, obj }) => {\n const isCanonicalOwner =\n canonicalOwners.get(obj.collection)?.objectName === objectName;\n let clientKey = isCanonicalOwner\n ? obj.collection\n : lowerFirst(obj.className);\n const baseClientKey = clientKey;\n let suffix = 2;\n\n while (\n usedKeys.has(clientKey) ||\n (!isCanonicalOwner && reservedCanonicalKeys.has(clientKey))\n ) {\n clientKey = `${baseClientKey}${suffix}`;\n suffix += 1;\n }\n\n usedKeys.add(clientKey);\n return {\n objectName,\n obj,\n clientKey,\n dataInterfaceName: resolveDataInterfaceName(obj, manifest),\n };\n });\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EACV,qBAAqB,EAEtB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,MAAM,EAAiC,MAAM,MAAM,CAAC;AAIlE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAoB5D,YAAY,EACV,wBAAwB,EACxB,gBAAgB,GACjB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,6BAA6B,EAC7B,uBAAuB,EACvB,iBAAiB,EACjB,mBAAmB,EACnB,4BAA4B,GAC7B,MAAM,0BAA0B,CAAC;AAalC,MAAM,WAAW,iBAAiB;IAChC,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,0BAA0B;IAC1B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,6BAA6B;IAC7B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qBAAqB;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,sCAAsC;IACtC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,kFAAkF;IAClF,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,4EAA4E;IAC5E,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,wEAAwE;IACxE,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;IACpC;;OAEG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,8CAA8C;IAC9C,SAAS,CAAC,EAAE;QACV,yDAAyD;QACzD,OAAO,EAAE,OAAO,CAAC;QACjB,wEAAwE;QACxE,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,qEAAqE;QACrE,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,mEAAmE;QACnE,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,mDAAmD;QACnD,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB;;;;WAIG;QACH,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB;;;WAGG;QACH,YAAY,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,OAAO,CAAA;SAAE,CAAC;QACrC;;;;WAIG;QACH,WAAW,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,OAAO,CAAC;YAAC,cAAc,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAC9D,CAAC;IACF,mEAAmE;IACnE,SAAS,CAAC,EAAE,qBAAqB,GAAG,KAAK,CAAC;IAC1C;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;AA+BD,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAG1D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,mBAAmB,GAC5B,MAAM,CAaR;AAED,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,MAAM,CA86BlE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EACV,qBAAqB,EAEtB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,MAAM,EAAiC,MAAM,MAAM,CAAC;AAIlE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAqB5D,YAAY,EACV,wBAAwB,EACxB,gBAAgB,GACjB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,6BAA6B,EAC7B,uBAAuB,EACvB,iBAAiB,EACjB,mBAAmB,EACnB,4BAA4B,GAC7B,MAAM,0BAA0B,CAAC;AAalC,MAAM,WAAW,iBAAiB;IAChC,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,0BAA0B;IAC1B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,6BAA6B;IAC7B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qBAAqB;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,sCAAsC;IACtC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,kFAAkF;IAClF,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,4EAA4E;IAC5E,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,wEAAwE;IACxE,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;IACpC;;OAEG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,8CAA8C;IAC9C,SAAS,CAAC,EAAE;QACV,yDAAyD;QACzD,OAAO,EAAE,OAAO,CAAC;QACjB,wEAAwE;QACxE,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,qEAAqE;QACrE,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,mEAAmE;QACnE,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,mDAAmD;QACnD,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB;;;;WAIG;QACH,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB;;;WAGG;QACH,YAAY,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,OAAO,CAAA;SAAE,CAAC;QACrC;;;;WAIG;QACH,WAAW,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,OAAO,CAAC;YAAC,cAAc,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAC9D,CAAC;IACF,mEAAmE;IACnE,SAAS,CAAC,EAAE,qBAAqB,GAAG,KAAK,CAAC;IAC1C;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;AA+BD,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAG1D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,mBAAmB,GAC5B,MAAM,CAaR;AAED,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,MAAM,CA86BlE"}
@@ -4,6 +4,7 @@ import { buildDomainKnowledgeManifest } from "../knowledge.js";
4
4
  import { importWorkspaceModule } from "../utils/import-workspace-module.js";
5
5
  import { discoverSmrtPackages } from "../manifest/discover-smrt-packages.js";
6
6
  import { CLIENT_FETCH_RUNTIME } from "../generated-client-runtime.js";
7
+ import { selectApiClientEntries } from "./api-client-entries.js";
7
8
  import { importBuildAwareModule } from "./import-build-aware.js";
8
9
  import { existsSync, readFileSync } from "node:fs";
9
10
  import { dirname, join } from "node:path";
@@ -591,12 +592,6 @@ export { setupRoutes as default };
591
592
  return "export function setupRoutes(app, options = {}) { console.warn(\"Routes generation failed\"); }";
592
593
  }
593
594
  }
594
- /**
595
- * Generate virtual client module
596
- */
597
- function lowerFirst(value) {
598
- return value ? value[0].toLowerCase() + value.slice(1) : value;
599
- }
600
595
  var GENERATED_CLIENT_CRUD_METHODS = /* @__PURE__ */ new Set([
601
596
  "list",
602
597
  "get",
@@ -605,57 +600,6 @@ var GENERATED_CLIENT_CRUD_METHODS = /* @__PURE__ */ new Set([
605
600
  "delete"
606
601
  ]);
607
602
  var GENERATED_CLIENT_BASE_METHODS = /* @__PURE__ */ new Set([...GENERATED_CLIENT_CRUD_METHODS, "search"]);
608
- function findManifestObjectByName(manifest, name) {
609
- return Object.entries(manifest.objects).find(([manifestKey, candidate]) => manifestKey === name || candidate.qualifiedName === name || candidate.className === name)?.[1];
610
- }
611
- function resolveCollectionItemObject(obj, manifest) {
612
- const seen = /* @__PURE__ */ new Set();
613
- let candidate = obj;
614
- while (candidate) {
615
- if (candidate.extendsTypeArg) {
616
- const itemObject = findManifestObjectByName(manifest, candidate.extendsTypeArg);
617
- if (itemObject) return itemObject;
618
- }
619
- if (candidate.className.endsWith("Collection")) {
620
- const conventionalItem = findManifestObjectByName(manifest, candidate.className.slice(0, -10));
621
- if (conventionalItem) return conventionalItem;
622
- }
623
- const parentName = candidate.extendsQualified || candidate.extends;
624
- if (!parentName || seen.has(parentName)) return void 0;
625
- seen.add(parentName);
626
- candidate = findManifestObjectByName(manifest, parentName);
627
- }
628
- }
629
- function resolveApiClientDataInterfaceName(obj, manifest) {
630
- if (!isCollectionManifestClass(manifest, obj)) return `${obj.className}Data`;
631
- return `${resolveCollectionItemObject(obj, manifest)?.className || obj.className}Data`;
632
- }
633
- function uniqueApiClientEntries(manifest) {
634
- const canonicalCollectionOwners = /* @__PURE__ */ new Map();
635
- const objects = Object.entries(manifest.objects);
636
- for (const [, obj] of objects) {
637
- const current = canonicalCollectionOwners.get(obj.collection);
638
- if (!current || isCollectionManifestClass(manifest, current) && !isCollectionManifestClass(manifest, obj)) canonicalCollectionOwners.set(obj.collection, obj);
639
- }
640
- const reservedCanonicalKeys = new Set(canonicalCollectionOwners.keys());
641
- const usedKeys = /* @__PURE__ */ new Set();
642
- return objects.map(([objectName, obj]) => {
643
- const isCanonicalOwner = canonicalCollectionOwners.get(obj.collection) === obj;
644
- let clientKey = isCanonicalOwner ? obj.collection : lowerFirst(obj.className);
645
- const baseClientKey = clientKey;
646
- let suffix = 2;
647
- while (usedKeys.has(clientKey) || !isCanonicalOwner && reservedCanonicalKeys.has(clientKey)) {
648
- clientKey = `${baseClientKey}${suffix}`;
649
- suffix += 1;
650
- }
651
- usedKeys.add(clientKey);
652
- return {
653
- objectName,
654
- obj,
655
- clientKey
656
- };
657
- });
658
- }
659
603
  function generateClientModule(manifest, options = {}) {
660
604
  return `
661
605
  // Auto-generated API client from SMRT objects
@@ -709,7 +653,7 @@ function __smrtActionUrl(url, options, pathParamNames, includeQuery) {
709
653
  }
710
654
 
711
655
  export function createClient(basePath = '/api/v1') {
712
- return {${uniqueApiClientEntries(manifest).map(({ obj, clientKey }) => {
656
+ return {${selectApiClientEntries(manifest).map(({ obj, clientKey }) => {
713
657
  const { collection, methods = {} } = obj;
714
658
  const exposedActions = resolveApiActionSet(obj);
715
659
  const apiConfig = obj.decoratorConfig?.api;
@@ -988,10 +932,10 @@ ${Object.entries(obj.fields).map(([fieldName, field]) => {
988
932
  updated_at?: string;
989
933
  }`;
990
934
  }).join("\n\n");
991
- const apiClientInterface = uniqueApiClientEntries(manifest).map(({ obj, clientKey }) => {
935
+ const apiClientInterface = selectApiClientEntries(manifest).map(({ obj, clientKey, dataInterfaceName }) => {
992
936
  const { methods = {} } = obj;
993
937
  const apiConfig = obj.decoratorConfig?.api;
994
- const interfaceName = resolveApiClientDataInterfaceName(obj, manifest);
938
+ const interfaceName = dataInterfaceName;
995
939
  const exposedActions = resolveApiActionSet(obj);
996
940
  const customMethods = Object.entries(methods).filter(([name, method]) => !GENERATED_CLIENT_CRUD_METHODS.has(name) && method.isPublic && exposedActions.has(name));
997
941
  const customMethodSignatures = customMethods.map(([methodName, method]) => {
@@ -1285,6 +1229,16 @@ declare module '@happyvertical/smrt-virt-cli' {
1285
1229
  */
1286
1230
  function mapTypeScriptType(smrtType) {
1287
1231
  return {
1232
+ text: "string",
1233
+ integer: "number",
1234
+ decimal: "number",
1235
+ datetime: "string",
1236
+ json: "any",
1237
+ foreignKey: "string",
1238
+ crossPackageRef: "string",
1239
+ oneToMany: "any[]",
1240
+ manyToMany: "any[]",
1241
+ meta: "any",
1288
1242
  string: "string",
1289
1243
  number: "number",
1290
1244
  boolean: "boolean",