@happyvertical/smrt-core 0.55.0 → 0.55.2

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 (73) hide show
  1. package/AGENTS.md +8 -0
  2. package/agents/build-knowledge.md +42 -0
  3. package/agents/generators.md +13 -0
  4. package/agents/recipes.md +150 -0
  5. package/dist/browser.d.ts +3 -0
  6. package/dist/browser.d.ts.map +1 -1
  7. package/dist/browser.js +4 -1
  8. package/dist/generators/mcp.d.ts.map +1 -1
  9. package/dist/generators/mcp.js +10 -6
  10. package/dist/generators/mcp.js.map +1 -1
  11. package/dist/generators/swagger.d.ts.map +1 -1
  12. package/dist/generators/swagger.js +3 -0
  13. package/dist/generators/swagger.js.map +1 -1
  14. package/dist/generators/tool-schema.d.ts +9 -0
  15. package/dist/generators/tool-schema.d.ts.map +1 -1
  16. package/dist/generators/tool-schema.js +10 -7
  17. package/dist/generators/tool-schema.js.map +1 -1
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +4 -1
  21. package/dist/knowledge.d.ts.map +1 -1
  22. package/dist/knowledge.js +13 -2
  23. package/dist/knowledge.js.map +1 -1
  24. package/dist/manifest/generator.d.ts.map +1 -1
  25. package/dist/manifest/generator.js +3 -1
  26. package/dist/manifest/generator.js.map +1 -1
  27. package/dist/manifest/static-manifest.js +1 -1
  28. package/dist/manifest/static-manifest.js.map +1 -1
  29. package/dist/manifest/store.js +1 -1
  30. package/dist/manifest.json +1 -1
  31. package/dist/object.d.ts.map +1 -1
  32. package/dist/object.js +9 -3
  33. package/dist/object.js.map +1 -1
  34. package/dist/prebuild/index.d.ts.map +1 -1
  35. package/dist/prebuild/index.js +1 -0
  36. package/dist/prebuild/index.js.map +1 -1
  37. package/dist/recipe-help.d.ts +186 -0
  38. package/dist/recipe-help.d.ts.map +1 -0
  39. package/dist/recipe-help.js +311 -0
  40. package/dist/recipe-help.js.map +1 -0
  41. package/dist/recipe.d.ts +83 -0
  42. package/dist/recipe.d.ts.map +1 -0
  43. package/dist/recipe.js +30 -0
  44. package/dist/recipe.js.map +1 -0
  45. package/dist/registry/manifest-field-merge.d.ts +6 -0
  46. package/dist/registry/manifest-field-merge.d.ts.map +1 -1
  47. package/dist/registry/manifest-field-merge.js +12 -1
  48. package/dist/registry/manifest-field-merge.js.map +1 -1
  49. package/dist/registry/types.d.ts +17 -0
  50. package/dist/registry/types.d.ts.map +1 -1
  51. package/dist/scanner/index.d.ts +1 -1
  52. package/dist/scanner/index.d.ts.map +1 -1
  53. package/dist/scanner/manifest-generator.d.ts +33 -0
  54. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  55. package/dist/scanner/manifest-generator.js +124 -0
  56. package/dist/scanner/manifest-generator.js.map +1 -1
  57. package/dist/scanner/types.d.ts +47 -0
  58. package/dist/scanner/types.d.ts.map +1 -1
  59. package/dist/scanner/types.js.map +1 -1
  60. package/dist/smrt-knowledge.json +16 -9
  61. package/dist/ui-metadata.d.ts +60 -0
  62. package/dist/ui-metadata.d.ts.map +1 -0
  63. package/dist/ui-metadata.js +85 -0
  64. package/dist/ui-metadata.js.map +1 -0
  65. package/dist/utils/scanner-module.d.ts +18 -1
  66. package/dist/utils/scanner-module.d.ts.map +1 -1
  67. package/dist/vite-plugin/index.d.ts.map +1 -1
  68. package/dist/vite-plugin/index.js +4 -1
  69. package/dist/vite-plugin/index.js.map +1 -1
  70. package/dist/vite-plugin/web-collections.d.ts.map +1 -1
  71. package/dist/vite-plugin/web-collections.js +8 -3
  72. package/dist/vite-plugin/web-collections.js.map +1 -1
  73. package/package.json +15 -10
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/prebuild/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAkB,CAAC;AAW5D,MAAM,WAAW,eAAe;IAC9B,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAAC;IACvC,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,sCAAsC;IACtC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,IAAI,CAAC,CAkCf;AA+ZD;;GAEG;AACH,wBAAsB,2BAA2B,CAC/C,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,IAAI,CAAC,CAkBf"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/prebuild/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAkB,CAAC;AAW5D,MAAM,WAAW,eAAe;IAC9B,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAAC;IACvC,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,sCAAsC;IACtC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,IAAI,CAAC,CAkCf;AAgaD;;GAEG;AACH,wBAAsB,2BAA2B,CAC/C,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,IAAI,CAAC,CAkBf"}
@@ -225,6 +225,7 @@ declare module '@smrt/web' {
225
225
  group?: string;
226
226
  order?: number;
227
227
  locked?: boolean;
228
+ widget?: 'textarea' | 'currency' | 'email' | 'url' | 'phone';
228
229
  }
229
230
 
230
231
  export interface SmrtWebFieldDefinition {
@@ -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 }\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":";;;;;;;;;;;;AAkCA,eAAsB,qBACpB,SACe;CACf,MAAM,EACJ,UAAU,eACV,QACA,wBAAwB,MACxB,qBAAqB,MACrB,cAAc,QAAQ,IAAI,MACxB;CAGJ,MAAM,WACJ,OAAO,kBAAkB,WACrB,KAAK,MAAM,GAAG,aAAa,eAAe,OAAO,CAAC,IAClD;CAGN,MAAM,aAAa,KAAK,WAAW,MAAM,IACrC,SACA,KAAK,KAAK,aAAa,MAAM;CACjC,GAAG,UAAU,YAAY,EAAE,WAAW,KAAK,CAAC;CAE5C,QAAQ,IAAI,8CAA8C,YAAY;CAEtE,IAAI,oBACF,MAAM,+BAA+B,UAAU,UAAU;CAG3D,IAAI,uBACF,MAAM,kCAAkC,UAAU,UAAU;CAG9D,QAAQ,IACN,8CAA8C,OAAO,KAAK,SAAS,OAAO,CAAC,CAAC,OAAO,cACrF;AACF;;;;AAKA,eAAe,+BACb,UACA,QACe;CACf,MAAM,aAAuB,CAAC;CAK9B,MAAM,sBAAsB,6BAA6B,QAAQ;CACjE,KAAK,MAAM,CAAC,aAAa,eAAe,qBAAqB;EAC3D,MAAM,SAAS,WAAW,UAAU,CAAC;EACrC,MAAM,gBAA0B,CAAC;EAGjC,cAAc,KAAK,gBAAgB;EACnC,cAAc,KAAK,wBAAwB;EAC3C,cAAc,KAAK,wBAAwB;EAG3C,KAAK,MAAM,CAAC,WAAW,aAAa,OAAO,QAAQ,MAAM,GAAG;GAC1D,MAAM,OAAO,yBAAyB,SAAS,IAAI;GACnD,MAAM,WAAW,CAAC,SAAS,WAAW,MAAM;GAC5C,cAAc,KAAK,KAAK,YAAY,SAAS,IAAI,KAAK,EAAE;EAC1D;EAEA,MAAM,eAAe,oBAAoB,WAAW,UAAU;EAChE,cAAc,KAAK,IAAI,EAAE;;EAEvB,WAAW,KAAK,YAAY;CAC9B;CAGA,MAAM,qBAAqB;;;;;;;EAO3B,WAAW,KAAK,MAAM,EAAE;;CAGxB,GAAG,cAAc,KAAK,KAAK,QAAQ,mBAAmB,GAAG,kBAAkB;AAC7E;;;;AAKA,eAAe,kCACb,UACA,QACe;CAEf,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8E5B,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAhCC,uBAAuB,QAAQ,CAAC,CACxD,KAAK,EAAE,WAAW,mBAAmB,aAAa,oBAAoB;EAErE,MAAM,WAAW,wBACf,mBACA,aAHwB,cAAc,KAAK,EAAE,WAAW,IAIxD,CACF;EACA,MAAM,mBAAmB,cACtB,KACE,WACC,SAAS,OAAO,KAAK,GAAG,sCACtB,QACA,wBACF,EAAE,iBACN,CAAC,CACA,KAAK,IAAI;EAEZ,IAAI,kBAAkB;GACpB,MAAM,eAAe,WAAW,GAAG,SAAS,OAAO;GACnD,OAAO,OAAO,KAAK,UAAU,SAAS,EAAE,IAAI,aAAa,KAAK,iBAAiB;EACjF;EAEA,OAAO,OAAO,KAAK,UAAU,SAAS,EAAE,IAAI,YAAY,wBAAwB;CAClF,CAAC,CAAC,CACD,KAAK,IA4CR,EAAmB;;;;;;CAQnB,MAAM,oBAAoB;;;;;;;;;;;;;;CAgB1B,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;CAgCvB,MAAM,mBAAmB;;;;EAPH,6BAA6B,QAAQ,CAAC,CACzD,KACE,GAAG,SACF,iBAAiB,IAAI,UAAU,kCAAkC,IAAI,UAAU,MACnF,CAAC,CACA,KAAK,IAMR,EAAc;;;;;;;;;;;;;CAyBd,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAPM,2BAA2B,QAAQ,CAAC,CAC9D,KACE,EAAE,YAAY,UACb,OAAO,KAAK,UAAU,UAAU,EAAE,yDAAyD,IAAI,UAAU,OAC7G,CAAC,CACA,KAAK,IAuGR,EAAqB;;;;;;;;;;;;;;;;;;;CAqBrB,GAAG,cACD,KAAK,KAAK,QAAQ,oBAAoB,GACtC,mBACF;CACA,GAAG,cAAc,KAAK,KAAK,QAAQ,kBAAkB,GAAG,iBAAiB;CACzE,GAAG,cAAc,KAAK,KAAK,QAAQ,kBAAkB,GAAG,iBAAiB;CACzE,GAAG,cAAc,KAAK,KAAK,QAAQ,eAAe,GAAG,cAAc;CACnE,GAAG,cAAc,KAAK,KAAK,QAAQ,iBAAiB,GAAG,gBAAgB;CACvE,GAAG,cAAc,KAAK,KAAK,QAAQ,eAAe,GAAG,cAAc;AACrE;;;;AAKA,SAAS,yBAAyB,UAA0B;CAC1D,QAAQ,UAAR;EACE,KAAK,QACH,OAAO;EACT,KAAK;EACL,KAAK,WACH,OAAO;EACT,KAAK,WACH,OAAO;EACT,KAAK,YACH,OAAO;EACT,KAAK,QACH,OAAO;EACT,KAAK,cACH,OAAO;EACT,SACE,OAAO;CACX;AACF;;;;AAKA,eAAsB,4BACpB,MACe;CACf,MAAM,eAAe,KAAK;CAC1B,MAAM,SAAS,KAAK,MAAM;CAE1B,IAAI,CAAC,cAAc;EACjB,QAAQ,MAAM,2DAA2D;EACzE,QAAQ,KAAK,CAAC;CAChB;CAEA,IAAI,CAAC,GAAG,WAAW,YAAY,GAAG;EAChC,QAAQ,MAAM,4BAA4B,cAAc;EACxD,QAAQ,KAAK,CAAC;CAChB;CAEA,MAAM,qBAAqB;EACzB,UAAU;EACV;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/prebuild/index.ts"],"sourcesContent":["/**\n * Pre-build utilities for generating TypeScript declarations\n * Solves virtual module resolution by creating physical .d.ts files\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport type { SmartObjectManifest } from '../scanner/types';\nimport {\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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"}
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Recipe help (#3591): the pure, browser-safe helper that turns a recipe's
3
+ * `help` into something a host can render, following how the app configured
4
+ * the recipe's fields.
5
+ *
6
+ * Contract. A recipe entry carries `help: { markdown, fieldRefs }`. The
7
+ * Markdown is user-facing prose: an overview and tasks. A step names a field as
8
+ * `{field:name}` or `{field:Model.name}`, and the Markdown block that contains a
9
+ * reference (a heading, a paragraph or a list item) is tied to that field.
10
+ * `fieldRefs` is derived from the Markdown at build time, never written by hand.
11
+ *
12
+ * Rendering. {@link renderHelp} replaces each reference with the field's
13
+ * effective label, drops the blocks tied to a field the app does not show
14
+ * (hidden, disabled, advanced-and-not-enabled, or unknown), prunes a heading
15
+ * whose whole section was dropped, builds the glossary from the effective label
16
+ * and help (falling back to the `@field({ description })`), and lists the
17
+ * recipe's generated REST, MCP and CLI surfaces last as "Connect other tools".
18
+ *
19
+ * The result is an AST of text nodes, never HTML: no raw markup from help
20
+ * content can reach a page, so nothing needs sanitizing. {@link helpToMarkdown}
21
+ * flattens the same result to Markdown for hosts that prefer their own
22
+ * renderer. Only a small Markdown subset is understood: `##`/`###`/`####`
23
+ * headings, paragraphs, flat `-`/`*`/`1.` lists, and `**bold**`, `*italic*` and
24
+ * `` `code` `` inline. Anything else is plain text.
25
+ *
26
+ * @packageDocumentation
27
+ */
28
+ import type { RecipeExposureNarrowing, RecipeHelp } from '@happyvertical/smrt-types';
29
+ export type { RecipeHelp };
30
+ export type Inline = {
31
+ type: 'text';
32
+ text: string;
33
+ } | {
34
+ type: 'code';
35
+ text: string;
36
+ } | {
37
+ type: 'strong';
38
+ children: Inline[];
39
+ } | {
40
+ type: 'em';
41
+ children: Inline[];
42
+ }
43
+ /** Present only before resolution; {@link renderHelp} replaces it. */
44
+ | {
45
+ type: 'field';
46
+ ref: string;
47
+ };
48
+ export interface HelpHeading {
49
+ type: 'heading';
50
+ level: 2 | 3 | 4;
51
+ inlines: Inline[];
52
+ }
53
+ export interface HelpParagraph {
54
+ type: 'paragraph';
55
+ inlines: Inline[];
56
+ }
57
+ export interface HelpItem {
58
+ inlines: Inline[];
59
+ }
60
+ export interface HelpList {
61
+ type: 'list';
62
+ ordered: boolean;
63
+ items: HelpItem[];
64
+ }
65
+ export type HelpBlock = HelpHeading | HelpParagraph | HelpList;
66
+ /** Parse the supported Markdown subset into blocks. */
67
+ export declare function parseHelp(markdown: string): HelpBlock[];
68
+ /** Every distinct field reference in the Markdown, as written, sorted. */
69
+ export declare function extractFieldRefs(markdown: string): string[];
70
+ /** Build a contract entry from Markdown, deriving `fieldRefs`. */
71
+ export declare function createRecipeHelp(markdown: string): RecipeHelp;
72
+ /** A field as the app's options leave it: smrt-fields vocabulary. */
73
+ export interface HelpField {
74
+ name: string;
75
+ /** Effective label. */
76
+ label: string;
77
+ /** Effective help; empty or absent falls back to the description. */
78
+ help?: string | null;
79
+ /**
80
+ * `basic` fields are shown. `hidden` fields are not. `advanced` fields are
81
+ * treated like hidden unless {@link RenderHelpOptions.showAdvanced} is set.
82
+ */
83
+ visibility: 'basic' | 'advanced' | 'hidden';
84
+ /** A disabled field is never shown, whatever its visibility. */
85
+ disabled?: boolean;
86
+ }
87
+ export interface HelpModel {
88
+ /** Qualified name, `@scope/pkg:Class`. */
89
+ id: string;
90
+ /** Class name, what `{field:Model.name}` uses. */
91
+ name: string;
92
+ /** In display order. */
93
+ fields: readonly HelpField[];
94
+ /** Field descriptions (`@field({ description })`), keyed by field name. */
95
+ descriptions?: Readonly<Record<string, string>>;
96
+ }
97
+ export interface GlossaryEntry {
98
+ model: string;
99
+ name: string;
100
+ label: string;
101
+ text: string;
102
+ }
103
+ /** One generated REST route, MCP tool or CLI command of a recipe model. */
104
+ export interface ConnectSurface {
105
+ /** Class name of the model it belongs to. */
106
+ model: string;
107
+ kind: 'api' | 'mcp' | 'cli';
108
+ /** The operation: a CRUD verb or a method name, e.g. `list`. */
109
+ operation: string;
110
+ /** Tool or command name. */
111
+ name: string;
112
+ /** REST route, when the surface has one. */
113
+ path?: string;
114
+ /** HTTP method, when the surface has one. */
115
+ method?: string;
116
+ }
117
+ export interface RenderHelpOptions {
118
+ /** Treat `advanced` fields as shown; by default they are dropped like hidden ones. */
119
+ showAdvanced?: boolean;
120
+ /** The recipe's generated surfaces, for the "Connect other tools" section. */
121
+ surfaces?: readonly ConnectSurface[];
122
+ /**
123
+ * The recipe's `options.<Model>.exposure` narrowing, keyed by class name:
124
+ * `false` withdraws a transport, `{ exclude }` withdraws named operations.
125
+ */
126
+ exposure?: Readonly<Record<string, Partial<Record<'api' | 'mcp' | 'cli', RecipeExposureNarrowing>>>>;
127
+ }
128
+ /** The developer-facing surfaces, listed last and collapsed by default. */
129
+ export interface ConnectSection {
130
+ title: string;
131
+ /** Hosts render this section collapsed. */
132
+ collapsed: true;
133
+ groups: Array<{
134
+ kind: 'api' | 'mcp' | 'cli';
135
+ label: string;
136
+ entries: Array<{
137
+ model: string;
138
+ text: string;
139
+ }>;
140
+ }>;
141
+ }
142
+ export interface RenderedHelp {
143
+ blocks: HelpBlock[];
144
+ /**
145
+ * One entry per SHOWN field that has text. A shown field with neither
146
+ * effective help nor a description is skipped, not listed with a blank.
147
+ */
148
+ glossary: GlossaryEntry[];
149
+ /** Present only when the recipe has generated surfaces left after narrowing. */
150
+ connect?: ConnectSection;
151
+ }
152
+ /** Find a reference's field: `name` in the first model declaring it, or `Model.name`. */
153
+ export declare function findField(ref: string, models: readonly HelpModel[]): {
154
+ model: HelpModel;
155
+ field: HelpField;
156
+ } | undefined;
157
+ /**
158
+ * Problems that should fail a build: no Markdown, references to fields the
159
+ * recipe's models do not declare, and a `fieldRefs` list that disagrees with
160
+ * the Markdown. An empty list means the help is valid.
161
+ */
162
+ export declare function validateHelp(help: RecipeHelp, models: readonly HelpModel[]): string[];
163
+ /** Resolve the Markdown against the app's options. */
164
+ export declare function resolveHelp(help: RecipeHelp, models: readonly HelpModel[], options?: RenderHelpOptions): HelpBlock[];
165
+ /**
166
+ * One entry per shown field: effective label, then effective help or, failing
167
+ * that, the field's `@field({ description })`. A shown field with neither is
168
+ * skipped rather than listed with an empty definition.
169
+ */
170
+ export declare function buildGlossary(models: readonly HelpModel[], options?: RenderHelpOptions): GlossaryEntry[];
171
+ /**
172
+ * The generated REST routes, MCP tools and CLI commands left after the
173
+ * recipe's exposure narrowing, grouped by transport. `undefined` when none.
174
+ */
175
+ export declare function buildConnectSection(options?: RenderHelpOptions): ConnectSection | undefined;
176
+ /** The whole renderer: resolved blocks, the field glossary and the connect section. */
177
+ export declare function renderHelp(help: RecipeHelp, models: readonly HelpModel[], options?: RenderHelpOptions): RenderedHelp;
178
+ /**
179
+ * Flatten a rendered help to Markdown, for hosts that prefer their own
180
+ * Markdown renderer. The glossary and the connect section follow the blocks as
181
+ * ordinary headings and lists; collapsing is left to the host. Text is carried
182
+ * verbatim, so a host must render the Markdown with raw HTML disabled; prefer
183
+ * the AST when the output goes to a page.
184
+ */
185
+ export declare function helpToMarkdown(rendered: RenderedHelp): string;
186
+ //# sourceMappingURL=recipe-help.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recipe-help.d.ts","sourceRoot":"","sources":["../src/recipe-help.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,311 @@
1
+ //#region src/recipe-help.ts
2
+ var FIELD_REF = /\{field:([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?)\}/;
3
+ var INLINE = /\{field:[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?\}|`[^`]+`|\*\*[^*]+\*\*|\*[^*\s][^*]*\*/;
4
+ function parseInline(source) {
5
+ const out = [];
6
+ let rest = source;
7
+ while (rest) {
8
+ const match = INLINE.exec(rest);
9
+ if (!match) break;
10
+ if (match.index > 0) out.push({
11
+ type: "text",
12
+ text: rest.slice(0, match.index)
13
+ });
14
+ const token = match[0];
15
+ const ref = FIELD_REF.exec(token);
16
+ if (ref && ref[0] === token) out.push({
17
+ type: "field",
18
+ ref: ref[1]
19
+ });
20
+ else if (token.startsWith("`")) out.push({
21
+ type: "code",
22
+ text: token.slice(1, -1)
23
+ });
24
+ else if (token.startsWith("**")) out.push({
25
+ type: "strong",
26
+ children: parseInline(token.slice(2, -2))
27
+ });
28
+ else out.push({
29
+ type: "em",
30
+ children: parseInline(token.slice(1, -1))
31
+ });
32
+ rest = rest.slice(match.index + token.length);
33
+ }
34
+ if (rest) out.push({
35
+ type: "text",
36
+ text: rest
37
+ });
38
+ return out;
39
+ }
40
+ var HEADING = /^(#{2,4})\s+(.+?)\s*#*\s*$/;
41
+ var BULLET = /^[-*]\s+(.*)$/;
42
+ var NUMBERED = /^\d+[.)]\s+(.*)$/;
43
+ /** Parse the supported Markdown subset into blocks. */
44
+ function parseHelp(markdown) {
45
+ const blocks = [];
46
+ let paragraph = [];
47
+ let list = null;
48
+ const flushParagraph = () => {
49
+ if (paragraph.length) {
50
+ blocks.push({
51
+ type: "paragraph",
52
+ inlines: parseInline(paragraph.join(" "))
53
+ });
54
+ paragraph = [];
55
+ }
56
+ };
57
+ const flushList = () => {
58
+ if (list) {
59
+ blocks.push({
60
+ type: "list",
61
+ ordered: list.ordered,
62
+ items: list.items.map((item) => ({ inlines: parseInline(item) }))
63
+ });
64
+ list = null;
65
+ }
66
+ };
67
+ for (const raw of markdown.replace(/\r\n?/g, "\n").split("\n")) {
68
+ const trimmed = raw.trimEnd().trim();
69
+ if (!trimmed) {
70
+ flushParagraph();
71
+ flushList();
72
+ continue;
73
+ }
74
+ const heading = HEADING.exec(trimmed);
75
+ if (heading) {
76
+ flushParagraph();
77
+ flushList();
78
+ blocks.push({
79
+ type: "heading",
80
+ level: heading[1].length,
81
+ inlines: parseInline(heading[2])
82
+ });
83
+ continue;
84
+ }
85
+ const bullet = BULLET.exec(trimmed);
86
+ const numbered = bullet ? null : NUMBERED.exec(trimmed);
87
+ const marker = bullet ?? numbered;
88
+ if (marker && raw === raw.trimStart()) {
89
+ flushParagraph();
90
+ const ordered = numbered !== null;
91
+ if (list && list.ordered !== ordered) flushList();
92
+ if (!list) list = {
93
+ ordered,
94
+ items: []
95
+ };
96
+ list.items.push(marker[1]);
97
+ continue;
98
+ }
99
+ if (list) list.items[list.items.length - 1] += ` ${trimmed}`;
100
+ else paragraph.push(trimmed);
101
+ }
102
+ flushParagraph();
103
+ flushList();
104
+ return blocks;
105
+ }
106
+ function refsOf(inlines) {
107
+ return inlines.flatMap((node) => node.type === "field" ? [node.ref] : node.type === "strong" || node.type === "em" ? refsOf(node.children) : []);
108
+ }
109
+ /** Every distinct field reference in the Markdown, as written, sorted. */
110
+ function extractFieldRefs(markdown) {
111
+ const refs = parseHelp(markdown).flatMap((block) => block.type === "list" ? block.items.flatMap((item) => refsOf(item.inlines)) : refsOf(block.inlines));
112
+ return [...new Set(refs)].sort();
113
+ }
114
+ /** Build a contract entry from Markdown, deriving `fieldRefs`. */
115
+ function createRecipeHelp(markdown) {
116
+ return {
117
+ markdown,
118
+ fieldRefs: extractFieldRefs(markdown)
119
+ };
120
+ }
121
+ /** Find a reference's field: `name` in the first model declaring it, or `Model.name`. */
122
+ function findField(ref, models) {
123
+ const dot = ref.indexOf(".");
124
+ const modelName = dot === -1 ? null : ref.slice(0, dot);
125
+ const fieldName = dot === -1 ? ref : ref.slice(dot + 1);
126
+ for (const model of models) {
127
+ if (modelName !== null && model.name !== modelName) continue;
128
+ const field = model.fields.find((f) => f.name === fieldName);
129
+ if (field) return {
130
+ model,
131
+ field
132
+ };
133
+ }
134
+ }
135
+ /**
136
+ * Problems that should fail a build: no Markdown, references to fields the
137
+ * recipe's models do not declare, and a `fieldRefs` list that disagrees with
138
+ * the Markdown. An empty list means the help is valid.
139
+ */
140
+ function validateHelp(help, models) {
141
+ const problems = [];
142
+ if (!help.markdown.trim()) problems.push("help has no markdown");
143
+ const actual = extractFieldRefs(help.markdown);
144
+ for (const ref of actual) if (!findField(ref, models)) problems.push(`{field:${ref}} names a field the recipe's models lack`);
145
+ if (JSON.stringify([...help.fieldRefs].sort()) !== JSON.stringify(actual)) problems.push(`fieldRefs does not match the references in the markdown (markdown: [${actual.join(", ")}]; fieldRefs: [${[...help.fieldRefs].sort().join(", ")}]); an unbalanced backtick across lines is a common cause`);
146
+ return problems;
147
+ }
148
+ function isShown(field, options) {
149
+ if (field.disabled === true) return false;
150
+ return field.visibility === "basic" || field.visibility === "advanced" && options.showAdvanced === true;
151
+ }
152
+ /** Replace references with labels, or `null` when any is not shown. */
153
+ function resolveInlines(inlines, models, options) {
154
+ const out = [];
155
+ for (const node of inlines) if (node.type === "field") {
156
+ const found = findField(node.ref, models);
157
+ if (!found || !isShown(found.field, options)) return null;
158
+ out.push({
159
+ type: "text",
160
+ text: found.field.label
161
+ });
162
+ } else if (node.type === "strong" || node.type === "em") {
163
+ const inner = resolveInlines(node.children, models, options);
164
+ if (!inner) return null;
165
+ out.push({
166
+ type: node.type,
167
+ children: inner.inlines
168
+ });
169
+ } else out.push(node);
170
+ return { inlines: out };
171
+ }
172
+ /**
173
+ * Does the section under the heading at `index` hold any non-heading block?
174
+ * With `survivors`, only blocks that were kept count.
175
+ */
176
+ function sectionHasBody(entries, index, survivors) {
177
+ const heading = entries[index]?.source;
178
+ for (let i = index + 1; i < entries.length; i++) {
179
+ const next = entries[i];
180
+ if (next.source.type === "heading") {
181
+ if (next.source.level <= heading.level) return false;
182
+ continue;
183
+ }
184
+ if (!survivors || next.kept) return true;
185
+ }
186
+ return false;
187
+ }
188
+ /** Resolve the Markdown against the app's options. */
189
+ function resolveHelp(help, models, options = {}) {
190
+ const entries = parseHelp(help.markdown).map((source) => {
191
+ if (source.type === "list") {
192
+ const items = source.items.flatMap((item) => {
193
+ const resolved = resolveInlines(item.inlines, models, options);
194
+ return resolved ? [{ inlines: resolved.inlines }] : [];
195
+ });
196
+ return {
197
+ source,
198
+ kept: items.length ? {
199
+ ...source,
200
+ items
201
+ } : null
202
+ };
203
+ }
204
+ const resolved = resolveInlines(source.inlines, models, options);
205
+ return {
206
+ source,
207
+ kept: resolved ? {
208
+ ...source,
209
+ inlines: resolved.inlines
210
+ } : null
211
+ };
212
+ });
213
+ return entries.flatMap((entry, index) => {
214
+ if (!entry.kept) return [];
215
+ if (entry.source.type === "heading" && sectionHasBody(entries, index, false) && !sectionHasBody(entries, index, true)) return [];
216
+ return [entry.kept];
217
+ });
218
+ }
219
+ /**
220
+ * One entry per shown field: effective label, then effective help or, failing
221
+ * that, the field's `@field({ description })`. A shown field with neither is
222
+ * skipped rather than listed with an empty definition.
223
+ */
224
+ function buildGlossary(models, options = {}) {
225
+ return models.flatMap((model) => model.fields.flatMap((field) => {
226
+ if (!isShown(field, options)) return [];
227
+ const text = field.help?.trim() || model.descriptions?.[field.name] || "";
228
+ return text ? [{
229
+ model: model.name,
230
+ name: field.name,
231
+ label: field.label,
232
+ text
233
+ }] : [];
234
+ }));
235
+ }
236
+ var CONNECT_KINDS = [
237
+ ["api", "REST routes"],
238
+ ["mcp", "MCP tools"],
239
+ ["cli", "CLI commands"]
240
+ ];
241
+ function isWithdrawn(surface, options) {
242
+ const narrowing = options.exposure?.[surface.model]?.[surface.kind];
243
+ if (narrowing === false) return true;
244
+ return typeof narrowing === "object" && narrowing !== null && narrowing.exclude.includes(surface.operation);
245
+ }
246
+ /**
247
+ * The generated REST routes, MCP tools and CLI commands left after the
248
+ * recipe's exposure narrowing, grouped by transport. `undefined` when none.
249
+ */
250
+ function buildConnectSection(options = {}) {
251
+ const surfaces = (options.surfaces ?? []).filter((surface) => !isWithdrawn(surface, options));
252
+ const groups = CONNECT_KINDS.flatMap(([kind, label]) => {
253
+ const entries = surfaces.filter((surface) => surface.kind === kind).map((surface) => ({
254
+ model: surface.model,
255
+ text: surface.method && surface.path ? `${surface.method.toUpperCase()} ${surface.path}` : surface.name
256
+ }));
257
+ return entries.length ? [{
258
+ kind,
259
+ label,
260
+ entries
261
+ }] : [];
262
+ });
263
+ return groups.length ? {
264
+ title: "Connect other tools",
265
+ collapsed: true,
266
+ groups
267
+ } : void 0;
268
+ }
269
+ /** The whole renderer: resolved blocks, the field glossary and the connect section. */
270
+ function renderHelp(help, models, options = {}) {
271
+ const connect = buildConnectSection(options);
272
+ return {
273
+ blocks: resolveHelp(help, models, options),
274
+ glossary: buildGlossary(models, options),
275
+ ...connect ? { connect } : {}
276
+ };
277
+ }
278
+ function inlinesToMarkdown(inlines) {
279
+ return inlines.map((node) => {
280
+ switch (node.type) {
281
+ case "text": return node.text;
282
+ case "code": return `\`${node.text}\``;
283
+ case "strong": return `**${inlinesToMarkdown(node.children)}**`;
284
+ case "em": return `*${inlinesToMarkdown(node.children)}*`;
285
+ default: return node.ref;
286
+ }
287
+ }).join("");
288
+ }
289
+ /**
290
+ * Flatten a rendered help to Markdown, for hosts that prefer their own
291
+ * Markdown renderer. The glossary and the connect section follow the blocks as
292
+ * ordinary headings and lists; collapsing is left to the host. Text is carried
293
+ * verbatim, so a host must render the Markdown with raw HTML disabled; prefer
294
+ * the AST when the output goes to a page.
295
+ */
296
+ function helpToMarkdown(rendered) {
297
+ const parts = [];
298
+ for (const block of rendered.blocks) if (block.type === "heading") parts.push(`${"#".repeat(block.level)} ${inlinesToMarkdown(block.inlines)}`);
299
+ else if (block.type === "paragraph") parts.push(inlinesToMarkdown(block.inlines));
300
+ else parts.push(block.items.map((item, index) => `${block.ordered ? `${index + 1}.` : "-"} ${inlinesToMarkdown(item.inlines)}`).join("\n"));
301
+ if (rendered.glossary.length > 0) parts.push(`## Fields\n\n${rendered.glossary.map((entry) => `- **${entry.label}**: ${entry.text}`).join("\n")}`);
302
+ if (rendered.connect) {
303
+ const groups = rendered.connect.groups.map((group) => `### ${group.label}\n\n${group.entries.map((entry) => `- ${entry.model}: \`${entry.text}\``).join("\n")}`).join("\n\n");
304
+ parts.push(`## ${rendered.connect.title}\n\n${groups}`);
305
+ }
306
+ return `${parts.join("\n\n")}\n`;
307
+ }
308
+ //#endregion
309
+ export { buildConnectSection, buildGlossary, createRecipeHelp, extractFieldRefs, findField, helpToMarkdown, parseHelp, renderHelp, resolveHelp, validateHelp };
310
+
311
+ //# sourceMappingURL=recipe-help.js.map