Created
March 4, 2025 13:40
-
-
Save mfpiccolo/a273133302d168925d23376e199abec2 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"rules": [ | |
// File Structure Rules | |
{ | |
"name": "enforce-step-file-naming", | |
"description": "Ensure step files follow the *.step.{ts,js,py,rb} naming convention", | |
"pattern": "\\b(?!.*\\.step\\.(ts|js|py|rb)$).*step\\b", | |
"severity": "error", | |
"message": "Step files must follow the pattern: filename.step.{ts|js|py|rb}", | |
"example": { | |
"correct": "processPayment.step.ts", | |
"incorrect": "processPayment.ts or processPaymentStep.ts" | |
} | |
}, | |
{ | |
"name": "enforce-ui-step-file-naming", | |
"description": "Ensure UI step files match their corresponding step files", | |
"pattern": "\\b(?!.*\\.step\\.(tsx|jsx)$).*\\.step\\.(tsx|jsx)\\b", | |
"severity": "error", | |
"message": "UI step files must follow the pattern: filename.step.{tsx|jsx} and match a corresponding step file", | |
"example": { | |
"correct": "processPayment.step.tsx (with matching processPayment.step.ts)", | |
"incorrect": "processPaymentUI.tsx or processPayment.tsx" | |
} | |
}, | |
// Step Configuration Rules | |
{ | |
"name": "missing-step-config", | |
"description": "Check for missing config in step files", | |
"pattern": "\\b(?:export\\s+const\\s+config|exports\\.config|def\\s+config|config\\s+=)\\b", | |
"inverse": true, | |
"filePattern": ".*\\.step\\.(ts|js|py|rb)$", | |
"severity": "error", | |
"message": "Step files must export a config object", | |
"example": { | |
"typescript": "export const config: EventConfig = { ... }", | |
"javascript": "exports.config = { ... }", | |
"python": "config = { ... }", | |
"ruby": "def config\n { ... }\nend" | |
} | |
}, | |
{ | |
"name": "missing-step-handler", | |
"description": "Check for missing handler in step files", | |
"pattern": "\\b(?:export\\s+const\\s+handler|exports\\.handler|def\\s+handler|async\\s+def\\s+handler)\\b", | |
"inverse": true, | |
"filePattern": ".*\\.step\\.(ts|js|py|rb)$", | |
"severity": "error", | |
"message": "Step files must export a handler function", | |
"example": { | |
"typescript": "export const handler: StepHandler<typeof config> = async (input, { emit }) => { ... }", | |
"javascript": "exports.handler = async (input, { emit }) => { ... }", | |
"python": "async def handler(input, ctx):\n # ...", | |
"ruby": "def handler(input, ctx)\n # ...\nend" | |
} | |
}, | |
{ | |
"name": "missing-step-type", | |
"description": "Check for missing type in step config", | |
"pattern": "\\b(?:type:\\s*['\"](?:event|api|cron|noop)['\"])\\b", | |
"inverse": true, | |
"filePattern": ".*\\.step\\.(ts|js)$", | |
"severity": "error", | |
"message": "Step config must specify a type (event, api, cron, or noop)", | |
"example": { | |
"correct": "type: 'event', // or 'api', 'cron', 'noop'", | |
"incorrect": "Missing type property" | |
} | |
}, | |
{ | |
"name": "undefined-flow", | |
"description": "Check for undefined flows in step config", | |
"pattern": "\\bflows:\\s*\\[\\s*\\]", | |
"filePattern": ".*\\.step\\.(ts|js|py|rb)$", | |
"severity": "warning", | |
"message": "Step has empty flows array. Steps should belong to at least one flow for proper visualization.", | |
"example": { | |
"correct": "flows: ['payment-processing']", | |
"incorrect": "flows: []" | |
} | |
}, | |
// Event Step Rules | |
{ | |
"name": "event-step-subscribes", | |
"description": "Check that event steps have subscribes", | |
"pattern": "\\btype:\\s*['\"]event['\"].*?subscribes:\\s*\\[", | |
"inverse": true, | |
"filePattern": ".*\\.step\\.(ts|js)$", | |
"severity": "warning", | |
"message": "Event steps should have at least one topic they subscribe to", | |
"example": { | |
"correct": "type: 'event',\nsubscribes: ['payment.initiated']", | |
"incorrect": "type: 'event',\n// missing subscribes property" | |
} | |
}, | |
{ | |
"name": "event-step-emits", | |
"description": "Check that event steps have emits", | |
"pattern": "\\btype:\\s*['\"]event['\"].*?emits:\\s*\\[", | |
"inverse": true, | |
"filePattern": ".*\\.step\\.(ts|js)$", | |
"severity": "warning", | |
"message": "Event steps should emit at least one topic", | |
"example": { | |
"correct": "type: 'event',\nemits: ['payment.processed']", | |
"incorrect": "type: 'event',\n// missing emits property" | |
} | |
}, | |
// API Step Rules | |
{ | |
"name": "api-step-path", | |
"description": "Check that API steps have a path", | |
"pattern": "\\btype:\\s*['\"]api['\"].*?path:\\s*['\"]", | |
"inverse": true, | |
"filePattern": ".*\\.step\\.(ts|js)$", | |
"severity": "error", | |
"message": "API steps must have a path", | |
"example": { | |
"correct": "type: 'api',\npath: '/api/payments'", | |
"incorrect": "type: 'api',\n// missing path property" | |
} | |
}, | |
{ | |
"name": "api-step-method", | |
"description": "Check that API steps have a method", | |
"pattern": "\\btype:\\s*['\"]api['\"].*?method:\\s*['\"](?:GET|POST)['\"]", | |
"inverse": true, | |
"filePattern": ".*\\.step\\.(ts|js)$", | |
"severity": "error", | |
"message": "API steps must have a method (GET or POST)", | |
"example": { | |
"correct": "type: 'api',\nmethod: 'POST'", | |
"incorrect": "type: 'api',\n// missing method property" | |
} | |
}, | |
{ | |
"name": "api-step-path-starts-with-slash", | |
"description": "Check that API paths start with a slash", | |
"pattern": "\\bpath:\\s*['\"][^/]", | |
"filePattern": ".*\\.step\\.(ts|js)$", | |
"severity": "error", | |
"message": "API paths must start with a slash", | |
"example": { | |
"correct": "path: '/api/payments'", | |
"incorrect": "path: 'api/payments'" | |
} | |
}, | |
// Cron Step Rules | |
{ | |
"name": "cron-step-expression", | |
"description": "Check that cron steps have a cron expression", | |
"pattern": "\\btype:\\s*['\"]cron['\"].*?cron:\\s*['\"]", | |
"inverse": true, | |
"filePattern": ".*\\.step\\.(ts|js)$", | |
"severity": "error", | |
"message": "Cron steps must have a cron expression", | |
"example": { | |
"correct": "type: 'cron',\ncron: '0 * * * *'", | |
"incorrect": "type: 'cron',\n// missing cron property" | |
} | |
}, | |
{ | |
"name": "cron-step-emits", | |
"description": "Check that cron steps have emits", | |
"pattern": "\\btype:\\s*['\"]cron['\"].*?emits:\\s*\\[", | |
"inverse": true, | |
"filePattern": ".*\\.step\\.(ts|js)$", | |
"severity": "error", | |
"message": "Cron steps must emit at least one topic", | |
"example": { | |
"correct": "type: 'cron',\nemits: ['daily.cleanup']", | |
"incorrect": "type: 'cron',\n// missing emits property" | |
} | |
}, | |
// NOOP Step Rules | |
{ | |
"name": "noop-step-virtualemits", | |
"description": "Check that NOOP steps have virtualEmits", | |
"pattern": "\\btype:\\s*['\"]noop['\"].*?virtualEmits:\\s*\\[", | |
"inverse": true, | |
"filePattern": ".*\\.step\\.(ts|js)$", | |
"severity": "error", | |
"message": "NOOP steps must have virtualEmits", | |
"example": { | |
"correct": "type: 'noop',\nvirtualEmits: ['payment.external.processed']", | |
"incorrect": "type: 'noop',\n// missing virtualEmits property" | |
} | |
}, | |
{ | |
"name": "noop-step-virtualsubscribes", | |
"description": "Check that NOOP steps have virtualSubscribes (even if empty)", | |
"pattern": "\\btype:\\s*['\"]noop['\"].*?virtualSubscribes:\\s*\\[", | |
"inverse": true, | |
"filePattern": ".*\\.step\\.(ts|js)$", | |
"severity": "error", | |
"message": "NOOP steps must have virtualSubscribes (even if empty)", | |
"example": { | |
"correct": "type: 'noop',\nvirtualSubscribes: [] // or ['payment.external.initiated']", | |
"incorrect": "type: 'noop',\n// missing virtualSubscribes property" | |
} | |
}, | |
// UI Step Rules | |
{ | |
"name": "ui-step-default-export", | |
"description": "Check that UI step files have a default export", | |
"pattern": "\\bexport\\s+default\\s+", | |
"inverse": true, | |
"filePattern": ".*\\.step\\.(tsx|jsx)$", | |
"severity": "error", | |
"message": "UI step files must have a default export", | |
"example": { | |
"correct": "export default function MyStep() {\n return (\n // ...\n )\n}", | |
"incorrect": "function MyStep() {\n return (\n // ...\n )\n}\n// missing export default" | |
} | |
}, | |
{ | |
"name": "ui-step-basehandle", | |
"description": "Check that UI step files include BaseHandle", | |
"pattern": "\\bBaseHandle\\b", | |
"inverse": true, | |
"filePattern": ".*\\.step\\.(tsx|jsx)$", | |
"severity": "warning", | |
"message": "UI step files should include BaseHandle components for proper flow connections", | |
"example": { | |
"correct": "import { BaseHandle, Position } from 'motia'\n\nexport default function MyStep() {\n return (\n <div>\n <BaseHandle type=\"target\" position={Position.Top} />\n // ...\n <BaseHandle type=\"source\" position={Position.Bottom} />\n </div>\n )\n}", | |
"incorrect": "export default function MyStep() {\n return (\n <div>\n // Missing BaseHandle components\n </div>\n )\n}" | |
} | |
}, | |
// Handler Function Rules | |
{ | |
"name": "missing-emit-import", | |
"description": "Check for missing emit in handler destructuring", | |
"pattern": "\\bhandler.*?=.*?\\{\\s*(?!.*?emit).*?\\}", | |
"filePattern": ".*\\.step\\.(ts|js)$", | |
"severity": "warning", | |
"message": "Handler function should destructure the emit function if it needs to emit events", | |
"example": { | |
"correct": "export const handler = async (input, { emit, logger }) => { ... }", | |
"incorrect": "export const handler = async (input, { logger }) => { ... } // missing emit" | |
} | |
}, | |
{ | |
"name": "missing-logger-import", | |
"description": "Check for missing logger in handler destructuring", | |
"pattern": "\\bhandler.*?=.*?\\{\\s*(?!.*?logger).*?\\}", | |
"filePattern": ".*\\.step\\.(ts|js)$", | |
"severity": "warning", | |
"message": "Handler function should destructure the logger for proper logging", | |
"example": { | |
"correct": "export const handler = async (input, { emit, logger }) => { ... }", | |
"incorrect": "export const handler = async (input, { emit }) => { ... } // missing logger" | |
} | |
}, | |
{ | |
"name": "missing-emit-call", | |
"description": "Check for steps with emits that don't call emit", | |
"pattern": "\\bemits:\\s*\\[.*?\\].*?handler.*?{[^}]*?}", | |
"filePattern": ".*\\.step\\.(ts|js)$", | |
"severity": "warning", | |
"message": "Handler for step with emits should call the emit function", | |
"example": { | |
"correct": "emits: ['event.done'],\n// ...\nhandler: async (input, { emit }) => {\n await emit({\n topic: 'event.done',\n data: { result: 'success' }\n })\n}", | |
"incorrect": "emits: ['event.done'],\n// ...\nhandler: async (input, { emit }) => {\n // Missing emit call\n}" | |
} | |
}, | |
// State Management Rules | |
{ | |
"name": "missing-state-scope", | |
"description": "Check for state operations without scope", | |
"pattern": "\\bstate\\.(?:get|set|delete|clear)\\s*\\([^,)]*\\)", | |
"filePattern": ".*\\.step\\.(ts|js)$", | |
"severity": "error", | |
"message": "State operations must include a scope parameter (usually traceId)", | |
"example": { | |
"correct": "await state.get(traceId, 'user.data')", | |
"incorrect": "await state.get('user.data') // missing scope parameter" | |
} | |
}, | |
{ | |
"name": "missing-traceid-in-state", | |
"description": "Suggest using traceId for state operations", | |
"pattern": "\\bstate\\.(?:get|set|delete|clear)\\s*\\((?!traceId)[^,)]*,", | |
"filePattern": ".*\\.step\\.(ts|js)$", | |
"severity": "warning", | |
"message": "Consider using traceId as the scope for state operations for proper isolation", | |
"example": { | |
"correct": "await state.set(traceId, 'order.data', orderData)", | |
"incorrect": "await state.set('customScope', 'order.data', orderData) // consider using traceId" | |
} | |
}, | |
// Error Handling Rules | |
{ | |
"name": "missing-try-catch", | |
"description": "Suggest try-catch blocks for external operations", | |
"pattern": "\\bawait\\s+(?!emit|state|logger)\\w+", | |
"filePattern": ".*\\.step\\.(ts|js)$", | |
"severity": "info", | |
"message": "Consider wrapping external operations in try-catch blocks for better error handling", | |
"example": { | |
"correct": "try {\n const response = await apiClient.getData()\n // ...\n} catch (error) {\n logger.error('API call failed', { error })\n}", | |
"incorrect": "const response = await apiClient.getData() // consider try-catch" | |
} | |
}, | |
{ | |
"name": "missing-error-logging", | |
"description": "Check for catch blocks without error logging", | |
"pattern": "\\bcatch\\s*\\([^)]*\\)\\s*{[^}]*?(?!logger\\.error)[^}]*?}", | |
"filePattern": ".*\\.step\\.(ts|js)$", | |
"severity": "warning", | |
"message": "Catch blocks should log errors using logger.error", | |
"example": { | |
"correct": "catch (error) {\n logger.error('Operation failed', { error })\n}", | |
"incorrect": "catch (error) {\n // Missing error logging\n}" | |
} | |
}, | |
// TypeScript Specific Rules | |
{ | |
"name": "missing-type-annotation", | |
"description": "Check for missing type annotations in TypeScript", | |
"pattern": "\\bexport\\s+const\\s+config\\s*=", | |
"filePattern": ".*\\.step\\.ts$", | |
"severity": "warning", | |
"message": "Consider adding type annotations to config objects (e.g., export const config: EventConfig = ...)", | |
"example": { | |
"correct": "export const config: EventConfig = { ... }", | |
"incorrect": "export const config = { ... } // missing type annotation" | |
} | |
}, | |
{ | |
"name": "missing-handler-type", | |
"description": "Check for missing StepHandler type in TypeScript", | |
"pattern": "\\bexport\\s+const\\s+handler\\s*:\\s*StepHandler", | |
"inverse": true, | |
"filePattern": ".*\\.step\\.ts$", | |
"severity": "warning", | |
"message": "Consider using the StepHandler type for better type safety", | |
"example": { | |
"correct": "export const handler: StepHandler<typeof config> = async (input, { emit }) => { ... }", | |
"incorrect": "export const handler = async (input, { emit }) => { ... } // missing type" | |
} | |
}, | |
// Topic Naming Rules | |
{ | |
"name": "topic-naming-convention", | |
"description": "Check for topic naming convention", | |
"pattern": "\\b(?:subscribes|emits):\\s*\\[\\s*['\"](?!.*?\\.).*?['\"]", | |
"filePattern": ".*\\.step\\.(ts|js)$", | |
"severity": "info", | |
"message": "Consider using namespaced topics (e.g., 'domain.event') for better organization", | |
"example": { | |
"correct": "subscribes: ['payment.initiated', 'payment.approved']", | |
"incorrect": "subscribes: ['paymentInitiated', 'paymentApproved'] // consider domain.event format" | |
} | |
}, | |
// Testing Rules | |
{ | |
"name": "missing-test-file", | |
"description": "Check for missing test file", | |
"pattern": ".*\\.step\\.(ts|js)$", | |
"filePattern": ".*\\.step\\.(ts|js)$", | |
"severity": "info", | |
"message": "Consider creating a test file for this step", | |
"example": { | |
"test-file": "processPayment.step.test.ts (for processPayment.step.ts)" | |
} | |
} | |
], | |
"ai.enableCodeLens": true, | |
"ai.linters.enabled": true, | |
"documentation": { | |
"llmReference": "./llms.txt", | |
"description": "Reference this URL for additional context about Motia" | |
}, | |
"filePatterns": { | |
"steps": [ | |
"**/*.step.ts", | |
"**/*.step.js", | |
"**/*.step.py", | |
"**/*.step.rb", | |
"**/*.step.tsx" | |
], | |
"api": [ | |
"**/*Api.step.*" | |
], | |
"cron": [ | |
"**/*[cC]ron*.step.*" | |
], | |
"noop": [ | |
"**/*.step.{ts,tsx,js,jsx}" | |
] | |
}, | |
"stepStructure": { | |
"config": { | |
"type": "The step type (api, event, cron, noop)", | |
"name": "Unique identifier for the step", | |
"subscribes": "List of topics this step listens to", | |
"emits": "List of topics this step can emit", | |
"flows": "List of flow identifiers that this step belongs to", | |
"description": "Optional description of the step's purpose" | |
}, | |
"handler": "Function that contains the business logic of the step" | |
}, | |
"conventions": { | |
"apiSteps": "Create HTTP endpoints with path and method", | |
"eventSteps": "React to events and emit new events", | |
"cronSteps": "Schedule tasks with cron expressions", | |
"noopSteps": "Represent external processes or UI elements" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment