@happyvertical/smrt-core 0.55.10 → 0.55.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/AGENTS.md +2 -0
  2. package/agents/build-knowledge.md +40 -0
  3. package/agents/cookbook-engine.md +102 -0
  4. package/agents/cookbook.md +63 -0
  5. package/dist/consumer-plugin/artifact-publication.js +3 -3
  6. package/dist/consumer-plugin/artifact-publication.js.map +1 -1
  7. package/dist/consumer-plugin/hosted-sti-collections.d.ts +10 -0
  8. package/dist/consumer-plugin/hosted-sti-collections.d.ts.map +1 -0
  9. package/dist/consumer-plugin/hosted-sti-collections.js +92 -0
  10. package/dist/consumer-plugin/hosted-sti-collections.js.map +1 -0
  11. package/dist/consumer-plugin/index.d.ts +1 -1
  12. package/dist/consumer-plugin/index.d.ts.map +1 -1
  13. package/dist/consumer-plugin/index.js +76 -51
  14. package/dist/consumer-plugin/index.js.map +1 -1
  15. package/dist/cookbook/engine/commands.d.ts +37 -0
  16. package/dist/cookbook/engine/commands.d.ts.map +1 -0
  17. package/dist/cookbook/engine/commands.js +275 -0
  18. package/dist/cookbook/engine/commands.js.map +1 -0
  19. package/dist/cookbook/engine/defaults.d.ts +10 -0
  20. package/dist/cookbook/engine/defaults.d.ts.map +1 -0
  21. package/dist/cookbook/engine/defaults.js +49 -0
  22. package/dist/cookbook/engine/defaults.js.map +1 -0
  23. package/dist/cookbook/engine/document.d.ts +66 -0
  24. package/dist/cookbook/engine/document.d.ts.map +1 -0
  25. package/dist/cookbook/engine/document.js +357 -0
  26. package/dist/cookbook/engine/document.js.map +1 -0
  27. package/dist/cookbook/engine/engine.d.ts +49 -0
  28. package/dist/cookbook/engine/engine.d.ts.map +1 -0
  29. package/dist/cookbook/engine/engine.js +399 -0
  30. package/dist/cookbook/engine/engine.js.map +1 -0
  31. package/dist/cookbook/engine/index.d.ts +14 -0
  32. package/dist/cookbook/engine/index.d.ts.map +1 -0
  33. package/dist/cookbook/engine/index.js +8 -0
  34. package/dist/cookbook/engine/nav.d.ts +45 -0
  35. package/dist/cookbook/engine/nav.d.ts.map +1 -0
  36. package/dist/cookbook/engine/nav.js +191 -0
  37. package/dist/cookbook/engine/nav.js.map +1 -0
  38. package/dist/cookbook/engine/schemas.d.ts +29 -0
  39. package/dist/cookbook/engine/schemas.d.ts.map +1 -0
  40. package/dist/cookbook/engine/schemas.js +363 -0
  41. package/dist/cookbook/engine/schemas.js.map +1 -0
  42. package/dist/cookbook/engine/types.d.ts +331 -0
  43. package/dist/cookbook/engine/types.d.ts.map +1 -0
  44. package/dist/cookbook/engine/types.js +10 -0
  45. package/dist/cookbook/engine/types.js.map +1 -0
  46. package/dist/cookbook/engine.d.ts +2 -0
  47. package/dist/cookbook/engine.d.ts.map +1 -0
  48. package/dist/cookbook/engine.js +9 -0
  49. package/dist/cookbook/index.d.ts +6 -0
  50. package/dist/cookbook/index.d.ts.map +1 -0
  51. package/dist/cookbook/v1.schema.json +131 -0
  52. package/dist/cookbook/validate.d.ts +37 -0
  53. package/dist/cookbook/validate.d.ts.map +1 -0
  54. package/dist/cookbook/validate.js +172 -0
  55. package/dist/cookbook/validate.js.map +1 -0
  56. package/dist/cookbook.d.ts +7 -0
  57. package/dist/cookbook.d.ts.map +1 -0
  58. package/dist/cookbook.js +2 -0
  59. package/dist/manifest/static-manifest.js +1 -1
  60. package/dist/manifest/static-manifest.js.map +1 -1
  61. package/dist/manifest/store.js +1 -1
  62. package/dist/manifest.json +1 -1
  63. package/dist/prebuild/index.js +13 -13
  64. package/dist/prebuild/index.js.map +1 -1
  65. package/dist/smrt-knowledge.json +23 -7
  66. package/dist/vite-plugin/web-collections.d.ts +5 -0
  67. package/dist/vite-plugin/web-collections.d.ts.map +1 -1
  68. package/dist/vite-plugin/web-collections.js +1 -1
  69. package/dist/vite-plugin/web-collections.js.map +1 -1
  70. package/dist/vite-preset/cookbook-exposure.d.ts +11 -0
  71. package/dist/vite-preset/cookbook-exposure.d.ts.map +1 -0
  72. package/dist/vite-preset/cookbook-exposure.js +95 -0
  73. package/dist/vite-preset/cookbook-exposure.js.map +1 -0
  74. package/dist/vite-preset/index.d.ts +9 -0
  75. package/dist/vite-preset/index.d.ts.map +1 -1
  76. package/dist/vite-preset/index.js +17 -7
  77. package/dist/vite-preset/index.js.map +1 -1
  78. package/package.json +18 -6
@@ -2,7 +2,7 @@
2
2
  "version": "1.0.0",
3
3
  "timestamp": 0,
4
4
  "packageName": "@happyvertical/smrt-core",
5
- "packageVersion": "0.55.10",
5
+ "packageVersion": "0.55.11",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-core:SmrtClass": {
8
8
  "name": "smrtclass",
@@ -1,7 +1,7 @@
1
1
  import { orderedManifestObjectEntries, selectWebCollectionEntries } from "../vite-plugin/web-collections.js";
2
2
  import { renderApiClientCrudType, renderApiClientCustomMethodParameters, selectApiClientEntries } from "../vite-plugin/api-client-entries.js";
3
- import * as path from "node:path";
4
- import * as fs from "node:fs";
3
+ import * as path$1 from "node:path";
4
+ import * as fs$1 from "node:fs";
5
5
  //#region src/prebuild/index.ts
6
6
  /**
7
7
  * Pre-build utilities for generating TypeScript declarations
@@ -12,9 +12,9 @@ import * as fs from "node:fs";
12
12
  */
13
13
  async function generateDeclarations(options) {
14
14
  const { manifest: manifestInput, outDir, includeVirtualModules = true, includeObjectTypes = true, projectRoot = process.cwd() } = options;
15
- const manifest = typeof manifestInput === "string" ? JSON.parse(fs.readFileSync(manifestInput, "utf-8")) : manifestInput;
16
- const fullOutDir = path.isAbsolute(outDir) ? outDir : path.join(projectRoot, outDir);
17
- fs.mkdirSync(fullOutDir, { recursive: true });
15
+ const manifest = typeof manifestInput === "string" ? JSON.parse(fs$1.readFileSync(manifestInput, "utf-8")) : manifestInput;
16
+ const fullOutDir = path$1.isAbsolute(outDir) ? outDir : path$1.join(projectRoot, outDir);
17
+ fs$1.mkdirSync(fullOutDir, { recursive: true });
18
18
  console.log(`[smrt:prebuild] Generating declarations to ${fullOutDir}`);
19
19
  if (includeObjectTypes) await generateObjectTypeDeclarations(manifest, fullOutDir);
20
20
  if (includeVirtualModules) await generateVirtualModuleDeclarations(manifest, fullOutDir);
@@ -51,7 +51,7 @@ ${propertyLines.join("\n")}
51
51
 
52
52
  ${interfaces.join("\n\n")}
53
53
  `;
54
- fs.writeFileSync(path.join(outDir, "smrt-objects.d.ts"), objectTypesContent);
54
+ fs$1.writeFileSync(path$1.join(outDir, "smrt-objects.d.ts"), objectTypesContent);
55
55
  }
56
56
  /**
57
57
  * Generate virtual module declarations
@@ -326,12 +326,12 @@ ${selectWebCollectionEntries(manifest).map(({ collection, obj }) => ` ${JSON.
326
326
  export const manifestHash: string;
327
327
  export default collectionDefinitions;
328
328
  }`;
329
- fs.writeFileSync(path.join(outDir, "smrt-manifest.d.ts"), manifestDeclaration);
330
- fs.writeFileSync(path.join(outDir, "smrt-client.d.ts"), clientDeclaration);
331
- fs.writeFileSync(path.join(outDir, "smrt-routes.d.ts"), routesDeclaration);
332
- fs.writeFileSync(path.join(outDir, "smrt-mcp.d.ts"), mcpDeclaration);
333
- fs.writeFileSync(path.join(outDir, "smrt-types.d.ts"), typesDeclaration);
334
- fs.writeFileSync(path.join(outDir, "smrt-web.d.ts"), webDeclaration);
329
+ fs$1.writeFileSync(path$1.join(outDir, "smrt-manifest.d.ts"), manifestDeclaration);
330
+ fs$1.writeFileSync(path$1.join(outDir, "smrt-client.d.ts"), clientDeclaration);
331
+ fs$1.writeFileSync(path$1.join(outDir, "smrt-routes.d.ts"), routesDeclaration);
332
+ fs$1.writeFileSync(path$1.join(outDir, "smrt-mcp.d.ts"), mcpDeclaration);
333
+ fs$1.writeFileSync(path$1.join(outDir, "smrt-types.d.ts"), typesDeclaration);
334
+ fs$1.writeFileSync(path$1.join(outDir, "smrt-web.d.ts"), webDeclaration);
335
335
  }
336
336
  /**
337
337
  * Map SMRT field types to TypeScript types
@@ -358,7 +358,7 @@ async function generateDeclarationsFromCLI(args) {
358
358
  console.error("Usage: generate-declarations <manifest-path> [output-dir]");
359
359
  process.exit(1);
360
360
  }
361
- if (!fs.existsSync(manifestPath)) {
361
+ if (!fs$1.existsSync(manifestPath)) {
362
362
  console.error(`Manifest file not found: ${manifestPath}`);
363
363
  process.exit(1);
364
364
  }
@@ -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 {\n renderApiClientCrudType,\n renderApiClientCustomMethodParameters,\n selectApiClientEntries,\n} from '../vite-plugin/api-client-entries.js';\nimport {\n orderedManifestObjectEntries,\n selectWebCollectionEntries,\n} 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. Manifest key order\n // follows scan/discovery order, which is not stable across runs (#2749),\n // so sort deterministically before emitting.\n const sortedObjectEntries = orderedManifestObjectEntries(manifest);\n for (const [_objectName, objectMeta] of sortedObjectEntries) {\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, crudMethods, customMethods }) => {\n const overriddenMethods = customMethods.map(({ name }) => name);\n const crudType = renderApiClientCrudType(\n dataInterfaceName,\n crudMethods,\n overriddenMethods,\n );\n const customSignatures = customMethods\n .map(\n (method) =>\n ` ${method.name}(${renderApiClientCustomMethodParameters(\n method,\n mapFieldTypeToTypeScript,\n )}): Promise<any>;`,\n )\n .join('\\n');\n\n if (customSignatures) {\n const intersection = crudType ? `${crudType} & ` : '';\n return ` ${JSON.stringify(clientKey)}: ${intersection}{\\n${customSignatures}\\n };`;\n }\n\n return ` ${JSON.stringify(clientKey)}: ${crudType ?? 'Record<string, never>'};`;\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 physical declarations share client\n // entry selection and method-signature rendering with the Vite declaration.\n // 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 /** Structured custom-action failure nested under an API error body's \\`error\\` key. */\n export interface SmrtClientFailure {\n ok: false;\n code: string;\n message: string;\n status?: number;\n }\n\n /** Shape of a JSON error body carried by a rejected request (SmrtClientError.body). */\n export interface ApiError {\n error?: string | SmrtClientFailure;\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 /** Machine-readable code from a structured custom-action failure, when present. */\n code?: string;\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. Sorted for the\n // same reason as generateObjectTypeDeclarations (#2749).\n const objectImports = orderedManifestObjectEntries(manifest)\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 ` ${JSON.stringify(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 type SmrtWebFieldType =\n | 'text'\n | 'decimal'\n | 'boolean'\n | 'integer'\n | 'datetime'\n | 'json'\n | 'foreignKey'\n | 'crossPackageRef';\n\n /** Static \\`@field({ ui })\\` hints (#2046) — the field-policy rail seed. */\n export interface SmrtWebFieldUIHints {\n basic?: boolean;\n group?: string;\n order?: number;\n locked?: boolean;\n widget?: 'textarea' | 'currency' | 'email' | 'url' | 'phone';\n }\n\n export interface SmrtWebFieldDefinition {\n type: SmrtWebFieldType;\n required?: boolean;\n nullable?: boolean;\n default?: unknown;\n /** Developer-authored \\`@field({ description })\\` (#2046) — end-user help seed. */\n description?: string;\n /** Static \\`@field({ ui })\\` hints (#2046). */\n ui?: SmrtWebFieldUIHints;\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 export interface WebToolRouteDescriptor {\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n scope: 'item' | 'collection';\n path: string[];\n parameterAliases?: Record<string, string>;\n optionsBag?: boolean;\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 effect: 'read' | 'write' | 'destructive';\n idempotent: boolean;\n openWorld: boolean;\n route?: WebToolRouteDescriptor;\n }\n\n /** Canonical data-only definition for one generated browser tool. */\n export interface WebMcpToolDefinition extends WebToolDescriptor {\n collection: string;\n objectRef: string;\n className: string;\n endpoint: string;\n idField: string;\n idType: 'uuid' | 'text';\n route: WebToolRouteDescriptor;\n relationships: SmrtWebRelationship[];\n }\n\n export interface SmrtWebCollectionDefinition<TData = Record<string, unknown>> {\n name: string;\n /** Canonical qualified model identity (\\`@package/name:ClassName\\`) for policy APIs. */\n objectRef: 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 /** Every API-backed WebMCP tool, independent of list materialization. */\n export const webMcpToolDefinitions: readonly WebMcpToolDefinition[];\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
+ {"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 {\n renderApiClientCrudType,\n renderApiClientCustomMethodParameters,\n selectApiClientEntries,\n} from '../vite-plugin/api-client-entries.js';\nimport {\n orderedManifestObjectEntries,\n selectWebCollectionEntries,\n} 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. Manifest key order\n // follows scan/discovery order, which is not stable across runs (#2749),\n // so sort deterministically before emitting.\n const sortedObjectEntries = orderedManifestObjectEntries(manifest);\n for (const [_objectName, objectMeta] of sortedObjectEntries) {\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, crudMethods, customMethods }) => {\n const overriddenMethods = customMethods.map(({ name }) => name);\n const crudType = renderApiClientCrudType(\n dataInterfaceName,\n crudMethods,\n overriddenMethods,\n );\n const customSignatures = customMethods\n .map(\n (method) =>\n ` ${method.name}(${renderApiClientCustomMethodParameters(\n method,\n mapFieldTypeToTypeScript,\n )}): Promise<any>;`,\n )\n .join('\\n');\n\n if (customSignatures) {\n const intersection = crudType ? `${crudType} & ` : '';\n return ` ${JSON.stringify(clientKey)}: ${intersection}{\\n${customSignatures}\\n };`;\n }\n\n return ` ${JSON.stringify(clientKey)}: ${crudType ?? 'Record<string, never>'};`;\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 physical declarations share client\n // entry selection and method-signature rendering with the Vite declaration.\n // 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 /** Structured custom-action failure nested under an API error body's \\`error\\` key. */\n export interface SmrtClientFailure {\n ok: false;\n code: string;\n message: string;\n status?: number;\n }\n\n /** Shape of a JSON error body carried by a rejected request (SmrtClientError.body). */\n export interface ApiError {\n error?: string | SmrtClientFailure;\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 /** Machine-readable code from a structured custom-action failure, when present. */\n code?: string;\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. Sorted for the\n // same reason as generateObjectTypeDeclarations (#2749).\n const objectImports = orderedManifestObjectEntries(manifest)\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 ` ${JSON.stringify(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 type SmrtWebFieldType =\n | 'text'\n | 'decimal'\n | 'boolean'\n | 'integer'\n | 'datetime'\n | 'json'\n | 'foreignKey'\n | 'crossPackageRef';\n\n /** Static \\`@field({ ui })\\` hints (#2046) — the field-policy rail seed. */\n export interface SmrtWebFieldUIHints {\n basic?: boolean;\n group?: string;\n order?: number;\n locked?: boolean;\n widget?: 'textarea' | 'currency' | 'email' | 'url' | 'phone';\n }\n\n export interface SmrtWebFieldDefinition {\n type: SmrtWebFieldType;\n required?: boolean;\n nullable?: boolean;\n default?: unknown;\n /** Developer-authored \\`@field({ description })\\` (#2046) — end-user help seed. */\n description?: string;\n /** Static \\`@field({ ui })\\` hints (#2046). */\n ui?: SmrtWebFieldUIHints;\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 export interface WebToolRouteDescriptor {\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n scope: 'item' | 'collection';\n path: string[];\n parameterAliases?: Record<string, string>;\n optionsBag?: boolean;\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 effect: 'read' | 'write' | 'destructive';\n idempotent: boolean;\n openWorld: boolean;\n route?: WebToolRouteDescriptor;\n }\n\n /** Canonical data-only definition for one generated browser tool. */\n export interface WebMcpToolDefinition extends WebToolDescriptor {\n collection: string;\n objectRef: string;\n className: string;\n endpoint: string;\n idField: string;\n idType: 'uuid' | 'text';\n route: WebToolRouteDescriptor;\n relationships: SmrtWebRelationship[];\n }\n\n export interface SmrtWebCollectionDefinition<TData = Record<string, unknown>> {\n name: string;\n /** Canonical qualified model identity (\\`@package/name:ClassName\\`) for policy APIs. */\n objectRef: 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 /** Every API-backed WebMCP tool, independent of list materialization. */\n export const webMcpToolDefinitions: readonly WebMcpToolDefinition[];\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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"}
@@ -3,13 +3,13 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-core",
6
- "packageVersion": "0.55.10",
6
+ "packageVersion": "0.55.11",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "e3ba6f4d7487aaeb9724fd93d8942c1dcc15511541e0d4aa2bd25865abe6a11c",
11
- "packageJson": "4453c9997591761fca4cba9d0868820c2b1033a411b6cd74b1ac9ba1e816e045",
12
- "agents": "0a94ccfbd3b7b48e0e88388ba0385f386f3e9b2816d97adf84097fd06bb488de",
10
+ "manifest": "4f7e9d98bc070c76d2a2ed6bf5c568ac5f369cdd0ba03ef80e6c11e7279c542a",
11
+ "packageJson": "1aa61ee3993c8c69dddf5fe304fd718d342c0b04332dd8d26c344a24196d99a3",
12
+ "agents": "a55cc2d7dccf8f5dc9fd515361fddf719466159ee11e265285cf14b3fcdd96fa",
13
13
  "moduleDoc:agents/object-runtime.md": "652352cdf1eddd0dcc91f6589b8241d9d99581f54bcc2318520a3ff97cd22cf5",
14
14
  "moduleDoc:agents/revision-guard.md": "f6ea32a6860964c98aba14b93587f40b33c8f7d0deccb2c54d21bff45db3da42",
15
15
  "moduleDoc:agents/collection-reads.md": "e5bb79b59c17e79a85a71580a019e6b0b641e498ac4d850073fd5ad634b57cb7",
@@ -19,19 +19,24 @@
19
19
  "moduleDoc:agents/change-feed.md": "c3d6206758dd1d2e0475177e1938fb7d7b6cfe8a6c4d3c3b56ef62bc7bc7739d",
20
20
  "moduleDoc:agents/change-signals.md": "a2dbe7077483b5a19bd5ea9589b5a08fe847377c8e05d01d57f813c6d9cbc28b",
21
21
  "moduleDoc:agents/generators.md": "55244a454c2ab8937143600eb785f069d949d75fc4da8bfdf78fb801a59facc8",
22
- "moduleDoc:agents/build-knowledge.md": "695cdcb80eb82347db9203493293da62da44c4dc8cacb489a09be64bc976fb79",
22
+ "moduleDoc:agents/build-knowledge.md": "d123541604372fd6fa7f0ade665a19b066d2e9c572e09d39c3cb4849211d09ef",
23
23
  "moduleDoc:agents/memory.md": "a86bdf370403d08ecad3be86e341eb322dfe6e6b7a4e8806a6f8be9bf3421da5",
24
24
  "moduleDoc:agents/system-diagnostics.md": "b7ccae18ee312bf788d5470f95fa78d3535f1789247d5372e40ca86538290772",
25
25
  "moduleDoc:agents/registry-snapshot.md": "6234aa540427396d1dda1825a4641b1072e247187d5ddf413ae46c691344ee30",
26
26
  "moduleDoc:agents/model-moves.md": "c64147fee1b7ec242fd1c178e7aea7cc90cafc704dee2738141c7a6d61403cd8",
27
27
  "moduleDoc:agents/run-once.md": "0b858c18c78eca5050257a2af06f16de99327e05e0bf9fee9e6277242e7707ef",
28
28
  "moduleDoc:agents/recipes.md": "50ddedb14583e073c4af9c15142f4838c4326c1955931f743148e41e15dbdf38",
29
+ "moduleDoc:agents/cookbook.md": "c77173a6386969f777d3c6d921172e19bd887434e61e04cf0a112118a647b5b7",
30
+ "moduleDoc:agents/cookbook-engine.md": "d4943b15e7b90a133b28a4c86b498c16eb623cd78b5022585873276cb1a5a5ab",
29
31
  "moduleDoc:agents/browser-entry.md": "04d26eff8a65f02e88b20cbb77b1f94f58e29b62178ac6fbec129ee436a732eb",
30
32
  "moduleDoc:agents/test-types.md": "37c009ae2bd21001d83bba6cfab2047465fe32d5443de12661e24ba7d4e2d613"
31
33
  },
32
34
  "exports": [
33
35
  ".",
34
36
  "./consumer-plugin",
37
+ "./cookbook",
38
+ "./cookbook/engine",
39
+ "./cookbook/v1.schema.json",
35
40
  "./filesystem",
36
41
  "./generators",
37
42
  "./generators/custom-action",
@@ -88,6 +93,7 @@
88
93
  "@types/node": "24.13.6",
89
94
  "@types/pluralize": "^0.0.33",
90
95
  "@xenova/transformers": "^2.17.2",
96
+ "ajv": "8.20.0",
91
97
  "svelte": "^5.57.1",
92
98
  "svelte-check": "^4.7.6",
93
99
  "typescript": "6.0.3",
@@ -1130,7 +1136,7 @@
1130
1136
  "polymorphicAssociations": 1,
1131
1137
  "uuidColumns": 3
1132
1138
  },
1133
- "agentDoc": "# @happyvertical/smrt-core\n\nFoundation ORM, registry, schema/code generation, AI integration, and DispatchBus.\nRead the module for the subsystem being edited; root AGENTS covers shared model\nand repository rules.\n\n## Modules\n\n| Source | Scope | Module doc |\n|---|---|---|\n| `src/object.ts`, `src/collection.ts`, `src/child-accessors.ts` | Lifecycle, hydration, operators, STI, child accessors, dispatch | [agents/object-runtime.md](agents/object-runtime.md) |\n| `src/revision-guard.ts`, `src/unchanged-indexed.ts`, `src/native-duckdb.ts` | Guarded writes, PostgreSQL revision precision, unchanged indexed columns on referenced DuckDB rows (#3737) | [agents/revision-guard.md](agents/revision-guard.md) |\n| `src/collection.ts` | Projections, latest-related, facets, counts, read plans | [agents/collection-reads.md](agents/collection-reads.md) |\n| `src/collection.ts` | Limits, sort whitelist, generated list order | [agents/query-bounds.md](agents/query-bounds.md) |\n| `src/data-query.ts` | Transport-neutral bounded query normalization | [agents/data-query.md](agents/data-query.md) |\n| `src/schema/`, `src/migrations/`, `src/cascade.ts`, `src/system/` | DDL parity, indexes, migrations, delete integrity, retention | [agents/schema-paths.md](agents/schema-paths.md) |\n| `src/postgres-permissions.ts` | Explicit PostgreSQL ACL plans and atomic reconciliation | [deployment permission contract](../../docs/content/postgres-permissions.md) |\n| `src/change-feed.ts` | Durable changes, cursors, table versions, retention | [agents/change-feed.md](agents/change-feed.md) |\n| `src/change-feed-authz.ts` | Consumer table/row change-feed authorization hooks (#3020) | [agents/change-feed.md](agents/change-feed.md) |\n| `src/change-signals.ts` | Signal bus, replica fan-out, SSE | [agents/change-signals.md](agents/change-signals.md) |\n| `src/generators/`, `src/vite-plugin/web-collections.ts` | REST/CLI/MCP generation, manifest hashes, ETags | [agents/generators.md](agents/generators.md) |\n| `src/vite-plugin/`, `src/consumer-plugin/`, `src/knowledge.ts` | Decorator UI hints, knowledge projection, generation snapshots | [agents/build-knowledge.md](agents/build-knowledge.md) |\n| `src/knowledge-graph.ts` | Cross-package knowledge graph merge and freshness check (#2863) — see [standards](../../docs/content/standards.md) | — |\n| `src/object.ts`, `src/collection.ts`, `src/learning/memory.ts` | Context memory and semantic search | [agents/memory.md](agents/memory.md) |\n| `src/system/diagnostics.ts` | SELECT-only `_smrt_*` diagnostics reader behind smrt-dev-mcp runtime tools (#1824) | [agents/system-diagnostics.md](agents/system-diagnostics.md) |\n| `src/system/registry-snapshot.ts` | Sanitized plain-JSON projection of the booted `ObjectRegistry` for the smrt-dev-mcp runtime dev-plane (#1831); never constructors, validators, values, or absolute paths | [agents/registry-snapshot.md](agents/registry-snapshot.md) |\n| `src/registry/qualified-name-aliases.ts`, `src/migrations/qualified-name-aliases.ts` | Model moves: `previousQualifiedNames` aliases, deprecation, doctor counts, opt-in backfill (#3338) | [agents/model-moves.md](agents/model-moves.md) |\n| `src/run-once.ts` | Shared idempotency seam: insert-only claim + caller work in one transaction, replay on retry, typed in-flight/unknown-outcome answers (#3080) | [agents/run-once.md](agents/run-once.md) |\n| `src/recipe.ts`, `../scanner/src/recipes.ts` | `SmrtRecipe`: declared user-facing units (models, nav, `requires`, curation `options`) collected into manifest and knowledge `recipes` (#3590) | [agents/recipes.md](agents/recipes.md) |\n| `src/browser.ts`, `src/host.ts`, `src/host.browser.ts` | Browser entry that builds and evaluates without Node-only modules: host seam, `package.json#browser` swaps, `pnpm test:browser` (#2838) | [agents/browser-entry.md](agents/browser-entry.md) |\n\n## Cross-module invariants\n\n- `ObjectRegistry` is a `globalThis` singleton so registration survives HMR.\n- Any output that names a database URL (CLI banners, logs, errors) renders it\n with `redactDatabaseUrl` / `redactDatabaseUrlsInText`\n (`src/utils/database-url.ts`, #3527): never the password or query string,\n and never the raw string when it cannot be parsed.\n- Per-class registry work on the hydration path (manifest reconciliation in\n `ensureManifestLoaded()`, the simple-name index behind `findClass()`) is\n memoized per registry generation (`src/registry/generation.ts`, #3047). Any\n new writer of registered classes, their field/method metadata, or loaded\n manifests must go through the tracked maps or call\n `bumpRegistryGeneration()`; `issue-3047-hydration-registry-work.test.ts`\n guards that hydration does not repeat this work per row.\n\n- Production DDL uses manifest generators; `getTestDatabase()` uses registry\n generators. Keep columns, indexes, FK actions, and runtime conflict targets in\n parity (`src/schema/schema-path-parity.test.ts`). Every new query predicate\n needs its index or an explicit reason none is needed.\n- Tenant scoping covers every read and every unique/conflict key. Explicit\n conflict columns are not rewritten; their author must include tenant scope.\n A `tenantId` field makes rows tenant-OWNED without any tenancy declaration:\n the default key leads with `tenant_id`. A new object's natural-key save reads\n the row its upsert would hit and refuses a different owner\n (`TENANT_ISOLATION_VIOLATION`) or adopts a same-owner row's id, so\n `DO UPDATE` never rewrites a primary key or owner. Only an explicit natural\n key adopts: a slug `getSlug()` derived from `name`/`title`/`label` moves to\n a free `-2`, `-3`, … slug and INSERTs. See `agents/schema-paths.md`\n (\"Tenant OWNERSHIP\").\n- Persisted saves use `id` and loaded `updated_at`; new saves use natural keys.\n Preserve revision compare-and-swap ordering through public `save()`,\n `claimRevision()`, and transaction APIs. Embedded saves, deletes, and complete\n `withTransaction()` callbacks share a process-local write queue.\n- Collection model hydration is serial in result order: initialization may query\n the same transaction-bound PostgreSQL client. Use projections for plain rows.\n- Native DuckDB UUIDs must be cast coherently on read before identity reuse;\n custom embedded revision paths use `getCanonicalPersistedRow()`. Never replay\n a mutation to discover result types.\n- `withDatabase(db, callback)` restores only database bindings (including public\n `options.db`); `withTransaction(callback)` also restores identity/revision\n metadata after rollback. Do not use a bound instance concurrently.\n- `runOnce({ db, tenantId, actor, token, content }, work)` (`src/run-once.ts`,\n #3080) claims a key derived from all four inputs — never the token alone,\n which collapses distinct submissions — via `INSERT ... ON CONFLICT\n (claim_key) DO NOTHING RETURNING claim_key` into the hand-DDL\n `_smrt_run_once_claims` system table, and runs `work` in the SAME\n transaction; a completed claim replays its stored result instead of\n re-running, a failed `work` rolls the claim back with the transaction, and\n an unresolved claim raises typed `RunOnceClaimError`\n (`RUN_ONCE_IN_FLIGHT` / `RUN_ONCE_OUTCOME_UNKNOWN`) rather than guessing.\n The claim insert must never raise on conflict: PostgreSQL aborts the whole\n transaction on any raised error, which would take the recovery `SELECT`\n down with it too — this is why it is `DO NOTHING`, not a plain `INSERT`\n caught for `isUniqueViolationError`. Completed claims are bounded by\n `runRetentionSweep()`'s `run-once-claims` task (`system/retention.ts`,\n `DEFAULT_RETENTION_POLICY.runOnceClaims`, default 30 days on\n `completed_at`; `in_progress` rows are never matched) — the window is a\n replay deadline, not just disk hygiene: a token replayed after its claim\n is swept runs `work()` again. See `agents/run-once.md`.\n- `ensureSystemTables(db, typeHint?)` provisions framework tables idempotently;\n call it on a base PostgreSQL connection before caller-owned transactions.\n Bootstrap uses a transaction-scoped advisory lock, taken only when the\n PostgreSQL schema version is not yet stamped: inside a caller's transaction it\n is held until that transaction ends (#2868). Application tables still require migrations;\n runtime table verification checks existence only.\n- Manifest generation fails closed on scanner errors, including unresolved\n decorator spreads. Never emit partial, default-open registration.\n- Generated registration repairs bundled class identity using the exact imported\n constructor, explicit package, and isolated one-object manifest. Never infer\n ownership from paths, simple names, or table names; packages can share names.\n Consumer regression gate: `packages/bundle-gate/src/__tests__/registry-identity.spec.ts`.\n- Decorator registration trusts a class's stack-derived package only when that\n package declares the class (stub, own manifest entry, or explicit\n `packageName`); it then matches the qualified key and never adopts another\n package's same-named registration. Otherwise a loaded (or the cwd project's)\n manifest entry describing the declaring file supplies the identity — apps\n scan workspace-package sources under the app's name (#3110). A class of\n another known package coexists under its qualified key\n (`decorator-different-packages-qualified-coexist`); bundled output needs\n a resolved table that differs first (#3106). Stack\n attribution skips smrt-core (including source-mapped installed\n `smrt-core/src` frames, #3109), decorator-helper, and module-runner frames,\n and drops a frame's Vite `?v=` query (#3490). Library builds stamp each\n decorated class with `static __smrtPackage__` (own property, read at\n decoration as its explicit package) and publish `stampsConstructors`; an\n unstamped class never takes a stamping package's entry by name and table,\n in either order (#3490, `issue-3490-*.test.ts`, `package-stamp.test.ts`).\n Qualified names are `@scope/pkg:Class`; the scope is a deliberate\n discriminator, so manifest generation (`generateManifest`,\n `applyGenerationPasses`, hence `pnpm build`) throws\n `CONFIG_UNSCOPED_PACKAGE_NAME` for an unscoped `package.json` name (#3408).\n Instance and collection registry lookups pass the qualified name. The schema\n planner rejects a table claimed by two unrelated classes\n (`CONFIG_TABLE_NAME_COLLISION`): only one STI family shares a table (#3098);\n collections (including `SmrtReportCollection`) never claim one. Consumer\n gates: `registry/__tests__/issue-3110-*.test.ts`, `issue-3109-*.test.ts`,\n `packages/bundle-gate/src/__tests__/consumer-same-name.spec.ts`.\n- API custom-action eligibility has ONE resolver, `resolveApiMethodExposure()`\n in `generators/custom-action.ts`: both SvelteKit route emitters,\n `resolveApiActionSet`, and `knowledge.ts` read it, and a new consumer must too\n — a local mirror is how a method gets reported unavailable while its route\n file is still written. A public method routes by default only when every\n parameter is JSON-shaped; `@method({ expose })` overrides in both directions,\n `expose: true` bypasses the heuristic ALONE, and an explicit `api.include` or\n `api.routes` entry keeps its pre-#2686 route. Fail closed on scanner\n uncertainty: read `parameters[].typeUnresolved`, never the `'any'` it\n substitutes. Accepting `Date` as wire-able and hydrating it\n (`toCustomActionDate`) are one decision — changing either breaks the other.\n The runtime `APIGenerator` transport stays declaration-gated: `rest.ts`\n dispatch and `preflight-route.ts` prediction both read\n `declaresRuntimeRestRoute()`, which must accept every `ApiCustomRouteConfig`\n option so a sweep moving one onto its method cannot delete the endpoint. Its\n twin `declaresRuntimeRestRouteShape()` deliberately ignores `expose: false`:\n the dispatcher must still SEE a withheld declaration to answer 404, because\n `POST /<collection>/<segment>` resolves to `create` when nothing claims the\n segment. Split unions and type arguments with `splitTopLevel()` — a naive\n `split('|')` truncates `Record<string, Asset | null>` into fragments that\n match no rule and are then accepted, widening the gate. `extractTypeName`\n returns `null` for a generic with an unresolvable ARGUMENT or a union with an\n unresolvable BRANCH so those reach that fail-closed path instead of arriving\n as a bare `'Array'`. Runtime transports read\n `ObjectRegistry.resolveRuntimeMethod()`, which tries the item class's manifest\n entry, then the COLLECTION class's (where a collection-hosted action's\n parameters live), then the live `@method()` store — keyed by CONSTRUCTOR,\n never by simple name (`Account` exists in two packages), and recording\n `isStatic` because an unscanned runtime has no manifest to recover the\n receiver from. `isRestActionRoutable` ANDs \"declared\" with that receiver, so\n it never predicts `allow` for an action dispatch refuses. A declared action\n with no receiver is refused (501 collection-scoped, 404 item-scoped), never\n allowed to fall through into `create`. All eight consumers read the resolver,\n including `packages/smrt-workbench/src/discovery.ts`; it imports the\n `./generators/custom-action` LEAF subpath, not `./generators`, because that\n barrel value-re-exports `MCPGenerator` and so drags `@happyvertical/ai` and\n `@happyvertical/sql` into a build-time helper (2 modules vs 109). Keep\n `custom-action.ts` free of value imports outside `tools/tool-generator`.\n\n## Gotchas\n\n- Optional filesystem support stays lazy: use `createFilesystemAdapter()` in\n `src/filesystem-loader.ts`, not a static files-SDK import. Fully bundled apps\n import `@happyvertical/smrt-core/filesystem` at startup. Use\n `importOptionalDependency()` for similarly heavy optional dependencies.\n- `src/browser.ts` is the `browser`-condition entry of the package root: the\n Node entry (`index.ts`) minus HTTP servers, manifest/knowledge discovery, run-once,\n the Vite plugin, and the test helper. A new export added to `index.ts` must also\n go to `browser.ts` unless it is Node-only; `src/__tests__/browser-entry-parity.test.ts`\n fails otherwise (allowlist: `NODE_ONLY_GROUPS` in that test, each with a reason; the browser\n entry may not publish a name the Node entry lacks), and also\n fails when a workspace package imports a root value the browser entry lacks.\n- `exports['.'].browser` (`dist/browser.js`) is the browser surface. Model\n package roots are bundled for browsers by the bundle-gate browser fixture\n (#3621): anything a model root imports from core must be exported by the\n browser entry and must not reach `node:` built-ins, `pg`, `cosmiconfig`, or\n `jiti`. Keep Node-only code behind a subpath or a lazy import; when a fix\n makes a package pass, delete its entry in\n `packages/bundle-gate/src/browser-gate/expected-failures.ts`.\n- Database retries are transient-only, four attempts total for `get`/`upsert`.\n Use `src/db-errors.ts` classifiers through the cause chain, never message\n matching (SDK driver text can live in `context.originalError`). Constraints,\n bad input, missing tables, and aborted PostgreSQL transactions fail immediately.\n Unique/PK violations become `VALIDATION_UNIQUE_CONSTRAINT`, NOT NULL becomes\n `VALIDATION_REQUIRED_FIELD`; other failures keep the driver error as `cause`.\n- Property initializers precede option values; options win. Arrays/objects are\n shallow-cloned. Collection creation caches fields; table verification caches\n by DB URL and table. Preserve these scopes when changing initialization.\n- The Vite plugin loads scanner/schema code from `dist/` when present. Rebuild\n core after editing those sources before testing consumer manifest generation.\n Vite 8 requires `oxc.decorator: { legacy: true, emitDecoratorMetadata: true }`.\n\n## Validation\n\n`typecheck` checks production source first, then the test project in\n`tsconfig.test.json`. Test coverage and the shared relaxed config\ncontract are documented in [agents/test-types.md](agents/test-types.md).\n\nRun focused tests first, then applicable package checks:\n\n```bash\npnpm --filter @happyvertical/smrt-core test\npnpm --filter @happyvertical/smrt-core typecheck\npnpm --filter @happyvertical/smrt-core build\npnpm --filter @happyvertical/smrt-core test:postgres\npnpm check:agents-chain\npnpm smrt dev:knowledge-check\n```\n\nThe PostgreSQL lane is required for numeric types, UUID casts, conflict targets,\ntimestamps, and migrations. Tests generate their manifest before Vitest; restart\nwatch mode after adding decorated classes. Documentation-only changes need\ninstruction-chain and knowledge freshness checks, not the runtime test suite.\n",
1139
+ "agentDoc": "# @happyvertical/smrt-core\n\nFoundation ORM, registry, schema/code generation, AI integration, and DispatchBus.\nRead the module for the subsystem being edited; root AGENTS covers shared model\nand repository rules.\n\n## Modules\n\n| Source | Scope | Module doc |\n|---|---|---|\n| `src/object.ts`, `src/collection.ts`, `src/child-accessors.ts` | Lifecycle, hydration, operators, STI, child accessors, dispatch | [agents/object-runtime.md](agents/object-runtime.md) |\n| `src/revision-guard.ts`, `src/unchanged-indexed.ts`, `src/native-duckdb.ts` | Guarded writes, PostgreSQL revision precision, unchanged indexed columns on referenced DuckDB rows (#3737) | [agents/revision-guard.md](agents/revision-guard.md) |\n| `src/collection.ts` | Projections, latest-related, facets, counts, read plans | [agents/collection-reads.md](agents/collection-reads.md) |\n| `src/collection.ts` | Limits, sort whitelist, generated list order | [agents/query-bounds.md](agents/query-bounds.md) |\n| `src/data-query.ts` | Transport-neutral bounded query normalization | [agents/data-query.md](agents/data-query.md) |\n| `src/schema/`, `src/migrations/`, `src/cascade.ts`, `src/system/` | DDL parity, indexes, migrations, delete integrity, retention | [agents/schema-paths.md](agents/schema-paths.md) |\n| `src/postgres-permissions.ts` | Explicit PostgreSQL ACL plans and atomic reconciliation | [deployment permission contract](../../docs/content/postgres-permissions.md) |\n| `src/change-feed.ts` | Durable changes, cursors, table versions, retention | [agents/change-feed.md](agents/change-feed.md) |\n| `src/change-feed-authz.ts` | Consumer table/row change-feed authorization hooks (#3020) | [agents/change-feed.md](agents/change-feed.md) |\n| `src/change-signals.ts` | Signal bus, replica fan-out, SSE | [agents/change-signals.md](agents/change-signals.md) |\n| `src/generators/`, `src/vite-plugin/web-collections.ts` | REST/CLI/MCP generation, manifest hashes, ETags | [agents/generators.md](agents/generators.md) |\n| `src/vite-plugin/`, `src/consumer-plugin/`, `src/knowledge.ts` | Decorator UI hints, knowledge projection, generation snapshots | [agents/build-knowledge.md](agents/build-knowledge.md) |\n| `src/knowledge-graph.ts` | Cross-package knowledge graph merge and freshness check (#2863) — see [standards](../../docs/content/standards.md) | — |\n| `src/object.ts`, `src/collection.ts`, `src/learning/memory.ts` | Context memory and semantic search | [agents/memory.md](agents/memory.md) |\n| `src/system/diagnostics.ts` | SELECT-only `_smrt_*` diagnostics reader behind smrt-dev-mcp runtime tools (#1824) | [agents/system-diagnostics.md](agents/system-diagnostics.md) |\n| `src/system/registry-snapshot.ts` | Sanitized plain-JSON projection of the booted `ObjectRegistry` for the smrt-dev-mcp runtime dev-plane (#1831); never constructors, validators, values, or absolute paths | [agents/registry-snapshot.md](agents/registry-snapshot.md) |\n| `src/registry/qualified-name-aliases.ts`, `src/migrations/qualified-name-aliases.ts` | Model moves: `previousQualifiedNames` aliases, deprecation, doctor counts, opt-in backfill (#3338) | [agents/model-moves.md](agents/model-moves.md) |\n| `src/run-once.ts` | Shared idempotency seam: insert-only claim + caller work in one transaction, replay on retry, typed in-flight/unknown-outcome answers (#3080) | [agents/run-once.md](agents/run-once.md) |\n| `src/recipe.ts`, `../scanner/src/recipes.ts` | `SmrtRecipe`: declared user-facing units (models, nav, `requires`, curation `options`) collected into manifest and knowledge `recipes` (#3590) | [agents/recipes.md](agents/recipes.md) |\n| `src/cookbook/` | Cookbook v1 document: browser-safe validate/migrate, JSON Schema (`./cookbook`, `./cookbook/v1.schema.json`; #3748) | [agents/cookbook.md](agents/cookbook.md) |\n| `src/cookbook/engine/` | Headless cookbook command engine: typed commands, idempotent retries, atomic batches, receipts, tool schemas (`./cookbook/engine`; #3753) | [agents/cookbook-engine.md](agents/cookbook-engine.md) |\n| `src/browser.ts`, `src/host.ts`, `src/host.browser.ts` | Browser entry that builds and evaluates without Node-only modules: host seam, `package.json#browser` swaps, `pnpm test:browser` (#2838) | [agents/browser-entry.md](agents/browser-entry.md) |\n\n## Cross-module invariants\n\n- `ObjectRegistry` is a `globalThis` singleton so registration survives HMR.\n- Any output that names a database URL (CLI banners, logs, errors) renders it\n with `redactDatabaseUrl` / `redactDatabaseUrlsInText`\n (`src/utils/database-url.ts`, #3527): never the password or query string,\n and never the raw string when it cannot be parsed.\n- Per-class registry work on the hydration path (manifest reconciliation in\n `ensureManifestLoaded()`, the simple-name index behind `findClass()`) is\n memoized per registry generation (`src/registry/generation.ts`, #3047). Any\n new writer of registered classes, their field/method metadata, or loaded\n manifests must go through the tracked maps or call\n `bumpRegistryGeneration()`; `issue-3047-hydration-registry-work.test.ts`\n guards that hydration does not repeat this work per row.\n\n- Production DDL uses manifest generators; `getTestDatabase()` uses registry\n generators. Keep columns, indexes, FK actions, and runtime conflict targets in\n parity (`src/schema/schema-path-parity.test.ts`). Every new query predicate\n needs its index or an explicit reason none is needed.\n- Tenant scoping covers every read and every unique/conflict key. Explicit\n conflict columns are not rewritten; their author must include tenant scope.\n A `tenantId` field makes rows tenant-OWNED without any tenancy declaration:\n the default key leads with `tenant_id`. A new object's natural-key save reads\n the row its upsert would hit and refuses a different owner\n (`TENANT_ISOLATION_VIOLATION`) or adopts a same-owner row's id, so\n `DO UPDATE` never rewrites a primary key or owner. Only an explicit natural\n key adopts: a slug `getSlug()` derived from `name`/`title`/`label` moves to\n a free `-2`, `-3`, … slug and INSERTs. See `agents/schema-paths.md`\n (\"Tenant OWNERSHIP\").\n- Persisted saves use `id` and loaded `updated_at`; new saves use natural keys.\n Preserve revision compare-and-swap ordering through public `save()`,\n `claimRevision()`, and transaction APIs. Embedded saves, deletes, and complete\n `withTransaction()` callbacks share a process-local write queue.\n- Collection model hydration is serial in result order: initialization may query\n the same transaction-bound PostgreSQL client. Use projections for plain rows.\n- Native DuckDB UUIDs must be cast coherently on read before identity reuse;\n custom embedded revision paths use `getCanonicalPersistedRow()`. Never replay\n a mutation to discover result types.\n- `withDatabase(db, callback)` restores only database bindings (including public\n `options.db`); `withTransaction(callback)` also restores identity/revision\n metadata after rollback. Do not use a bound instance concurrently.\n- `runOnce({ db, tenantId, actor, token, content }, work)` (`src/run-once.ts`,\n #3080) claims a key derived from all four inputs — never the token alone,\n which collapses distinct submissions — via `INSERT ... ON CONFLICT\n (claim_key) DO NOTHING RETURNING claim_key` into the hand-DDL\n `_smrt_run_once_claims` system table, and runs `work` in the SAME\n transaction; a completed claim replays its stored result instead of\n re-running, a failed `work` rolls the claim back with the transaction, and\n an unresolved claim raises typed `RunOnceClaimError`\n (`RUN_ONCE_IN_FLIGHT` / `RUN_ONCE_OUTCOME_UNKNOWN`) rather than guessing.\n The claim insert must never raise on conflict: PostgreSQL aborts the whole\n transaction on any raised error, which would take the recovery `SELECT`\n down with it too — this is why it is `DO NOTHING`, not a plain `INSERT`\n caught for `isUniqueViolationError`. Completed claims are bounded by\n `runRetentionSweep()`'s `run-once-claims` task (`system/retention.ts`,\n `DEFAULT_RETENTION_POLICY.runOnceClaims`, default 30 days on\n `completed_at`; `in_progress` rows are never matched) — the window is a\n replay deadline, not just disk hygiene: a token replayed after its claim\n is swept runs `work()` again. See `agents/run-once.md`.\n- `ensureSystemTables(db, typeHint?)` provisions framework tables idempotently;\n call it on a base PostgreSQL connection before caller-owned transactions.\n Bootstrap uses a transaction-scoped advisory lock, taken only when the\n PostgreSQL schema version is not yet stamped: inside a caller's transaction it\n is held until that transaction ends (#2868). Application tables still require migrations;\n runtime table verification checks existence only.\n- Manifest generation fails closed on scanner errors, including unresolved\n decorator spreads. Never emit partial, default-open registration.\n- Generated registration repairs bundled class identity using the exact imported\n constructor, explicit package, and isolated one-object manifest. Never infer\n ownership from paths, simple names, or table names; packages can share names.\n Consumer regression gate: `packages/bundle-gate/src/__tests__/registry-identity.spec.ts`.\n- Decorator registration trusts a class's stack-derived package only when that\n package declares the class (stub, own manifest entry, or explicit\n `packageName`); it then matches the qualified key and never adopts another\n package's same-named registration. Otherwise a loaded (or the cwd project's)\n manifest entry describing the declaring file supplies the identity — apps\n scan workspace-package sources under the app's name (#3110). A class of\n another known package coexists under its qualified key\n (`decorator-different-packages-qualified-coexist`); bundled output needs\n a resolved table that differs first (#3106). Stack\n attribution skips smrt-core (including source-mapped installed\n `smrt-core/src` frames, #3109), decorator-helper, and module-runner frames,\n and drops a frame's Vite `?v=` query (#3490). Library builds stamp each\n decorated class with `static __smrtPackage__` (own property, read at\n decoration as its explicit package) and publish `stampsConstructors`; an\n unstamped class never takes a stamping package's entry by name and table,\n in either order (#3490, `issue-3490-*.test.ts`, `package-stamp.test.ts`).\n Qualified names are `@scope/pkg:Class`; the scope is a deliberate\n discriminator, so manifest generation (`generateManifest`,\n `applyGenerationPasses`, hence `pnpm build`) throws\n `CONFIG_UNSCOPED_PACKAGE_NAME` for an unscoped `package.json` name (#3408).\n Instance and collection registry lookups pass the qualified name. The schema\n planner rejects a table claimed by two unrelated classes\n (`CONFIG_TABLE_NAME_COLLISION`): only one STI family shares a table (#3098);\n collections (including `SmrtReportCollection`) never claim one. Consumer\n gates: `registry/__tests__/issue-3110-*.test.ts`, `issue-3109-*.test.ts`,\n `packages/bundle-gate/src/__tests__/consumer-same-name.spec.ts`.\n- API custom-action eligibility has ONE resolver, `resolveApiMethodExposure()`\n in `generators/custom-action.ts`: both SvelteKit route emitters,\n `resolveApiActionSet`, and `knowledge.ts` read it, and a new consumer must too\n — a local mirror is how a method gets reported unavailable while its route\n file is still written. A public method routes by default only when every\n parameter is JSON-shaped; `@method({ expose })` overrides in both directions,\n `expose: true` bypasses the heuristic ALONE, and an explicit `api.include` or\n `api.routes` entry keeps its pre-#2686 route. Fail closed on scanner\n uncertainty: read `parameters[].typeUnresolved`, never the `'any'` it\n substitutes. Accepting `Date` as wire-able and hydrating it\n (`toCustomActionDate`) are one decision — changing either breaks the other.\n The runtime `APIGenerator` transport stays declaration-gated: `rest.ts`\n dispatch and `preflight-route.ts` prediction both read\n `declaresRuntimeRestRoute()`, which must accept every `ApiCustomRouteConfig`\n option so a sweep moving one onto its method cannot delete the endpoint. Its\n twin `declaresRuntimeRestRouteShape()` deliberately ignores `expose: false`:\n the dispatcher must still SEE a withheld declaration to answer 404, because\n `POST /<collection>/<segment>` resolves to `create` when nothing claims the\n segment. Split unions and type arguments with `splitTopLevel()` — a naive\n `split('|')` truncates `Record<string, Asset | null>` into fragments that\n match no rule and are then accepted, widening the gate. `extractTypeName`\n returns `null` for a generic with an unresolvable ARGUMENT or a union with an\n unresolvable BRANCH so those reach that fail-closed path instead of arriving\n as a bare `'Array'`. Runtime transports read\n `ObjectRegistry.resolveRuntimeMethod()`, which tries the item class's manifest\n entry, then the COLLECTION class's (where a collection-hosted action's\n parameters live), then the live `@method()` store — keyed by CONSTRUCTOR,\n never by simple name (`Account` exists in two packages), and recording\n `isStatic` because an unscanned runtime has no manifest to recover the\n receiver from. `isRestActionRoutable` ANDs \"declared\" with that receiver, so\n it never predicts `allow` for an action dispatch refuses. A declared action\n with no receiver is refused (501 collection-scoped, 404 item-scoped), never\n allowed to fall through into `create`. All eight consumers read the resolver,\n including `packages/smrt-workbench/src/discovery.ts`; it imports the\n `./generators/custom-action` LEAF subpath, not `./generators`, because that\n barrel value-re-exports `MCPGenerator` and so drags `@happyvertical/ai` and\n `@happyvertical/sql` into a build-time helper (2 modules vs 109). Keep\n `custom-action.ts` free of value imports outside `tools/tool-generator`.\n\n## Gotchas\n\n- Optional filesystem support stays lazy: use `createFilesystemAdapter()` in\n `src/filesystem-loader.ts`, not a static files-SDK import. Fully bundled apps\n import `@happyvertical/smrt-core/filesystem` at startup. Use\n `importOptionalDependency()` for similarly heavy optional dependencies.\n- `src/browser.ts` is the `browser`-condition entry of the package root: the\n Node entry (`index.ts`) minus HTTP servers, manifest/knowledge discovery, run-once,\n the Vite plugin, and the test helper. A new export added to `index.ts` must also\n go to `browser.ts` unless it is Node-only; `src/__tests__/browser-entry-parity.test.ts`\n fails otherwise (allowlist: `NODE_ONLY_GROUPS` in that test, each with a reason; the browser\n entry may not publish a name the Node entry lacks), and also\n fails when a workspace package imports a root value the browser entry lacks.\n- `exports['.'].browser` (`dist/browser.js`) is the browser surface. Model\n package roots are bundled for browsers by the bundle-gate browser fixture\n (#3621): anything a model root imports from core must be exported by the\n browser entry and must not reach `node:` built-ins, `pg`, `cosmiconfig`, or\n `jiti`. Keep Node-only code behind a subpath or a lazy import; when a fix\n makes a package pass, delete its entry in\n `packages/bundle-gate/src/browser-gate/expected-failures.ts`.\n- Database retries are transient-only, four attempts total for `get`/`upsert`.\n Use `src/db-errors.ts` classifiers through the cause chain, never message\n matching (SDK driver text can live in `context.originalError`). Constraints,\n bad input, missing tables, and aborted PostgreSQL transactions fail immediately.\n Unique/PK violations become `VALIDATION_UNIQUE_CONSTRAINT`, NOT NULL becomes\n `VALIDATION_REQUIRED_FIELD`; other failures keep the driver error as `cause`.\n- Property initializers precede option values; options win. Arrays/objects are\n shallow-cloned. Collection creation caches fields; table verification caches\n by DB URL and table. Preserve these scopes when changing initialization.\n- The Vite plugin loads scanner/schema code from `dist/` when present. Rebuild\n core after editing those sources before testing consumer manifest generation.\n Vite 8 requires `oxc.decorator: { legacy: true, emitDecoratorMetadata: true }`.\n\n## Validation\n\n`typecheck` checks production source first, then the test project in\n`tsconfig.test.json`. Test coverage and the shared relaxed config\ncontract are documented in [agents/test-types.md](agents/test-types.md).\n\nRun focused tests first, then applicable package checks:\n\n```bash\npnpm --filter @happyvertical/smrt-core test\npnpm --filter @happyvertical/smrt-core typecheck\npnpm --filter @happyvertical/smrt-core build\npnpm --filter @happyvertical/smrt-core test:postgres\npnpm check:agents-chain\npnpm smrt dev:knowledge-check\n```\n\nThe PostgreSQL lane is required for numeric types, UUID casts, conflict targets,\ntimestamps, and migrations. Tests generate their manifest before Vitest; restart\nwatch mode after adding decorated classes. Documentation-only changes need\ninstruction-chain and knowledge freshness checks, not the runtime test suite.\n",
1134
1140
  "moduleDocs": [
1135
1141
  {
1136
1142
  "path": "agents/object-runtime.md",
@@ -1180,7 +1186,7 @@
1180
1186
  {
1181
1187
  "path": "agents/build-knowledge.md",
1182
1188
  "module": "build-knowledge",
1183
- "content": "# Decorators, build integration, and knowledge\n\nKey options: `tableName`, `tableStrategy` ('cti'|'sti'), `conflictColumns`, `indexes` (declared multi-column indexes; see [schema-paths.md](schema-paths.md)), `api`/`mcp`/`cli` (generation config), `ai` (callable methods), `hooks` (beforeSave/afterSave/beforeDelete/afterDelete), `embeddings` (auto-generate), `tenantScoped`, `agent`, `ui` (`{ icon, label, description }` — nav/help hints round-tripped through the manifest as plain data; `description` is the object-level seed for form-level help, #2046).\n\nRegistration sets `SMRT_TABLE_NAME` static property (survives minification).\n\nAn explicit `@smrt({ collection: 'domain.records' })` names the route/permission\nnamespace independently of `tableName`, including standalone non-STI models.\nScanner output preserves it in both `collection` and `decoratorConfig.collection`.\nGenerated registration reads the merged declaration, so older manifests with a\nderived top-level collection still agree with native decorators. Explicit runtime\nregistration options win; absent declarations retain manifest/STI/default naming.\n\n## @field() UI hints (#2046)\n\n`@field({ ui: { basic, group, order, locked } })` — a static, presentation-only\nseed for the field-policy rail (epic #2045). Carried in the manifest under the\nfield's `_meta.ui` (never a top-level `FieldDefinition` key), readable at\nruntime via `getAllFields()` at `field._meta.ui`, and emitted (sanitized) with\n`description` into generated web-collection definitions and browser MCP tool\nschemas. No schema/persistence/security effect — `sensitive`/`readPermission`\nstay the security rail, and `sensitive`/`transient` fields never emit to the\nclient at all.\n\n`ui.widget` (#3599) adds a presentation widget hint:\n`'textarea' | 'currency' | 'email' | 'url' | 'phone'`. It rides the same\n`_meta.ui` channel and is also emitted (sanitized) into web-collection\ndefinitions. The manifest generator validates it against the field type and\nfails the build otherwise: every widget needs a `text` field (`currency` is a\ncurrency-CODE picker for an ISO 4217 string such as `currency = 'USD'`, not a\nmoney amount; amounts are integer minor units and take no widget); an unknown\nvalue or a non-text field is an error. `ui.widget` is a hint only — it never changes\nthe column type or validation.\n\n## Presentation metadata (#3599)\n\nPure helpers live in `src/ui-metadata.ts`, published browser-safe as\n`@happyvertical/smrt-core/ui-metadata` (also re-exported from the package\nroot). `ManifestGenerator.applyUiMetadata()` runs in `applyGenerationPasses`\nafter inheritance, so every producer (Vite plugin, `ManifestBuilder`,\n`generateManifest`) validates the same way.\n\n- **Display label.** `@smrt({ display: { label: '<field>' } })` names the own\n field generic pickers and assistants use to label a record. Own fields only —\n no paths into other packages; a model whose label lives elsewhere ships a\n selector. Undeclared, it defaults to the first of `name`, `title`, `label`,\n `code` that is a usable own field. Build fails when the declared field is\n missing, `sensitive`, `transient`, or a relationship/json field. The resolved\n field is emitted as `displayLabelField` on the manifest object and in the\n knowledge artifact, and generated MCP/WebMCP `list`/`get`/`update`/`delete`\n descriptions end with ``Records are identified by their `<field>` field.``\n (`describeAction` in `generators/tool-schema.ts`; nothing is added when the\n model has no label field).\n- **Selector slots.** `ModuleUISlot.selects: '@scope/pkg:Model'`\n (`@happyvertical/smrt-types`) marks a package component as THE selector for a\n model. The scanner reads such slots statically from `src/**/ui.ts`\n (`parseUiSelectorsFile`, independent of the class glob) and the manifest\n carries them as `uiSelectors` keyed by slot id. `selects` must be a string\n literal; a duplicate slot id, two selectors for one model, or an own-package\n target that does not exist fails the build. Hosts call\n `findSelectorFor(manifests, qualifiedName)`; generic forms use the selector\n for any `@foreignKey`/`@crossPackageRef` field targeting that model and fall\n back to a generic picker labelled by `displayLabelField`. `display` is read\n per class: an STI subclass that declares none uses the default lookup rather\n than its base's declaration, and a CTI child cannot name a parent's field.\n\n## Worker registration artifact (#3117)\n\nWith `svelteKit.enabled`, a production `vite build` compiles the generated\n`smrt-register.ts` into `.smrt/runtime/register.js` after the server build\n(`src/vite-plugin/worker-registration.ts`, called once from `closeBundle`).\nadapter-node re-bundles the server from its own entries, so the web bundle is\nnever importable by a separate Node process; this artifact is how a deployed\ntask/schedule worker registers the app's local objects (and, through the\n`.smrt/register.js` it imports, consumed packages' objects) with the same\nqualified identities and isolated manifests as the web server. It is a nested\nSSR build with no consumer plugins: only aliases pointing into the project\nsources (e.g. `$lib`) apply, and every other bare import stays external so the\nworker shares the installed `@happyvertical/smrt-core` registry. SvelteKit\nvirtual modules (`$env/*`, `$app/*`) are not available to a worker; an object\nimporting one fails at worker startup. A failure fails the build.\n\n## Lightweight discovery\n\nFilesystem-only discovery, module-doc parsing, and graph freshness are owned by\n`@happyvertical/smrt-scanner/knowledge`. Core's knowledge exports remain compatible\nreexports. Runtime manifest projection and custom-action semantics stay in core.\n\n`src/knowledge-discovery.ts`, exported through `smrt-core/knowledge`, enumerates\ninstalled scope directories and reads canonical AGENTS/legacy CLAUDE docs without\nloading package code, artifacts, or scanning objects. CLI snapshots and MCP share\nthese primitives. Callers retain selection policy: snapshots resolve links and\nfall back to `packages/*` only when no installed SMRT package loads; MCP preserves\nnode_modules paths, deduplicates realpaths, excludes authored workspace links,\nand then enriches the selected packages. Workspace-root/glob discovery remains\nowned by each consumer.\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\nThe CLI's schema-command gate reads that same merged file: a pure-consumer\nproject (0 local objects) qualifies on the consumed packages its manifest\nrecords, so a clobbered `objects` map is what made `smrt db:migrate` report\n`missing_local_manifest` (#2925).\n\n`.smrt/manifest.json` has two writers and neither owns all of it: `smrtPlugin()`\ncontributes the project's scanned objects and `smrtConsumer()` the consumed\npackages' entries. Both writes are merge-preserving, keyed on each entry's\nrecorded `packageName` — a plain overwrite in either direction silently drops\nthe other's objects, and the last pass of a multi-environment build is not\nreliably the aggregating one (#1760, #2925). `dist/manifest.json` stays\nlocal-only: a published package must not carry its dependencies' objects.\n\nThe schema-version-1 object projection is additive and high-signal: it retains\nnormalized tenant mode/field, explicit `cti`/`sti` strategy, conflict columns,\nmethod signatures, and field defaults/constraints/readonly/transient flags.\nSensitive fields are removed before both `fields` and `relationships` are\nderived, including legacy flags stored under `_meta`; matching field and\nsnake-case column names are also removed from projected conflict columns, and a\nsensitive custom tenant field is omitted while retaining scope and mode.\nGenerated artifacts assert this boundary with `sensitiveFieldsExcluded: true`;\nthe optional marker keeps schema version 1 additive while letting readers\nidentify older artifacts that require raw-manifest corroboration.\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\nTest generation uses that same effective configuration and publishes knowledge\nmatching the manifest it generated. Disabling knowledge removes a previous local\nknowledge artifact, so consumers cannot mistake it for current enabled output.\n`new ManifestBuilder(projectRoot)` keeps discovery, package identity, and output\nat that explicit root, including installed providers and external base classes;\nomitting the argument retains the current-working-directory\nbehavior. Both manifest writers retain declared package dependencies when merging\ntheir local and consumed-object projections.\n\nTest manifests carry optional `artifactPurpose: 'test'`; dependency discovery\nskips those artifacts, including `.smrt` fallbacks. Unmarked runtime manifests,\nempty published providers, and source-only development providers remain eligible.\n`ManifestBuilder` accepts explicit `outputMode: 'dev' | 'build'`; omitted mode\nretains legacy filename inference. Vitest selects dev mode explicitly, so tests\nnever replace a production `dist/manifest.json`. Relative output directories are\nresolved against the builder's project root.\n\nWith `svelteKit.enabled`, `smrtPlugin()`'s transform prepends\n`import './smrt-register.js';` to the configured config module\n(`<configPath>/<configFileName>`, `src/vite-plugin/sveltekit-register-injection.ts`)\nonce the register module exists, on the same first line so line numbers hold.\nApps therefore never hand-write the guarded registration import (#3416); a\nconfig module that still has it imports the same module twice, harmlessly.\n\nTracked `src/lib/server/smrt-register.ts` belongs to SvelteKit config/type\ngeneration. Generic library builds consume it but do not cache or restore it\nas an output; otherwise a concurrent config producer's bytes can be replayed\nover a newer registration after dependency eligibility changes.\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\n## Vite Plugin\n\n`src/vite-preset/` (`@happyvertical/smrt-core/vite`, `smrt()`) composes the Oxc\ndecorator block, `smrtConsumer`, then `smrtPlugin` with the template path\nconventions. Its package list comes only from the `smrt({ packages })` option, or\n`smrt.config` `consumer.packages` (smrt-config schema); it never sniffs\n`package.json`, and a missing or malformed list throws. Keep its output\nbyte-identical to the two-plugin form (`src/vite-preset/index.test.ts`). It\nalso dedupes `@sveltejs/kit` (`smrt:shared-runtime`): SvelteKit matches\n`redirect()`/`fail()` by class, and a linked/workspace SMRT SvelteKit entry\nwould otherwise import its own copy and turn redirects into 500s.\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\nFor independent CI invocations, both `smrtPlugin()` and `smrtConsumer()` accept\nthe same `generationSnapshot: { path, sha256, provenance, sourceRoot }`. The\nschema-v1 snapshot produced by `serializeSmrtGenerationSnapshot()` contains the\nmerged project/dependency manifest, portable source paths, and source-file\ndigests; each plugin selects its own view. Reuse mode fails closed on\nbyte/provenance/path/content drift, skips scans and manifest writes, and still\ngenerates routes, types, registration, and virtual modules. Omit it for normal\nlocal development and watch mode.\n\n## Clean consumer type checks (TypeScript 6)\n\nThe consumer config hook writes `.smrt/register.js` alongside generated types\nafter route preflight succeeds. Generated SvelteKit registration imports that\nmodule, so it must exist during `svelte-kit sync`, before Vite's `buildStart`.\nBoth hosted routes and the legacy `svelteKit: true` mode follow this order.\nThe clean-consumer regression fixture ships real provider declarations and\nchecks the generated imports with `svelte-check`.\nCore's `generate:test` task depends on its own `build`: both publish the local\n`.smrt` manifest/knowledge pair, so Turbo must serialize production before test\ngeneration instead of caching an interleaved pair.\n\n## Model descriptions\n\nEach manifest object and knowledge `objects[]` entry may carry a user-facing\n`description` (for the planner Features tab and similar). The scanner\n(`extractClassJsDocDescription` in `packages/scanner/src/oxc-parser.ts`) reads it\nfrom the parsed source, in priority order: explicit `@smrt({ description })`,\nelse the first paragraph of the class JSDoc (the JSDoc may sit above `export`\nand decorators), minus a leading `ClassName - ` / `ClassName: `, tags\n(`{@link}`, backticks, HTML) and later paragraphs, capped at 200 chars on a\nsentence boundary. `@internal` classes and classes without docs emit no key. It\nis not inherited from a parent class. Generated MCP tool descriptions are\nunchanged (they stay generic).\n"
1189
+ "content": "# Decorators, build integration, and knowledge\n\nKey options: `tableName`, `tableStrategy` ('cti'|'sti'), `conflictColumns`, `indexes` (declared multi-column indexes; see [schema-paths.md](schema-paths.md)), `api`/`mcp`/`cli` (generation config), `ai` (callable methods), `hooks` (beforeSave/afterSave/beforeDelete/afterDelete), `embeddings` (auto-generate), `tenantScoped`, `agent`, `ui` (`{ icon, label, description }` — nav/help hints round-tripped through the manifest as plain data; `description` is the object-level seed for form-level help, #2046).\n\nRegistration sets `SMRT_TABLE_NAME` static property (survives minification).\n\nAn explicit `@smrt({ collection: 'domain.records' })` names the route/permission\nnamespace independently of `tableName`, including standalone non-STI models.\nScanner output preserves it in both `collection` and `decoratorConfig.collection`.\nGenerated registration reads the merged declaration, so older manifests with a\nderived top-level collection still agree with native decorators. Explicit runtime\nregistration options win; absent declarations retain manifest/STI/default naming.\n\n## @field() UI hints (#2046)\n\n`@field({ ui: { basic, group, order, locked } })` — a static, presentation-only\nseed for the field-policy rail (epic #2045). Carried in the manifest under the\nfield's `_meta.ui` (never a top-level `FieldDefinition` key), readable at\nruntime via `getAllFields()` at `field._meta.ui`, and emitted (sanitized) with\n`description` into generated web-collection definitions and browser MCP tool\nschemas. No schema/persistence/security effect — `sensitive`/`readPermission`\nstay the security rail, and `sensitive`/`transient` fields never emit to the\nclient at all.\n\n`ui.widget` (#3599) adds a presentation widget hint:\n`'textarea' | 'currency' | 'email' | 'url' | 'phone'`. It rides the same\n`_meta.ui` channel and is also emitted (sanitized) into web-collection\ndefinitions. The manifest generator validates it against the field type and\nfails the build otherwise: every widget needs a `text` field (`currency` is a\ncurrency-CODE picker for an ISO 4217 string such as `currency = 'USD'`, not a\nmoney amount; amounts are integer minor units and take no widget); an unknown\nvalue or a non-text field is an error. `ui.widget` is a hint only — it never changes\nthe column type or validation.\n\n## Presentation metadata (#3599)\n\nPure helpers live in `src/ui-metadata.ts`, published browser-safe as\n`@happyvertical/smrt-core/ui-metadata` (also re-exported from the package\nroot). `ManifestGenerator.applyUiMetadata()` runs in `applyGenerationPasses`\nafter inheritance, so every producer (Vite plugin, `ManifestBuilder`,\n`generateManifest`) validates the same way.\n\n- **Display label.** `@smrt({ display: { label: '<field>' } })` names the own\n field generic pickers and assistants use to label a record. Own fields only —\n no paths into other packages; a model whose label lives elsewhere ships a\n selector. Undeclared, it defaults to the first of `name`, `title`, `label`,\n `code` that is a usable own field. Build fails when the declared field is\n missing, `sensitive`, `transient`, or a relationship/json field. The resolved\n field is emitted as `displayLabelField` on the manifest object and in the\n knowledge artifact, and generated MCP/WebMCP `list`/`get`/`update`/`delete`\n descriptions end with ``Records are identified by their `<field>` field.``\n (`describeAction` in `generators/tool-schema.ts`; nothing is added when the\n model has no label field).\n- **Selector slots.** `ModuleUISlot.selects: '@scope/pkg:Model'`\n (`@happyvertical/smrt-types`) marks a package component as THE selector for a\n model. The scanner reads such slots statically from `src/**/ui.ts`\n (`parseUiSelectorsFile`, independent of the class glob) and the manifest\n carries them as `uiSelectors` keyed by slot id. `selects` must be a string\n literal; a duplicate slot id, two selectors for one model, or an own-package\n target that does not exist fails the build. Hosts call\n `findSelectorFor(manifests, qualifiedName)`; generic forms use the selector\n for any `@foreignKey`/`@crossPackageRef` field targeting that model and fall\n back to a generic picker labelled by `displayLabelField`. `display` is read\n per class: an STI subclass that declares none uses the default lookup rather\n than its base's declaration, and a CTI child cannot name a parent's field.\n\n## Worker registration artifact (#3117)\n\nWith `svelteKit.enabled`, a production `vite build` compiles the generated\n`smrt-register.ts` into `.smrt/runtime/register.js` after the server build\n(`src/vite-plugin/worker-registration.ts`, called once from `closeBundle`).\nadapter-node re-bundles the server from its own entries, so the web bundle is\nnever importable by a separate Node process; this artifact is how a deployed\ntask/schedule worker registers the app's local objects (and, through the\n`.smrt/register.js` it imports, consumed packages' objects) with the same\nqualified identities and isolated manifests as the web server. It is a nested\nSSR build with no consumer plugins: only aliases pointing into the project\nsources (e.g. `$lib`) apply, and every other bare import stays external so the\nworker shares the installed `@happyvertical/smrt-core` registry. SvelteKit\nvirtual modules (`$env/*`, `$app/*`) are not available to a worker; an object\nimporting one fails at worker startup. A failure fails the build.\n\n## Lightweight discovery\n\nFilesystem-only discovery, module-doc parsing, and graph freshness are owned by\n`@happyvertical/smrt-scanner/knowledge`. Core's knowledge exports remain compatible\nreexports. Runtime manifest projection and custom-action semantics stay in core.\n\n`src/knowledge-discovery.ts`, exported through `smrt-core/knowledge`, enumerates\ninstalled scope directories and reads canonical AGENTS/legacy CLAUDE docs without\nloading package code, artifacts, or scanning objects. CLI snapshots and MCP share\nthese primitives. Callers retain selection policy: snapshots resolve links and\nfall back to `packages/*` only when no installed SMRT package loads; MCP preserves\nnode_modules paths, deduplicates realpaths, excludes authored workspace links,\nand then enriches the selected packages. Workspace-root/glob discovery remains\nowned by each consumer.\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\nThe CLI's schema-command gate reads that same merged file: a pure-consumer\nproject (0 local objects) qualifies on the consumed packages its manifest\nrecords, so a clobbered `objects` map is what made `smrt db:migrate` report\n`missing_local_manifest` (#2925).\n\n`.smrt/manifest.json` has two writers and neither owns all of it: `smrtPlugin()`\ncontributes the project's scanned objects and `smrtConsumer()` the consumed\npackages' entries. Both writes are merge-preserving, keyed on each entry's\nrecorded `packageName` — a plain overwrite in either direction silently drops\nthe other's objects, and the last pass of a multi-environment build is not\nreliably the aggregating one (#1760, #2925). `dist/manifest.json` stays\nlocal-only: a published package must not carry its dependencies' objects.\n\nThe schema-version-1 object projection is additive and high-signal: it retains\nnormalized tenant mode/field, explicit `cti`/`sti` strategy, conflict columns,\nmethod signatures, and field defaults/constraints/readonly/transient flags.\nSensitive fields are removed before both `fields` and `relationships` are\nderived, including legacy flags stored under `_meta`; matching field and\nsnake-case column names are also removed from projected conflict columns, and a\nsensitive custom tenant field is omitted while retaining scope and mode.\nGenerated artifacts assert this boundary with `sensitiveFieldsExcluded: true`;\nthe optional marker keeps schema version 1 additive while letting readers\nidentify older artifacts that require raw-manifest corroboration.\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\nTest generation uses that same effective configuration and publishes knowledge\nmatching the manifest it generated. Disabling knowledge removes a previous local\nknowledge artifact, so consumers cannot mistake it for current enabled output.\n`new ManifestBuilder(projectRoot)` keeps discovery, package identity, and output\nat that explicit root, including installed providers and external base classes;\nomitting the argument retains the current-working-directory\nbehavior. Both manifest writers retain declared package dependencies when merging\ntheir local and consumed-object projections.\n\nTest manifests carry optional `artifactPurpose: 'test'`; dependency discovery\nskips those artifacts, including `.smrt` fallbacks. Unmarked runtime manifests,\nempty published providers, and source-only development providers remain eligible.\n`ManifestBuilder` accepts explicit `outputMode: 'dev' | 'build'`; omitted mode\nretains legacy filename inference. Vitest selects dev mode explicitly, so tests\nnever replace a production `dist/manifest.json`. Relative output directories are\nresolved against the builder's project root.\n\nWith `svelteKit.enabled`, `smrtPlugin()`'s transform prepends\n`import './smrt-register.js';` to the configured config module\n(`<configPath>/<configFileName>`, `src/vite-plugin/sveltekit-register-injection.ts`)\nonce the register module exists, on the same first line so line numbers hold.\nApps therefore never hand-write the guarded registration import (#3416); a\nconfig module that still has it imports the same module twice, harmlessly.\n\nTracked `src/lib/server/smrt-register.ts` belongs to SvelteKit config/type\ngeneration. Generic library builds consume it but do not cache or restore it\nas an output; otherwise a concurrent config producer's bytes can be replayed\nover a newer registration after dependency eligibility changes.\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\n## Vite Plugin\n\n`src/vite-preset/` (`@happyvertical/smrt-core/vite`, `smrt()`) composes the Oxc\ndecorator block, `smrtConsumer`, then `smrtPlugin` with the template path\nconventions. Its package list comes only from the `smrt({ packages })` option, or\n`smrt.config` `consumer.packages` (smrt-config schema); it never sniffs\n`package.json`, and a missing or malformed list throws. Keep its output\nbyte-identical to the two-plugin form (`src/vite-preset/index.test.ts`). It\nalso dedupes `@sveltejs/kit` (`smrt:shared-runtime`): SvelteKit matches\n`redirect()`/`fail()` by class, and a linked/workspace SMRT SvelteKit entry\nwould otherwise import its own copy and turn redirects into 500s.\n\n**Package models over REST (#3749).** `consumer.packages` registers a package's\nmodels and gives `@smrt/web` (the consumer's `collectionDefinitions`, one per\nlist-capable model, endpoint `/<collection>`) their definitions, but a package\nmodel gets generated REST routes only when it is on the consumer's hosting\nallowlist (`smrtConsumer({ svelteKit: { objects } })`, #2850: an HTTP exposure\nboundary, each model's own `api` config still applies, so `api: false` and\nempty-include models stay closed, tenancy comes with the generated route). The\npreset fills that allowlist: with `smrt.cookbook.json` present it is the models\nof the cookbook's recipes plus `features`, minus models the cookbook `exposure`\nturns off for `api` or a recipe's `options[model].exposure.api === false`\n(`vite-preset/cookbook-exposure.ts`; recipe `{ exclude }` narrowing is not\napplied by the build yet). `smrt({ expose })` replaces the derived list. No\ncookbook and no `expose` keeps `svelteKit: true` (nothing hosted). The producer's\n`@happyvertical/smrt-virt-web` still covers only the app's own models; read\npackage models from `@smrt/web`. `smrt({ objectsDir })` is passed to the hosting\ncontribution too: the producer and consumer share one `routesDir`, and the\ngenerator refuses it when their settings differ.\n\n**STI siblings (#3749).** Package STI subclasses share their base's table, and\nthe manifest gives them the base's `collection` unless they declare one\n(`consumer-plugin/hosted-sti-collections.ts`). Two hosted siblings would both\nclaim `/<base collection>`, so each hosted subclass that inherits its\ncollection gets its class-derived one (`Order` -> `orders`,\n`defaultCollectionName` in the scanner), in every manifest view the consumer\nderives: routes, `@smrt/web`, the generated client and registration. This is the\nconvention the framework already has for an independent route collection\n(`@smrt({ collection })`), applied by default for hosted subclasses; an explicit\ncollection wins. The route therefore reads through the subclass's own\ncollection (rows scoped to its `_meta_type`) and the definition for that model\nadvertises that route (`objectRef` = the model, `endpoint` = `/orders`). A hosted\nbase keeps the shared name and the base-collection semantics (every row of the\ntable, any subtype). A subclass left off the allowlist keeps the shared\ncollection and folds into the base's definition, as before. A collection class\nthat inherits another's item type (`MaterialCollection extends\nProductCollection`) resolves to the same item and collection, so only one of\nthem is hosted (the class named for the item, else the first). Subclasses that\ndeclare no `api` config get the omitted-config default (full CRUD), not the\nbase's `include`; narrow them on the subclass if the base is meant to restrict\nthem.\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\nFor independent CI invocations, both `smrtPlugin()` and `smrtConsumer()` accept\nthe same `generationSnapshot: { path, sha256, provenance, sourceRoot }`. The\nschema-v1 snapshot produced by `serializeSmrtGenerationSnapshot()` contains the\nmerged project/dependency manifest, portable source paths, and source-file\ndigests; each plugin selects its own view. Reuse mode fails closed on\nbyte/provenance/path/content drift, skips scans and manifest writes, and still\ngenerates routes, types, registration, and virtual modules. Omit it for normal\nlocal development and watch mode.\n\n## Clean consumer type checks (TypeScript 6)\n\nThe consumer config hook writes `.smrt/register.js` alongside generated types\nafter route preflight succeeds. Generated SvelteKit registration imports that\nmodule, so it must exist during `svelte-kit sync`, before Vite's `buildStart`.\nBoth hosted routes and the legacy `svelteKit: true` mode follow this order.\nThe clean-consumer regression fixture ships real provider declarations and\nchecks the generated imports with `svelte-check`.\nCore's `generate:test` task depends on its own `build`: both publish the local\n`.smrt` manifest/knowledge pair, so Turbo must serialize production before test\ngeneration instead of caching an interleaved pair.\n\n## Model descriptions\n\nEach manifest object and knowledge `objects[]` entry may carry a user-facing\n`description` (for the planner Features tab and similar). The scanner\n(`extractClassJsDocDescription` in `packages/scanner/src/oxc-parser.ts`) reads it\nfrom the parsed source, in priority order: explicit `@smrt({ description })`,\nelse the first paragraph of the class JSDoc (the JSDoc may sit above `export`\nand decorators), minus a leading `ClassName - ` / `ClassName: `, tags\n(`{@link}`, backticks, HTML) and later paragraphs, capped at 200 chars on a\nsentence boundary. `@internal` classes and classes without docs emit no key. It\nis not inherited from a parent class. Generated MCP tool descriptions are\nunchanged (they stay generic).\n"
1184
1190
  },
1185
1191
  {
1186
1192
  "path": "agents/memory.md",
@@ -1212,6 +1218,16 @@
1212
1218
  "module": "recipes",
1213
1219
  "content": "# SmrtRecipe: declared units of app functionality (#3590)\n\nA **recipe** is a small, named, user-facing unit (\"Sales\", \"Customers\"),\nsmaller than a package. An app or agent adds a recipe and gets that section:\nnavigation, list and create/edit views, and its prerequisites, without every\nmodel in the owning package. Source: `src/recipe.ts` (authoring base class),\n`packages/scanner/src/recipes.ts` (matcher and validation), types in\n`@happyvertical/smrt-types` (`RecipeDefinition`).\n\n## Authoring\n\n`SmrtRecipe` is an abstract, **non-persisted, non-`@smrt()`** base class\nexported from both core entries (`index.ts` and `browser.ts`). A recipe is a\nsubclass with static properties, in the package that owns its models:\n\n```ts\nexport class SalesRecipe extends SmrtRecipe {\n static id = 'commerce.sales'; // dotted lowercase, unique\n static label = 'Sales';\n static summary = 'Take customer orders and track them.';\n static synonyms = ['sales orders', 'orders'];\n static models = [Order]; // class references\n static nav = [{ label: 'Sales Orders', model: Order }];\n static requires = ['commerce.customers']; // recipe ids, any package\n static options = { Order: { fields: { status: { default: 'draft' } } } };\n}\n```\n\nStatics are read **structurally** (nothing is evaluated), so each must be\nspelled literally; `models` and `nav[].model` take identifiers. Write\n`static options = { ... } as const` so `visibility` stays a literal type.\nThe emitted `options` are keyed by qualified model name.\n\n## Host metadata: group, section, nav detail, alternatives (#3604)\n\nEverything a catalog host (an app shell, an agent, a CLI) needs\nto present a recipe lives on the recipe, so it reads the manifest or knowledge\nartifact instead of importing runtime code. All are optional and additive:\n\n| Static | Shape | Meaning |\n| --- | --- | --- |\n| `group` | `{ id, label, summary? }` | Recipes with the same `group.id` share one card; the first declaration of an id supplies `label` and `summary`. Declaration order is the sub-switch order. |\n| `section` | `{ id, label, icon?, description? }` | Navigation section the recipe suggests for its `nav` entries. The host owns sections and the user may rename them, so keep `id` stable; the same `id` shares a section. |\n| `requiresAny` | `string[][]` | At least one id of each inner list must be on (`[['products.simple', 'products.clothing']]`); adding the recipe with none on adds the first. |\n| `nav[].icon` / `description` | string | Shell icon name and one friendly line for a business owner. The icon must be a key of `SHELL_ICON_PATHS` (smrt-svelte `admin-shell/shell-icons.ts`, camelCase such as `fileText` or `shoppingBag`); the same holds for `section.icon`. `packages/scanner/src/__tests__/workspace-recipes.test.ts` fails on any other name. |\n| `nav[].noun` | string | What \"New\" creates when the label is not countable (\"Stock levels\" -> `stock entry`). |\n| `nav[].key` | slug | Fixed key for the layout id `item:<pkg>:<Model>:<key>`. Required for a second entry over the same model and for a `filter`. |\n| `nav[].filter` | `{ field, value }` | Narrows the entry to rows where `field` equals `value` (Ingredients over Products: `productType` = `material`). Needs a `key`. |\n| `options.<Model>.fields.<f>.required` | boolean | The form refuses to save without a value. |\n\nAcross recipes of one package the scanner also rejects a repeated nav `key`\nover the same model (it names one layout id) and one `group`/`section` id with\ntwo different labels. `packages/scanner/src/__tests__/workspace-recipes.test.ts`\nscans every recipe in the workspace and checks ids, `requires`/`requiresAny`\ntargets (which a single package cannot see) and shared labels.\n\nChild models (line items) are simply listed in `models` without a `nav` entry.\nValidation: `group`/`section` take only their keys, slug ids, non-empty\nstrings; `requiresAny` ids follow the recipe-id format and cannot name the\nrecipe itself; each (model, key) pair appears once in `nav`; a `filter.field`\nmust be a field the model declares (checked in core on the merged manifest,\n`assertRecipeOptions`).\n\nNot part of the recipe declaration (still host-local): a host's\ncross-package `forms` and `extends` (form records spanning profiles, variants\nand SKUs).\n\nUser-facing field text for the recipes comes from `@field({ description })` on\nthe models (the glossary seed), so a package that declares a recipe carries the\ndescriptions of every field the recipe shows. A field the package does not\ndeclare itself (`parentId` from `SmrtHierarchical`) is described by the recipe's\n`options.<Model>.fields.<f>.help`, which wins over the description in the\nglossary. Write all of it in plain words: no API, JSON, REST, MCP, SQL or UUID\noutside help that is about connecting other tools.\n\n## Non-model surfaces, providers, runtime, demo seed (#3708)\n\nMany features are not a list of models: a shell widget (assistant dock,\nnotification bell), a component, a route, a settings panel. All four statics are\noptional and additive; a recipe that declares none emits none of the keys. They\nare emitted on the recipe entry in `manifest.json` and `smrt-knowledge.json`.\nComponents are named by `'<package specifier>#<ExportName>'` and never imported\nat scan time; a host resolves them. Relative or absolute specifiers are rejected.\n\n| Static | Shape | Meaning |\n| --- | --- | --- |\n| `surfaces` | `RecipeSurface[]` | `{ kind: 'shell-widget', slot, export, label, icon? }`, `{ kind: 'route', path, export, label }`, `{ kind: 'settings-panel', export, label }`, `{ kind: 'playground', export, label? }`, `{ kind: 'widget', type, export, label, ... }` (see below). `slot` is a smrt-svelte `ShellSlot`; `path` starts with `/` and has no whitespace, `?`, `#` or `..`. |\n| `providers` | `RecipeProvider[]` | `{ id, kind, options, required, secrets?, browserOptions? }`: lowercase slugs, `options` non-empty and distinct, `required` always written, `secrets` distinct `UPPER_SNAKE` names (never values), `browserOptions` a non-empty subset of `options` that run in a browser with none of the secrets (#3709). Ids are unique per recipe. |\n| `runtime` | `'browser' \\| 'server' \\| 'both'` | Where the recipe's runtime pieces can run. Omitted means `both`; the value is emitted as authored. The build refines it into `demo` (below). |\n| `demoSeed` | `{ export } \\| { data }` | A fixture export reference, or inline JSON of at most 8 KB, for demo hosts. Exactly one key. |\n\nWrite `surfaces` with `as const` (like `options`) so `kind` and `slot` stay\nliteral types. The slot union is `RecipeShellSlot` in smrt-types (types only) and the\nscanner's `RECIPE_SHELL_SLOTS` list copies smrt-svelte's `SHELL_SLOTS`; a\ntype-level test in smrt-svelte and a scanner test keep them equal, so there is\nno runtime dependency on smrt-svelte. Inline `demoSeed.data` must be finite JSON. Every violation (unknown\nkind or slot, extra key, duplicate route path or provider id, malformed\nreference) is a scan error. Example: `events.calendar` in\n`packages/events/src/recipes.ts`.\n\n## Widget surfaces (#3727)\n\n`{ kind: 'widget', ... }` contributes a widget to customizable overviews\n(smrt-svelte `./overview`). The manifest carries everything a host needs to\nregister it; the helper `registerRecipeWidgets` in smrt-svelte does that with an\ninjected module resolver, so nothing is imported at scan time.\n\n```ts\nstatic surfaces = [\n {\n kind: 'widget',\n type: 'sales-total', // registry type, unique per package\n export: '@acme/shop/svelte#SalesTotal', // the component\n label: 'Sales total', // text or an i18n key\n description: 'Order totals for the period.',\n icon: 'chart',\n version: 2, // option-schema version (default 1)\n migrate: '@acme/shop/server#migrateSalesTotal', // needs version above 1\n options: [\n { key: 'model', type: 'model', label: 'Model', required: true },\n { key: 'style', type: 'enum', label: 'Style', default: 'bar',\n choices: [{ value: 'bar', label: 'Bar' }, { value: 'line', label: 'Line' }] },\n ],\n data: { load: '@acme/shop/server#loadSalesTotal', models: ['@acme/shop:Order'] },\n allowedIn: ['shop.home'], // overview ids; omit for any\n defaultSpan: 2, minSpan: 1, maxSpan: 3,\n },\n] as const;\n```\n\nEmitted keys, in this order: `kind`, `type`, `export`, `label`, `description`,\n`icon`, `version`, `migrate`, `options`, `data`, `allowedIn`, `defaultSpan`,\n`minSpan`, `maxSpan`; absent ones are omitted. Rules (each violation is a scan\nerror, same strictness as the other kinds):\n\n- `type` is a lowercase kebab id (`[a-z][a-z0-9-]{0,31}`), unique among a\n recipe's surfaces and across the package's recipes. `export`, `data.load` and\n `migrate` are `'<package specifier>#<ExportName>'`; relative specifiers are\n rejected. `label` is required.\n- `options` is a non-empty list of plain-JSON fields limited to the overview\n option types: `text`, `markdown`, `identifier`, `model`, `integer`, `number`,\n `boolean`, closed `enum` (non-empty distinct `choices`). There is no free-form\n query, expression, URL or SQL type. Keys are camelCase (`[A-Za-z][A-Za-z0-9]{0,31}`)\n and unique; `default` must pass its field's own rule; `min`/`max` only on\n numbers, `maxLength` only on text. Only `key`, `type`, `label`, `help`,\n `required`, `default`, `min`, `max`, `maxLength`, `choices` are accepted.\n- `data` takes `load` and/or `models` (qualified class names, distinct). The\n scanner cannot see other packages, so a model is checked for format only.\n- `allowedIn` is a non-empty list of distinct overview ids\n (`[A-Za-z0-9][A-Za-z0-9_.:-]{0,95}`); spans are integers 1 to 4 with\n `minSpan <= defaultSpan <= maxSpan`.\n\nThe scanner's copies of these rules live in `packages/scanner/src/recipe-widgets.ts`\nand a scanner test compares them to the smrt-svelte source. See\n[overview-surfaces.md](../../smrt-svelte/agents/overview-surfaces.md#recipe-widgets-recipe-widgetsts)\nfor how a host registers them.\n\n## Browser-demo mode: Live, Mock, Sample, Server (#3709)\n\nA catalog host (smrt-planner) labels each feature by how far it runs in a\nbrowser-only demo. The label is **derived at build time**, never authored: the\nmanifest build combines what the bundle-gate measured for the recipe's package\nwith the recipe's own `runtime`, `providers` and `demoSeed`, and emits it as\n`demo` on the recipe entry and `browser` on the package. A recipe that declares\nnone of those keeps its shape except for `demo`, which every recipe of a\nmeasured package gets.\n\n```jsonc\n// manifest.json and smrt-knowledge.json, top level\n\"browser\": {\n \"status\": \"browser-safe\" | \"server-only\",\n \"issues\": [\"#3624\"], // tracking issues; omitted when browser-safe\n \"reason\": \"node:crypto\", // the package's own blocker; omitted when only inherited\n \"via\": [\"@happyvertical/smrt-tenancy\"] // inherited from these; omitted when none\n}\n// on each entry of \"recipes\"\n\"demo\": {\n \"mode\": \"live\" | \"mock\" | \"sample\" | \"server\",\n \"reasons\": [\"Needs a server because ...\"], // plain sentences, in rule order\n \"mocked\": [\"smtp\"] // providers a demo fakes; omitted when none\n}\n```\n\n| Mode | Meaning |\n| --- | --- |\n| `live` | Real browser data layer (PGlite); nothing faked. |\n| `mock` | Runs in the browser, but a provider (sending mail, OAuth) is faked. |\n| `sample` | Runs in the browser on `demoSeed` fixtures only. |\n| `server` | Needs a server; a browser-only host cannot offer it. |\n\n**Package capability.** The browser gate (`packages/bundle-gate`) fails any\nmodel package that is not in its expected-failures list and does not build for\na browser, and any listed package that now builds. So a package is `server-only`\nexactly when it is listed. The ratchet attributes a defect to the package that\nowns the fix, so a dependent passes while still dragging the failure into a\nbrowser bundle; the derivation therefore also marks a package `server-only`\nwhen a workspace package it depends on (transitively, through `dependencies`)\nis, and records them in `via`. `smrt-core` is the platform baseline and does not\npropagate. The table lives in `src/manifest/browser-capability.generated.ts`\n(generated, never edited) and a bundle-gate spec fails when it is stale; see\nthat package's AGENTS.md for the regenerate command. A package the gate does\nnot measure (an app's own, a UI package) emits no `browser`, and its recipes get\na `demo` only when they declare `runtime`, `providers` or `demoSeed`.\n\n**Rules** (`src/recipe-demo.ts`, pure and browser-safe; the worst mode wins,\n`live` < `mock` < `sample` < `server`):\n\n1. `runtime: 'server'` is `server`. `runtime: 'both'` or omitted makes no claim:\n the package capability refines it, so `both` on a `server-only` package is\n `server`.\n2. A `server-only` package is `server`.\n3. A **server provider** is one that lists `secrets` (credentials a browser\n cannot hold). One with a `browserOptions` entry (an in-browser model such as\n `webllm`) is satisfied in a browser and changes nothing. Otherwise it is\n **mockable** when its `options` include `mock`. A *required* server provider\n that is neither is `sample` when the recipe has a `demoSeed` (the fixtures\n stand in for what the provider would feed), else `server`. An optional one\n that is neither leaves the mode alone and adds a reason.\n4. Any mockable server provider makes the recipe `mock` and lands in `mocked`.\n5. Otherwise `live`.\n\n**Contradictions fail the build** (like `assertRecipeOptions`), naming the\nrecipe: `runtime: 'browser'` on a `server-only` package, or beside a required\nserver provider with no in-browser and no `mock` option. Fix by giving the\nprovider a `browserOptions` entry or a `mock` option, making it optional, or\ndeclaring `both`/`server`. An explicit `both` or\n`server` on a `server-only` package is not a contradiction.\n\n**Dependencies between recipes.** `demo` is the recipe's own answer. A host that\nwants the answer including what the recipe needs calls\n`effectiveRecipeDemo(id, recipes)` (exported from both core entries): the worst\nof its own mode and every `requires` (transitively), counting the best known\nalternative of each `requiresAny` list; unknown ids are ignored.\n\n**Importing it in a browser page.** Use the subpath\n`@happyvertical/smrt-core/recipe-demo` (`deriveRecipeDemo`,\n`effectiveRecipeDemo`, `RECIPE_DEMO_MODES` and the demo types). Its module graph\nis `src/recipe-demo.ts` alone, with type-only imports, so it carries no `node:`\nimport. The root entry and `/browser` both also export these names, but both\nreach `manifest/manifest-loader.ts` through the registry, and that module calls\n`createRequire` from `node:module` as it loads, which a browser page cannot\nsatisfy. `src/__tests__/recipe-demo-entry.test.ts` walks the module graph and\nfails if the subpath gains a `node:` or package import.\n\nThe classification runs in `ManifestGenerator.applyGenerationPasses` (the one\npass sequence the Vite plugin, `ManifestBuilder` and `generateManifest()` share)\nthrough `applyRecipeDemo` in `src/scanner/recipe-demo-pass.ts`, after the\nmerged-manifest recipe assertions. It needs the package name, so it cannot run\nin the scanner, which resolves recipes before it knows the package.\n\n## How the scanner collects it\n\nThe scanner class pass finds classes by decorator, which a recipe lacks, so\n`recipes.ts` is a second matcher in the style of the agent-surface one: a class\nwhose `extends` resolves to the `SmrtRecipe` import binding from\n`@happyvertical/smrt-core` (named, aliased `SmrtRecipe as R`, or namespace\n`Core.SmrtRecipe`; any core subpath such as `/browser`). A same-named local\nclass or an import from another library is not a recipe. A recipe must extend\n`SmrtRecipe` **directly**. Within one file, extending another recipe or declaring\none as a class expression or inside a block is a scan error rather than a silent\ndrop. Across files it is not detected: a module that never names `SmrtRecipe`\nis not read, so a recipe extending a recipe or intermediate base imported from\nanother module is unsupported and would be missing from the artifacts.\n\nClass references resolve to qualified names (`@scope/pkg:Class`) through the\nimport binding: a relative specifier picks the scanned class by file, then by\nclass name (barrel re-exports keep the name); an aliased import follows the\n*imported* name; `Ns.Class` namespace members and same-file classes work; a\nbare package specifier is rejected (models must belong to this package). When\nthe imported file declares no such class, the barrel fallback binds a same-named\nscanned class, so keep model class names unique within a package. This\nwas chosen over qualified-name strings because it type-checks and survives\nrenames; strings were not needed.\n\nRecipe discovery is independent of the model `include` glob (like intents; it is\nnot gated by the `agentSurface` option), and\n`ScanResults.recipes` is populated by `resolve()`. Every error is a scan error,\nso every producer that reads `scanAndResolve()` (`smrtPlugin`, `ManifestBuilder`)\nfails the build. (The legacy `ManifestGenerator.generateManifest()` path takes\npre-scanned results and never sees recipes.) The adapter qualifies names and emits a top-level `recipes` array in\n`manifest.json`; `buildDomainKnowledgeManifest` projects the same array into\n`smrt-knowledge.json`. A package with no recipe emits no key.\n\n## Build-time validation\n\nIds are dotted lowercase and unique; `models` is non-empty, resolvable, and\nduplicate-free; every `nav` model is in `models`; `requires` are well-formed,\nnot self-referential or duplicated, and acyclic within the package (ids in other\npackages cannot be checked at scan time); `options` name only listed models and\nand only the keys below. Field names are checked in core\n(`ManifestGenerator.assertRecipeOptions`, after inherited fields merge) against\nthe merged manifest, so STI-merged, tenant, and framework-base fields such as\n`parentId` are accepted along with the universal `id`, `slug`, `context`,\n`created_at`, `updated_at`; anything else fails the build.\n\n## options: curation hints, never a second schema\n\nKeyed by model class name. `fields.<name>` accepts the smrt-fields policy\nvocabulary: `default`, `label`, `help`, `order`, `visibility`\n(`basic | advanced | hidden`), `locked`; applying a recipe produces\nfield-policy rows. `exposure.<api|mcp|cli>` accepts only `false` or\nnon-empty `{ exclude: [...] }` naming a CRUD verb (`list`, `get`, `create`,\n`update`, `delete`) or a public instance method the model declares or inherits: `true` and `include` are\nrejected because they can widen what the model already declares. Custom fields are out of scope.\n\n## User-facing help (#3591)\n\nHelp is documentation for the people using the app, not developers. It lives\nbeside the recipe as Markdown and is pointed to by `static help`:\n\n```ts\nexport class SalesRecipe extends SmrtRecipe {\n // ...\n static help = './sales.recipe.md'; // relative to the recipe file, .md only\n}\n```\n\nThe file holds an **Overview** and **Tasks** (short how-tos). A step names a\nfield as `{field:<fieldName>}` or `{field:<Model>.<fieldName>}`; the Markdown\nblock (heading, paragraph, or list item) containing a reference is **tied to\nthat field**. Keep prose in the separate file rather than the recipe class.\nField text itself comes from `@field({ description })`, the user-facing seed\nsmrt-fields uses for form help; every field a recipe shows should carry one.\n\n**Emission.** The scanner reads the file (a missing, empty, non-`.md`, absolute\nor `..`-climbing path is a scan error) and emits\n`help: { markdown, fieldRefs }` on the recipe entry in `manifest.json` and\n`smrt-knowledge.json`, so a static host needs no further request.\n`fieldRefs` (distinct references as written, sorted) is **derived**, never\nauthored. Knowledge fields also carry `description` (sensitive fields stay\nexcluded). `ManifestGenerator.assertRecipeHelp` fails the build on a reference\nto a field the recipe's models do not declare, and on a `fieldRefs` list that\ndisagrees with the Markdown. The scanner has its own copy of the reference\ngrammar (`deriveHelpFieldRefs`; it cannot import core), so that comparison also\nkeeps the two from drifting. A reference to a **sensitive** or universal (`id`,\n`slug`, ...) field also fails the build: neither reaches the knowledge artifact,\nso a host building its field list from it would silently drop the step naming\none; describe such a field in prose without a reference.\n\nKnown limitation: the help file is a generator input, but editing only it does\nnot trigger a dev-server rescan (the watch matches source globs) and generation\nsnapshots do not digest it, so rebuild (or touch a `.ts` source) after editing.\n\n**Rendering.** `src/recipe-help.ts` is pure and browser-safe (exported from\nboth core entries). `renderHelp(help, models, options?)` takes the recipe's\nhelp plus the app's effective field policies (`HelpModel[]`; label, help,\nvisibility, disabled in smrt-fields vocabulary, with `@field` descriptions) and\nreturns `{ blocks, glossary, connect? }`:\n\n- `{field:x}` becomes the effective label;\n- a block tied to a hidden, disabled, undeclared, or **advanced** field (unless\n `showAdvanced`) is dropped; a list item drops alone; a heading whose section\n had content that was all dropped is pruned (a heading that never had content\n stays);\n- the glossary lists each shown field with its effective help, else its\n description. A shown field with neither is **skipped**, not listed blank;\n- `connect` is the collapsed \"Connect other tools\" section: the recipe's\n generated REST routes, MCP tools and CLI commands (`options.surfaces`, from\n the knowledge artifact), minus the recipe's `exposure` narrowing\n (`options.exposure`). Hosts render it last.\n\nThe result is an AST of text nodes (`text`, `code`, `strong`, `em`), never HTML,\nso authored markup stays inert text and nothing needs sanitizing. Only\n`##`-`####` headings, paragraphs, flat lists and bold/italic/code are\nunderstood. `helpToMarkdown(rendered)` flattens it for hosts with their own\nrenderer; render that with raw HTML disabled. `validateHelp(help, models)`\nreturns the build-failure problems for hand-built entries.\n"
1214
1220
  },
1221
+ {
1222
+ "path": "agents/cookbook.md",
1223
+ "module": "cookbook",
1224
+ "content": "# Cookbook v1 contract (#3747, #3748)\n\nA cookbook is the portable JSON document that says what an app is made of.\nDefined once here so the CLI (`smrt cookbook validate|apply`) and the cookbook\napp shell read the same thing.\n\n| Piece | Where |\n|---|---|\n| Types (type-only) | `@happyvertical/smrt-types`: `Cookbook`, `CookbookPolicyRow`, `CookbookLayout`, `CookbookTheme`, `CookbookOverviewOverride`, `CookbookExposureSurface` |\n| Parse / validate / migrate (browser-safe) | `@happyvertical/smrt-core/cookbook`: `validateCookbook(doc, { recipes? })`, `parseCookbookText`, `COOKBOOK_SCHEMA_URL`, `COOKBOOK_LEGACY_SCHEMA_URL`, `COOKBOOK_VERSION` |\n| JSON Schema | `@happyvertical/smrt-core/cookbook/v1.schema.json` (`$id` is the `$schema` URL; s-m-r-t.dev hosts it later) |\n| App shell | `@happyvertical/smrt-svelte/cookbook` renders a cookbook; `loadCookbookApp` in `/cookbook/server` loads and validates it ([cookbook-shell.md](../../smrt-svelte/agents/cookbook-shell.md)) |\n| Fixtures | `src/cookbook/__fixtures__/` (the planner's bakery, mechanic, welder, yoga-studio documents) |\n\nFields: `$schema`, `version: 1`, optional `name`/`description`, `recipes`,\n`features` (absent reads as `[]`), `policies` (app-scope smrt-fields rows),\n`exposure`, `layout` (smrt-svelte `ShellLayout`), `theme`, `overviews`\n(smrt-svelte `OverviewOverride` per overview id). Sample data is not part of\nthe document; the planner's library wrapper (id, icon, keywords, settings) is\nplanner-side.\n\n## Rules\n\n- Versioning: add optional fields within `version: 1`; readers keep and ignore\n unknown keys (`additionalProperties: true`). A breaking change is `version: 2`.\n- Layout and overview values are typed structurally so smrt-types and core never\n depend on smrt-svelte; smrt-svelte owns deep validation (widgets against page\n definitions, nav ids). `CookbookLayout`/`CookbookOverviewOverride` are\n identical to the smrt-svelte types; a typechecked assertion in smrt-svelte\n (`cookbook/__tests__/contract-types.ts`) fails when either side adds a field.\n- Legacy `.../blueprint/v1.json` is accepted on read and rewritten; never written.\n- Recipe existence, `requires`/`requiresAny` and setting targets need manifests:\n `validateCookbook(doc, { recipes })` checks ids when a catalog is passed;\n the rest is the CLI's job.\n- Keep the JSON Schema and `validate.ts` in step; the test validates the same\n fixtures against both.\n\n## Applying a cookbook\n\n`smrt cookbook validate <file|url>` and `smrt cookbook apply <file|url> [dir]`\n(`packages/cli/agents/cookbook-apply.md`) are the consumers. Apply writes\n`smrt.cookbook.json` at the project root, plus the packages the recipes,\nfeatures and policies need, into `package.json`. The file is the cookbook\nverbatim (legacy `$schema` rewritten); apps read it, apply does not interpret it.\n`policies` is required by the validator (use `[]`). The cookbook carries no package\nversions, so apply uses the project's smrt line.\n\n## Hosting package models\n\nAn app that applies a cookbook lists the recipes' packages in `consumer.packages`\nand nothing else: the `smrt()` Vite preset derives the REST allowlist from the\ncookbook, so the Orders of a `commerce.sales` recipe get `/api/orders` routes and\nan `@smrt/web` definition (see [build-knowledge.md](build-knowledge.md), \"Package\nmodels over REST\"). `exposure` listing `api` for a model withdraws its route. Models that share an\nSTI table (the bakery's `Order`, `WholesaleOrder`, `PurchaseOrder` and\n`ProductionOrder`; `Product` and `Material`) each get their own route and\ndefinition, at `/<class collection>` (`/orders`, `/productionorders`), listing\nonly their own rows; the build-knowledge section explains why.\n\n## Editing a cookbook\n\nThe command engine (`@happyvertical/smrt-core/cookbook/engine`) is the one place\nthat changes a cookbook: see [cookbook-engine.md](cookbook-engine.md).\n"
1225
+ },
1226
+ {
1227
+ "path": "agents/cookbook-engine.md",
1228
+ "module": "cookbook-engine",
1229
+ "content": "# Cookbook command engine (#3753)\n\n`@happyvertical/smrt-core/cookbook/engine` (`src/cookbook/engine/`) is the one\nplace the rules for changing a [cookbook](cookbook.md) live. A planner UI, a\nlocal Kitchen MCP server, the CLI and a website all call it; each is an adapter.\nIt imports only relative modules and `@happyvertical/smrt-types` types, so it\nruns in Node and in a browser. No SvelteKit, storage, singleton or inference.\n\nIt lives in `smrt-core` because the contract and validator (`./cookbook`) do and\nthe package DAG forbids a back edge: types -> core. A separate package would add\na node for pure functions with no other dependency.\n\n## API\n\n```ts\nimport { createCookbookEngine } from '@happyvertical/smrt-core/cookbook/engine';\nconst engine = createCookbookEngine({ catalog, cookbook?, state? });\nengine.run({ id?, name, input, expectedRevision? }) // CommandResult, never throws\nengine.batch({ id?, commands, expectedRevision? }) // BatchResult, atomic\nengine.snapshot() / .cookbook() / .state() / .subscribe(fn)\nengine.tools / engine.batchTool // JSON Schema tool definitions\n```\n\n- **Catalog is data** (`EngineCatalog`): `recipes` (manifest `RecipeDefinition`\n fits), `models` (`id, name, packageId, exposed?, fields?`), optional\n `cookbooks` (a library), `themePresets`, `currencies`, `settingTargets`.\n Nothing is bundled. Build it from published manifests or `smrt-knowledge.json`.\n- **Commands** (20, `COOKBOOK_COMMANDS_VERSION = 1`): `apply_cookbook` (select),\n `import_cookbook`, `add_cookbook`, `remove_cookbook`, `add_recipes`,\n `remove_recipes`, `add_features`, `remove_features`, `set_settings`,\n `set_policy`, `set_theme`, `reset_theme`, `set_name`, `rename_section`,\n `rename_item`, `hide`, `show`, `validate`, `export_cookbook`, `undo`. Navigation\n and focus are not commands: they are an adapter concern.\n- **Errors** are typed (`CommandError`: `code`, `message`, `path?`, `details?`):\n `unknown_command`, `invalid_input`, `not_found`, `confirmation_required`,\n `conflict`, `id_reuse`, `batch_failed`, `failed`.\n- **Result** carries the `receipt` (`name, id?, summary, changed, revisionBefore,\n revisionAfter, undoId?, changes?, warnings?`), the compact `snapshot` and\n optional `data` (`validate`, `export_cookbook`).\n\n## Execution contract\n\n- **Revisions** count applied changes (a no-op does not move it), so equal\n histories give equal revisions. `expectedRevision` that is not current fails\n `conflict` with `details.{expectedRevision,currentRevision}`: an older model\n action cannot overwrite a newer manual edit. Send it on every model action.\n- **Command ids**: a repeated `id` with the same command returns the first\n result with `replayed: true` and applies nothing (failures replay too: take a\n new id after re-reading). The same id with a different command is `id_reuse`.\n The journal keeps `maxJournal` (500) ids and is part of `state()`.\n- **Atomic batches** run on a working copy; one failure leaves the state\n byte-identical and returns `batch_failed` naming `details.index`. A batch that\n changes n times advances the revision by n, as n runs would, and has one undo.\n `undo` and per-command ids/revisions are not allowed inside a batch.\n- **Undo** restores the document from before the change; it is a `conflict`\n while the plan differs from what that change left, unless `force`.\n- **Recovery**: `state()` is plain JSON (revision, applied library id, document,\n journal); `createCookbookEngine({ catalog, state })` resumes it, retries\n included. Undo entries are not persisted.\n- **Canonical document**: ids sorted, policies sorted, empty sections absent,\n key order fixed, so two engines export identical text. Recipe/feature ids the\n catalog lacks stay in the document and show as `snapshot.unavailable`.\n\n## Deliberately not here\n\nPackage or starter version pinning (waits on #3604/#3754). Deep overview-widget\nand nav-id checks (smrt-svelte owns them; `import_cookbook` carries `overviews`\nand `layout` through unjudged, `validate` only flags layout ids the menu lacks).\nTyped diagnostics from `validateCookbook` itself (#3754): `validate` wraps its\nstrings as `invalid_document` until it returns them.\n\n## Becoming an adapter: what smrt-planner changes (after the smrt release)\n\nParity to hold: the planner's `tests/headless-planner.test.ts` scenario is\nported in `__tests__/engine.test.ts` (`SCRIPT`) over a data catalog.\n\n1. Depend on the released `@happyvertical/smrt-core`; build `EngineCatalog` from\n its catalog data (`recipes.json`/`catalog.json` -> `recipes`, `models`,\n `cookbooks`: the library wrapper keeps id/icon/keywords planner-side).\n2. Delete `commands/execute.ts`, `commands/runner.ts`, `commands/snapshot.ts`,\n `commands/schemas.ts` (keep `checkSchema` only if still used), `recipes/\n plan-data.ts`, `recipes/resolve.ts`, `settings/app-settings.ts` rules,\n `assistant/change.ts` apply* helpers, `cookbook/assemble.ts` and the\n `createPlainHost`. `createHeadlessPlanner` becomes `createCookbookEngine`\n behind the same `run/snapshot/subscribe/cookbook` names.\n3. The browser controller keeps: `focus` and the planner tab/section cells\n (snapshot `focus` is added by the adapter), reactive wrapping of\n `engine.subscribe`, `onReplaced` (regenerate sample data after\n `apply_cookbook`), and the `cookbookStore` <-> `engine.state()` sync\n (`engine.cookbook()` to persist; rebuild with `cookbook`/`state` on load).\n4. Command differences to absorb: `apply_cookbook`/`import_cookbook` report\n `changed` by content; `rename_*`/`hide`/`show` need the catalog `models`\n `packageId` for item ids; undo is whole-document (the old per-slice undo goes);\n `unsupported` is gone (the engine always has every slice); new `set_name`,\n `validate`; receipts gain `revisionBefore/After`; `expectedRevision` moves\n into the command (the option form still works).\n5. Overview customisation stays a planner/smrt-svelte concern; hold `overviews`\n in the document via `import_cookbook` and write them with a planner-side\n command until an engine command is agreed.\n6. MCP/Kitchen (#3752, done): `smrt kitchen mcp` holds one engine per plan id and\n serves `engine.batchTool`'s schema as `run_commands`; the plan reaches disk\n as `smrt.cookbook.json` through `cookbook apply` (`cli/agents/kitchen.md`).\n"
1230
+ },
1215
1231
  {
1216
1232
  "path": "agents/browser-entry.md",
1217
1233
  "module": "browser-entry",
@@ -148,6 +148,11 @@ export declare function findManifestObjectByName(manifest: SmartObjectManifest,
148
148
  export declare function isCollectionManifestClass(manifest: SmartObjectManifest, obj: SmartObjectDefinition, seen?: Set<string>): boolean;
149
149
  export declare function resolveCollectionItemTypeName(manifest: SmartObjectManifest, obj: SmartObjectDefinition): string | undefined;
150
150
  export declare function resolveCollectionItemObject(manifest: SmartObjectManifest, obj: SmartObjectDefinition): SmartObjectDefinition | undefined;
151
+ /**
152
+ * True when some ancestor model maps to the SAME REST collection (a shared STI
153
+ * table). The STI base model owns the shared table's single definition.
154
+ */
155
+ export declare function isStiChildModel(manifest: SmartObjectManifest, obj: SmartObjectDefinition): boolean;
151
156
  /**
152
157
  * Select the manifest entries that become web collection definitions: one per
153
158
  * REST collection, STI children folding into their base model, collection
@@ -1 +1 @@
1
- {"version":3,"file":"web-collections.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/web-collections.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"web-collections.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/web-collections.ts"],"names":[],"mappings":"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"}
@@ -760,6 +760,6 @@ export { collectionDefinitions as default };
760
760
  `;
761
761
  }
762
762
  //#endregion
763
- export { buildWebCollectionDefinition, buildWebFieldDefinitions, buildWebMcpToolDefinition, buildWebMcpToolDefinitions, buildWebRelationships, buildWebToolDescriptors, compareText, computeWebManifestHash, findManifestObjectByName, generateWebModule, isCollectionManifestClass, orderedManifestObjectEntries, resolveCollectionItemObject, resolveCollectionItemTypeName, selectWebCollectionEntries, selectWebMcpToolEntries };
763
+ export { buildWebCollectionDefinition, buildWebFieldDefinitions, buildWebMcpToolDefinition, buildWebMcpToolDefinitions, buildWebRelationships, buildWebToolDescriptors, compareText, computeWebManifestHash, findManifestObjectByName, generateWebModule, isCollectionManifestClass, isStiChildModel, orderedManifestObjectEntries, resolveCollectionItemObject, resolveCollectionItemTypeName, selectWebCollectionEntries, selectWebMcpToolEntries };
764
764
 
765
765
  //# sourceMappingURL=web-collections.js.map