.cursor/rules/canon-development.mdc---description: Workflow for adding or updating Canon ESLint rulesglobs:alwaysApply: false---# Canon Rule Development WorkflowWhen asked to add, update, or fix a Canon ESLint rule, follow these steps:## When to Use ESLint vs Documentation- **ESLint enforcement (preferred):** Always create an ESLint rule unless technically impossible to detect. Enforced patterns catch violations automatically.- **Documentation only:** Only use when ESLint cannot reasonably detect the violation (e.g., semantic naming conventions, architectural decisions that span multiple files).When in doubt, create an ESLint rule.## 1. Update Schema (if new rule)Edit `/Users/chrisb/Sites/canon/canon/schema.json`:- Add new pattern entry with id, title, file, category, status, enforcement, rule, summary- Add pattern id to relevant guarantees if applicable## 2. Create Pattern Documentation (if new rule)Create `canon/patterns/0XX-rule-name.md` with:- Summary, Rationale, Bad/Good examples, Exceptions## 3. Create or Update the ESLint RuleLocation: `canon/src/eslint/rules/rule-name.ts````typescriptimport type { Rule } from 'eslint'import { getCanonUrl, getCanonPattern } from '../utils/canon.js'const RULE_NAME = 'rule-name'const pattern = getCanonPattern(RULE_NAME)const rule: Rule.RuleModule = {meta: {type: 'suggestion',docs: {description: pattern?.summary || 'Description',recommended: true,url: getCanonUrl(RULE_NAME),},messages: {messageId: `[Canon ${pattern?.id || '0XX'}] Error message.`,},schema: [],},create(context) {const filename = context.filename || context.getFilename()if (!filename.includes('/blocks/')) return {}return {JSXOpeningElement(node: any) {// Rule logic - use context.report({ node, messageId }) to flag},}},}export default rule```## 4. Register the Rule (if new)Edit `canon/src/eslint/index.ts`:- Add import for the new rule- Add to `rules` object- Add to `recommended` config object## 5. Build and Publish```bashcd ~/Sites/canon/canonnpm run build```Bump version in `canon/package.json`:- Patch: bug fixes- Minor: new rules- Major: breaking changes```bashcd ~/Sites/canonnpm publish --workspace=canon --access public```## 6. Update Ascot```bashcd ~/Sites/ascotnpm install @gallop.software/canon@X.X.X```## 7. Test the Rule```bashnpx eslint src/blocks/test-file.tsx```## 8. Regenerate AI Rules```bashnpm run generate:ai-rules```## 9. Commit and Push Both ReposCanon:```bashcd ~/Sites/canongit add -A && git commit -m "feat: description" && git push```Ascot:```bashcd ~/Sites/ascotgit add -A && git commit -m "chore: update canon" && git push```## Common Rule Patterns### Check for specific component```typescriptif (node.name?.name === 'ComponentName') { ... }```### Get attribute value```typescriptconst attr = node.attributes?.find(a => a.name?.name === 'propName')const value = attr?.value?.value```### Check className for patterns```typescriptconst classAttr = node.attributes?.find(a => a.name?.name === 'className')if (classAttr?.value?.value?.includes('some-class')) { ... }```### Check parent elements```typescriptlet parent = node.parentwhile (parent) {if (parent.openingElement?.name?.name === 'Parent') returnparent = parent.parent}```### Check for text children```typescriptconst hasText = node.parent?.children?.some(c =>c.type === 'JSXText' && c.value.trim().length > 0)```