From da64f769da6ef84bc918730c72235a6a020abd9e Mon Sep 17 00:00:00 2001 From: Varun Date: Thu, 14 May 2026 13:33:13 +0530 Subject: [PATCH] feat(flutter-cursor-templates): introduce MCP integration and conventions in project brief - Added optional MCP integration settings in project-brief.yaml, allowing for environment-based server configurations. - Introduced conventions for strict package imports to enhance code organization and maintainability. - Updated brief schema to validate new MCP properties and ensure correct usage. - Implemented MCP JSON builder to generate .cursor/mcp.json based on project brief settings. - Enhanced resolver to include MCP configuration in generated files when enabled. This update improves integration capabilities and enforces coding standards across the project. --- example-project/project-brief.yaml | 11 + .../test/incremental_kernel.Ly9AZGFydD0zLjM= | Bin 4060200 -> 4095800 bytes .../generator/brief-schema.json | 32 +++ .../generator/src/brief_loader.dart | 8 + .../generator/src/mcp_json.dart | 88 +++++++ .../generator/src/models.dart | 12 + .../generator/src/renderer.dart | 86 ++++++- .../generator/src/resolver.dart | 184 ++++++++++++-- .../generator/src/validator.dart | 17 ++ .../templates/commands/build.md.tmpl | 1 + .../templates/commands/debug-issue.md.tmpl | 1 + .../templates/commands/explain-code.md.tmpl | 1 + .../templates/commands/verify-change.md.tmpl | 1 + .../templates/onboarding/ONBOARDING.md.tmpl | 20 ++ .../templates/root/.cursorignore.tmpl | 27 ++ .../generator/templates/root/AGENTS.md.tmpl | 3 +- .../templates/root/lefthook.yaml.tmpl | 1 + .../templates/root/tool/cursor_audit.sh.tmpl | 35 +++ .../templates/rules/cicd/cicd.mdc.tmpl | 29 +++ .../templates/rules/features/_stub.mdc.tmpl | 21 ++ .../rules/i18n/localization.mdc.tmpl | 3 +- .../rules/integrations/push-deeplink.mdc.tmpl | 21 ++ .../rules/telemetry/usage-logging.mdc.tmpl | 19 ++ .../templates/rules/theming/theming.mdc.tmpl | 20 ++ .../rules/universal/flutter-core.mdc.tmpl | 10 +- .../rules/universal/project-context.mdc.tmpl | 5 +- .../rules/universal/rule-authoring.mdc.tmpl | 24 ++ .../rules/universal/ui-ux-standards.mdc.tmpl | 5 +- .../templates/telemetry/gitignore.tmpl | 2 + .../generator/templates/telemetry/log.sh.tmpl | 7 + .../generator/test/generator_test.dart | 234 ++++++++++++++++++ .../golden/bloc-clean-firebase/ONBOARDING.md | 20 ++ .../__root__/.cursorignore | 27 ++ .../bloc-clean-firebase/__root__/AGENTS.md | 3 +- .../__root__/lefthook.yaml | 1 + .../__root__/tool/cursor_audit.sh | 35 +++ .../bloc-clean-firebase/commands/build.md | 1 + .../commands/debug-issue.md | 1 + .../commands/explain-code.md | 1 + .../commands/verify-change.md | 1 + .../bloc-clean-firebase/rules/cicd/cicd.mdc | 29 +++ .../rules/features/auth.mdc | 21 ++ .../rules/features/home.mdc | 21 ++ .../rules/features/products.mdc | 21 ++ .../rules/universal/flutter-core.mdc | 11 +- .../rules/universal/project-context.mdc | 5 +- .../rules/universal/rule-authoring.mdc | 24 ++ .../rules/universal/ui-ux-standards.mdc | 5 +- .../test/golden/getx-mvc-rest/ONBOARDING.md | 20 ++ .../getx-mvc-rest/__root__/.cursorignore | 27 ++ .../golden/getx-mvc-rest/__root__/AGENTS.md | 3 +- .../getx-mvc-rest/__root__/lefthook.yaml | 1 + .../__root__/tool/cursor_audit.sh | 35 +++ .../golden/getx-mvc-rest/commands/build.md | 1 + .../getx-mvc-rest/commands/debug-issue.md | 1 + .../getx-mvc-rest/commands/explain-code.md | 1 + .../getx-mvc-rest/commands/verify-change.md | 1 + .../test/golden/getx-mvc-rest/mcp.json | 57 +++++ .../golden/getx-mvc-rest/rules/cicd/cicd.mdc | 29 +++ .../getx-mvc-rest/rules/features/auth.mdc | 21 ++ .../rules/features/dashboard.mdc | 21 ++ .../rules/universal/flutter-core.mdc | 12 +- .../rules/universal/project-context.mdc | 5 +- .../rules/universal/rule-authoring.mdc | 24 ++ .../rules/universal/ui-ux-standards.mdc | 5 +- .../golden/riverpod-ff-supabase/ONBOARDING.md | 20 ++ .../__root__/.cursorignore | 27 ++ .../riverpod-ff-supabase/__root__/AGENTS.md | 3 +- .../__root__/lefthook.yaml | 1 + .../__root__/tool/cursor_audit.sh | 35 +++ .../riverpod-ff-supabase/commands/build.md | 1 + .../commands/debug-issue.md | 1 + .../commands/explain-code.md | 1 + .../commands/verify-change.md | 1 + .../riverpod-ff-supabase/rules/cicd/cicd.mdc | 29 +++ .../rules/features/auth.mdc | 21 ++ .../rules/features/profile.mdc | 21 ++ .../rules/features/tasks.mdc | 21 ++ .../rules/i18n/localization.mdc | 3 +- .../rules/theming/theming.mdc | 23 ++ .../rules/universal/flutter-core.mdc | 12 +- .../rules/universal/project-context.mdc | 9 +- .../rules/universal/rule-authoring.mdc | 24 ++ .../rules/universal/ui-ux-standards.mdc | 6 +- .../generator/test/golden_briefs.dart | 91 +++++++ .../generator/tool/cursor_audit.sh | 35 +++ .../generator/tool/refresh_goldens.dart | 47 ++++ .../templates/commands/build.md.tmpl | 1 + .../templates/commands/debug-issue.md.tmpl | 1 + .../templates/commands/explain-code.md.tmpl | 1 + .../templates/commands/verify-change.md.tmpl | 1 + .../templates/onboarding/ONBOARDING.md.tmpl | 20 ++ .../templates/root/.cursorignore.tmpl | 27 ++ .../templates/root/AGENTS.md.tmpl | 3 +- .../templates/root/lefthook.yaml.tmpl | 1 + .../templates/root/tool/cursor_audit.sh.tmpl | 35 +++ .../templates/rules/cicd/cicd.mdc.tmpl | 29 +++ .../templates/rules/features/_stub.mdc.tmpl | 21 ++ .../rules/i18n/localization.mdc.tmpl | 3 +- .../rules/integrations/push-deeplink.mdc.tmpl | 21 ++ .../rules/telemetry/usage-logging.mdc.tmpl | 19 ++ .../templates/rules/theming/theming.mdc.tmpl | 20 ++ .../rules/universal/flutter-core.mdc.tmpl | 10 +- .../rules/universal/project-context.mdc.tmpl | 5 +- .../rules/universal/rule-authoring.mdc.tmpl | 24 ++ .../rules/universal/ui-ux-standards.mdc.tmpl | 5 +- .../templates/telemetry/gitignore.tmpl | 2 + .../templates/telemetry/log.sh.tmpl | 7 + flutter-cursor-templates/tool/cursor_audit.sh | 54 ++++ 109 files changed, 2076 insertions(+), 85 deletions(-) create mode 100644 flutter-cursor-templates/generator/src/mcp_json.dart create mode 100644 flutter-cursor-templates/generator/templates/commands/build.md.tmpl create mode 100644 flutter-cursor-templates/generator/templates/commands/debug-issue.md.tmpl create mode 100644 flutter-cursor-templates/generator/templates/commands/explain-code.md.tmpl create mode 100644 flutter-cursor-templates/generator/templates/commands/verify-change.md.tmpl create mode 100644 flutter-cursor-templates/generator/templates/onboarding/ONBOARDING.md.tmpl create mode 100644 flutter-cursor-templates/generator/templates/root/.cursorignore.tmpl create mode 100644 flutter-cursor-templates/generator/templates/root/tool/cursor_audit.sh.tmpl create mode 100644 flutter-cursor-templates/generator/templates/rules/cicd/cicd.mdc.tmpl create mode 100644 flutter-cursor-templates/generator/templates/rules/features/_stub.mdc.tmpl create mode 100644 flutter-cursor-templates/generator/templates/rules/integrations/push-deeplink.mdc.tmpl create mode 100644 flutter-cursor-templates/generator/templates/rules/telemetry/usage-logging.mdc.tmpl create mode 100644 flutter-cursor-templates/generator/templates/rules/theming/theming.mdc.tmpl create mode 100644 flutter-cursor-templates/generator/templates/rules/universal/rule-authoring.mdc.tmpl create mode 100644 flutter-cursor-templates/generator/templates/telemetry/gitignore.tmpl create mode 100644 flutter-cursor-templates/generator/templates/telemetry/log.sh.tmpl create mode 100644 flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/ONBOARDING.md create mode 100644 flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/__root__/.cursorignore create mode 100644 flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/__root__/tool/cursor_audit.sh create mode 100644 flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/commands/build.md create mode 100644 flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/commands/debug-issue.md create mode 100644 flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/commands/explain-code.md create mode 100644 flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/commands/verify-change.md create mode 100644 flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/cicd/cicd.mdc create mode 100644 flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/features/auth.mdc create mode 100644 flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/features/home.mdc create mode 100644 flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/features/products.mdc create mode 100644 flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/rule-authoring.mdc create mode 100644 flutter-cursor-templates/generator/test/golden/getx-mvc-rest/ONBOARDING.md create mode 100644 flutter-cursor-templates/generator/test/golden/getx-mvc-rest/__root__/.cursorignore create mode 100644 flutter-cursor-templates/generator/test/golden/getx-mvc-rest/__root__/tool/cursor_audit.sh create mode 100644 flutter-cursor-templates/generator/test/golden/getx-mvc-rest/commands/build.md create mode 100644 flutter-cursor-templates/generator/test/golden/getx-mvc-rest/commands/debug-issue.md create mode 100644 flutter-cursor-templates/generator/test/golden/getx-mvc-rest/commands/explain-code.md create mode 100644 flutter-cursor-templates/generator/test/golden/getx-mvc-rest/commands/verify-change.md create mode 100644 flutter-cursor-templates/generator/test/golden/getx-mvc-rest/mcp.json create mode 100644 flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/cicd/cicd.mdc create mode 100644 flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/features/auth.mdc create mode 100644 flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/features/dashboard.mdc create mode 100644 flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/rule-authoring.mdc create mode 100644 flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/ONBOARDING.md create mode 100644 flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/__root__/.cursorignore create mode 100644 flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/__root__/tool/cursor_audit.sh create mode 100644 flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/commands/build.md create mode 100644 flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/commands/debug-issue.md create mode 100644 flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/commands/explain-code.md create mode 100644 flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/commands/verify-change.md create mode 100644 flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/cicd/cicd.mdc create mode 100644 flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/features/auth.mdc create mode 100644 flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/features/profile.mdc create mode 100644 flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/features/tasks.mdc create mode 100644 flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/theming/theming.mdc create mode 100644 flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/rule-authoring.mdc create mode 100644 flutter-cursor-templates/generator/test/golden_briefs.dart create mode 100755 flutter-cursor-templates/generator/tool/cursor_audit.sh create mode 100644 flutter-cursor-templates/generator/tool/refresh_goldens.dart create mode 100644 flutter-cursor-templates/templates/commands/build.md.tmpl create mode 100644 flutter-cursor-templates/templates/commands/debug-issue.md.tmpl create mode 100644 flutter-cursor-templates/templates/commands/explain-code.md.tmpl create mode 100644 flutter-cursor-templates/templates/commands/verify-change.md.tmpl create mode 100644 flutter-cursor-templates/templates/onboarding/ONBOARDING.md.tmpl create mode 100644 flutter-cursor-templates/templates/root/.cursorignore.tmpl create mode 100644 flutter-cursor-templates/templates/root/tool/cursor_audit.sh.tmpl create mode 100644 flutter-cursor-templates/templates/rules/cicd/cicd.mdc.tmpl create mode 100644 flutter-cursor-templates/templates/rules/features/_stub.mdc.tmpl create mode 100644 flutter-cursor-templates/templates/rules/integrations/push-deeplink.mdc.tmpl create mode 100644 flutter-cursor-templates/templates/rules/telemetry/usage-logging.mdc.tmpl create mode 100644 flutter-cursor-templates/templates/rules/theming/theming.mdc.tmpl create mode 100644 flutter-cursor-templates/templates/rules/universal/rule-authoring.mdc.tmpl create mode 100644 flutter-cursor-templates/templates/telemetry/gitignore.tmpl create mode 100644 flutter-cursor-templates/templates/telemetry/log.sh.tmpl create mode 100644 flutter-cursor-templates/tool/cursor_audit.sh diff --git a/example-project/project-brief.yaml b/example-project/project-brief.yaml index ab0dc9c..a222d88 100644 --- a/example-project/project-brief.yaml +++ b/example-project/project-brief.yaml @@ -116,6 +116,17 @@ localization: enabled: true locales: ["en", "es", "fr"] +# ----------------------------------------------------------------------------- +# Integrations & conventions (optional) +# ----------------------------------------------------------------------------- +integrations: + mcp: + enabled: false # true → .cursor/mcp.json (secrets only via env vars) + preset: auto # auto | minimal + +conventions: + strict_package_imports: false + # ----------------------------------------------------------------------------- # Telemetry (Pillar 6) — local-only generation / usage log under .cursor/ # ----------------------------------------------------------------------------- diff --git a/flutter-cursor-templates/generator/.dart_tool/test/incremental_kernel.Ly9AZGFydD0zLjM= b/flutter-cursor-templates/generator/.dart_tool/test/incremental_kernel.Ly9AZGFydD0zLjM= index 0421383dac0c6d894bb8fa1107c64a5fdacc8831..f57c918b8e6d41e0281e62a8e65557c6dca86286 100644 GIT binary patch delta 62434 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 z

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'mcpServers': {}}); + } + + final servers = >{ + 'filesystem': { + 'command': 'npx', + 'args': ['-y', '@modelcontextprotocol/server-filesystem', '.'], + 'description': 'Read/write project files under the workspace root', + }, + 'flutter-docs': { + 'command': 'npx', + 'args': ['-y', '@modelcontextprotocol/server-fetch'], + 'env': {'BASE_URL': 'https://api.flutter.dev/flutter'}, + 'description': 'Flutter API documentation lookup', + }, + 'dart-pub': { + 'command': 'npx', + 'args': ['-y', '@modelcontextprotocol/server-fetch'], + 'env': {'BASE_URL': 'https://pub.dev/api'}, + 'description': 'Pub.dev package metadata and versions', + }, + 'github': { + 'command': 'npx', + 'args': ['-y', '@modelcontextprotocol/server-github'], + 'env': {'GITHUB_TOKEN': r'${GITHUB_TOKEN}'}, + 'description': 'GitHub issues and PR context (requires GITHUB_TOKEN)', + }, + }; + + if (brief.backends.contains('firebase')) { + servers['firebase'] = { + 'command': 'npx', + 'args': ['-y', '@modelcontextprotocol/server-firebase'], + 'env': {'FIREBASE_PROJECT': r'${FIREBASE_PROJECT_ID}'}, + 'description': + 'Firebase project context (configure FIREBASE_PROJECT_ID)', + }; + } + + if (brief.designSource == 'figma_mcp') { + servers['figma'] = { + 'url': 'https://mcp.figma.com/mcp', + 'type': 'http', + 'headers': { + 'Authorization': r'Bearer ${FIGMA_ACCESS_TOKEN}', + }, + 'description': + 'Figma MCP — set FIGMA_ACCESS_TOKEN (never commit the value)', + }; + } + + if (brief.backends.contains('supabase')) { + servers['supabase'] = { + 'command': 'npx', + 'args': ['-y', '@supabase/mcp-server-supabase@latest'], + 'env': { + 'SUPABASE_ACCESS_TOKEN': r'${SUPABASE_ACCESS_TOKEN}', + }, + 'description': 'Supabase MCP — token from env only', + }; + } + + if (brief.apiDocsFormat == 'openapi' && brief.apiDocsPath.isNotEmpty) { + servers['openapi-ref'] = { + 'command': 'npx', + 'args': ['-y', '@modelcontextprotocol/server-fetch'], + 'env': { + 'OPENAPI_SPEC_URL': r'${OPENAPI_SPEC_URL}', + }, + 'description': + 'Optional fetch MCP — point OPENAPI_SPEC_URL at a hosted spec or file:// URL you expose locally', + }; + } + + return const JsonEncoder.withIndent(' ').convert({'mcpServers': servers}); + } +} diff --git a/flutter-cursor-templates/generator/src/models.dart b/flutter-cursor-templates/generator/src/models.dart index d92773d..8474f2d 100644 --- a/flutter-cursor-templates/generator/src/models.dart +++ b/flutter-cursor-templates/generator/src/models.dart @@ -59,6 +59,15 @@ class ProjectBrief { final bool rolesEnabled; final List roleNames; + /// When true, emit `.cursor/mcp.json` with safe env-placeholder servers. + final bool mcpConfigEnabled; + + /// When [mcpConfigEnabled]: `minimal` (empty `mcpServers`) or `auto` (stubs from brief). + final String mcpPreset; + + /// When true, generated flutter-core rule enforces package imports (no cross-feature relatives). + final bool strictPackageImports; + const ProjectBrief({ required this.projectName, required this.packageId, @@ -90,6 +99,9 @@ class ProjectBrief { this.themeVariants = const ['light', 'dark'], this.rolesEnabled = false, this.roleNames = const [], + this.mcpConfigEnabled = false, + this.mcpPreset = 'auto', + this.strictPackageImports = false, }); /// Local snapshot for tooling (written as `cursor-gen-metadata.json` under the output dir). diff --git a/flutter-cursor-templates/generator/src/renderer.dart b/flutter-cursor-templates/generator/src/renderer.dart index 179208a..52ff2c6 100644 --- a/flutter-cursor-templates/generator/src/renderer.dart +++ b/flutter-cursor-templates/generator/src/renderer.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:path/path.dart' as p; +import 'mcp_json.dart'; import 'models.dart'; class Renderer { @@ -10,22 +11,32 @@ class Renderer { required List templateFiles, required String templateSrc, }) async { - final context = _buildContext(brief); + final baseContext = _buildContext(brief); final output = {}; for (final key in templateFiles) { + if (key == 'config/mcp-json') { + output['mcp.json'] = McpJsonBuilder.build(brief); + continue; + } + + final outPath = _outputPath(key); final tmplPath = _templatePath(templateSrc, key); final file = File(tmplPath); if (!file.existsSync()) { - // Gracefully skip missing optional templates with a placeholder - output[_outputPath(key)] = _missingTemplatePlaceholder(key); + output[outPath] = _missingTemplatePlaceholder(key); continue; } var content = await file.readAsString(); - content = _substituteAll(content, context); - _checkUnreplacedPlaceholders( - content, key); // Pillar 3: validate no broken {{VAR}} - output[_outputPath(key)] = content; + final ctx = Map.from(baseContext); + if (key.startsWith('rules/features/')) { + final slug = p.basename(key); + ctx['FEATURE_MODULE'] = slug; + ctx['FEATURE_MODULE_TITLE'] = _titleCase(slug); + } + content = _substituteAll(content, ctx); + _checkUnreplacedPlaceholders(content, key); + output[outPath] = content; } return output; } @@ -71,9 +82,35 @@ class Renderer { 'TEST_PATTERN': _testPattern(brief.stateManagement), 'LOCALES_LIST': brief.locales.join(', '), 'TEMPLATE_VERSION': '1.0.4', + 'IMPORT_POLICY_BLOCK': _importPolicyBlock(brief.strictPackageImports), }; } + static String _titleCase(String slug) { + if (slug.isEmpty) return slug; + return slug + .split('_') + .where((w) => w.isNotEmpty) + .map((w) => '${w[0].toUpperCase()}${w.substring(1)}') + .join(' '); + } + + static String _importPolicyBlock(bool strictPackage) { + if (strictPackage) { + return ''' +### Imports (strict — `conventions.strict_package_imports: true`) +- Use `package:/...` imports everywhere in `lib/` and `test/` — **no** relative `../` across feature boundaries (the brief `project.package` id is often the app bundle id; prefer the **pubspec.yaml `name`** for Dart imports) +- Barrel files (`index.dart`) at feature roots; do not wildcard re-export third-party packages +'''; + } + return ''' +### Imports (default) +- Order: `dart:` → `package:` → relative +- Relative imports are allowed **within** the same feature directory; use `package:` imports for cross-feature code +- Never import another feature's internals from outside that feature +'''; + } + static String _substituteAll(String content, Map ctx) { for (final entry in ctx.entries) { content = content.replaceAll('{{${entry.key}}}', entry.value); @@ -107,6 +144,25 @@ class Renderer { } return p.join(templateSrc, 'hooks', '${p.basename(key)}.ts.tmpl'); } + if (key.startsWith('commands/')) { + final name = p.basename(key); + return p.join(templateSrc, 'commands', '$name.md.tmpl'); + } + if (key == 'onboarding/ONBOARDING') { + return p.join(templateSrc, 'onboarding', 'ONBOARDING.md.tmpl'); + } + if (key.startsWith('telemetry/')) { + final leaf = p.basename(key); + if (leaf == 'gitignore') { + return p.join(templateSrc, 'telemetry', 'gitignore.tmpl'); + } + if (leaf == 'log-sh') { + return p.join(templateSrc, 'telemetry', 'log.sh.tmpl'); + } + } + if (key.startsWith('rules/features/')) { + return p.join(templateSrc, 'rules', 'features', '_stub.mdc.tmpl'); + } return p.join(templateSrc, '$key.mdc.tmpl'); } @@ -122,6 +178,22 @@ class Renderer { if (key.endsWith('hooks-json')) return 'hooks/hooks.json'; return 'hooks/${p.basename(key)}.ts'; } + if (key.startsWith('commands/')) { + final name = p.basename(key); + return 'commands/$name.md'; + } + if (key == 'onboarding/ONBOARDING') { + return 'ONBOARDING.md'; + } + if (key == 'telemetry/gitignore') { + return 'telemetry/.gitignore'; + } + if (key == 'telemetry/log-sh') { + return 'telemetry/log.sh'; + } + if (key == 'config/mcp-json') { + return 'mcp.json'; + } return '${key.replaceAll('rules/', 'rules/')}.mdc'; } diff --git a/flutter-cursor-templates/generator/src/resolver.dart b/flutter-cursor-templates/generator/src/resolver.dart index 03e4b20..35d29c6 100644 --- a/flutter-cursor-templates/generator/src/resolver.dart +++ b/flutter-cursor-templates/generator/src/resolver.dart @@ -8,6 +8,9 @@ class Resolver { static List resolve(ProjectBrief brief) { final files = []; + // ── Meta: rule authoring (first — governs other .mdc files) ───────── + files.add('rules/universal/rule-authoring'); + // ── Universal (every project) ────────────────────────────────────── files.addAll([ 'rules/universal/flutter-core', @@ -15,6 +18,11 @@ class Resolver { 'rules/universal/project-context', ]); + // ── Theming tokens (when high contrast or extra variants) ─────────── + if (_emitThemingRule(brief)) { + files.add('rules/theming/theming'); + } + // ── Security (Pillar 5: always-on, not just for large/auth projects) ── files.add('rules/security/security-standards'); @@ -28,11 +36,19 @@ class Resolver { files.add('rules/architecture/${brief.architecture}'); // ── Backend — one or more ───────────────────────────────────────── - for (final b in brief.backends) files.add('rules/backend/$b'); + for (final b in brief.backends) { + files.add('rules/backend/$b'); + } if (brief.specialFeatures.contains('realtime')) { files.add('rules/backend/realtime'); } + // ── Push / deep linking ─────────────────────────────────────────── + if (brief.specialFeatures.contains('push_notifications') || + brief.specialFeatures.contains('deep_linking')) { + files.add('rules/integrations/push-deeplink'); + } + // ── Routing — exactly one ───────────────────────────────────────── files.add('rules/routing/${brief.routing}'); @@ -67,6 +83,19 @@ class Resolver { files.add('rules/i18n/localization'); } + // ── CI/CD + flavours ───────────────────────────────────────────── + if (brief.cicd != 'none' || brief.flavors.length > 1) { + files.add('rules/cicd/cicd'); + } + + // ── Feature-scoped rule stubs ───────────────────────────────────── + final featureKeys = {}; + for (final m in brief.featureModules) { + final k = featureRuleKey(m); + if (k != null) featureKeys.add(k); + } + files.addAll(featureKeys); + // ── Skills ──────────────────────────────────────────────────────── files.addAll([ 'skills/scaffold-feature', @@ -78,8 +107,8 @@ class Resolver { 'skills/explain-code', ]); if (brief.apiDocsFormat != 'none') files.add('skills/generate-api-client'); - if (brief.flavors.length > 1) files.add('skills/create-flavor'); - if (brief.cicd.isNotEmpty) files.add('skills/deploy'); + if (brief.flavors.length > 1) files.add('skills/create-flavor'); + if (brief.cicd != 'none') files.add('skills/deploy'); // ── Agents ──────────────────────────────────────────────────────── files.addAll([ @@ -97,11 +126,51 @@ class Resolver { files.add('agents/migration-agent'); } + // ── Cursor workspace extras (reference architecture) ──────────────── + files.addAll([ + 'root/.cursorignore', + 'root/tool/cursor_audit.sh', + 'onboarding/ONBOARDING', + 'commands/build', + 'commands/debug-issue', + 'commands/verify-change', + 'commands/explain-code', + ]); + + if (brief.mcpConfigEnabled) { + files.add('config/mcp-json'); + } + + if (brief.telemetryOptIn) { + files.addAll([ + 'telemetry/gitignore', + 'telemetry/log-sh', + 'rules/telemetry/usage-logging', + ]); + } + files.addAll(['root/AGENTS.md', 'root/lefthook.yaml']); return files; } + /// `lib/features//` ↔ `rules/features/.mdc` + static String? featureRuleKey(String module) { + var s = module + .trim() + .toLowerCase() + .replaceAll(RegExp(r'[^a-z0-9]+'), '_'); + s = s.replaceAll(RegExp(r'_+'), '_'); + s = s.replaceAll(RegExp(r'(^_+|_+$)'), ''); + if (s.isEmpty) return null; + return 'rules/features/$s'; + } + + static bool _emitThemingRule(ProjectBrief brief) { + return brief.themeVariants.contains('high_contrast') || + brief.themeVariants.length > 2; + } + /// Returns human-readable description of why each file was included static Map resolveWithReasons(ProjectBrief brief) { final resolved = resolve(brief); @@ -113,29 +182,100 @@ class Resolver { } static String _reason(String key, ProjectBrief brief) { - if (key.contains('universal')) return 'Always included'; - if (key.contains('security')) return 'Always included — Pillar 5'; + if (key == 'rules/universal/rule-authoring') { + return 'Always included — meta rules for authoring .cursor/rules'; + } + if (key.contains('universal')) return 'Always included'; + if (key == 'rules/theming/theming') { + return 'Theme variants include high contrast or extended set'; + } + if (key.contains('security')) { + return 'Always included — Pillar 5'; + } if (key.contains('error-handling')) return 'Always included'; - if (key.contains('state-management')) return 'Matches stack.state_management: ${brief.stateManagement}'; - if (key.contains('architecture')) return 'Matches stack.architecture: ${brief.architecture}'; - if (key.contains('routing')) return 'Matches stack.routing: ${brief.routing}'; - if (key.contains('testing-e2e')) return 'testing.depth includes e2e'; - if (key.contains('testing')) return 'Matches state_management testing patterns'; - if (key.contains('platform')) return 'Matches stack.platforms'; + if (key.contains('state-management')) { + return 'Matches stack.state_management: ${brief.stateManagement}'; + } + if (key.contains('architecture')) { + return 'Matches stack.architecture: ${brief.architecture}'; + } + if (key == 'rules/integrations/push-deeplink') { + return 'features.special includes push_notifications or deep_linking'; + } + if (key.contains('routing')) { + return 'Matches stack.routing: ${brief.routing}'; + } + if (key.contains('testing-e2e')) { + return 'testing.depth includes e2e'; + } + if (key.contains('testing')) { + return 'Matches state_management testing patterns'; + } + if (key.contains('platform')) { + return 'Matches stack.platforms'; + } if (key.startsWith('hooks/')) { return 'stack.codegen non-empty — Cursor hooks for analyze, boundaries, and tests'; } - if (key.contains('codegen')) return 'Matches stack.codegen'; - if (key.contains('i18n')) return 'localization.enabled: true'; - if (key.contains('migration')) return 'state_management is GetX — migration guidance included'; - if (key.contains('security-agent')) return 'scale: ${brief.scale} or auth is configured'; - if (key == 'skills/build') return 'Always included — universal TDD-first feature implementation command'; - if (key == 'skills/debug-issue') return 'Always included — structured bug triage and evidence-first debugging'; - if (key == 'skills/verify-change') return 'Always included — pre-PR verification checklist without full /build lifecycle'; - if (key == 'skills/explain-code') return 'Always included — explain-only walkthrough of code paths and stack behavior'; - if (key.contains('api-client')) return 'api_docs.format is set'; - if (key.contains('realtime')) return 'features.special contains realtime'; - if (key.startsWith('root/')) return 'Repo-level companion files'; + if (key.contains('codegen')) { + return 'Matches stack.codegen'; + } + if (key.contains('i18n')) { + return 'localization.enabled: true'; + } + if (key == 'rules/cicd/cicd') { + return 'environments.cicd is set or multiple flavors'; + } + if (key.startsWith('rules/features/')) { + return 'Listed under features.modules in project-brief.yaml'; + } + if (key.contains('migration')) { + return 'state_management is GetX — migration guidance included'; + } + if (key.contains('security-agent')) { + return 'scale: ${brief.scale} or auth is configured'; + } + if (key == 'skills/build') { + return 'Always included — universal TDD-first feature implementation command'; + } + if (key == 'skills/debug-issue') { + return 'Always included — structured bug triage and evidence-first debugging'; + } + if (key == 'skills/verify-change') { + return 'Always included — pre-PR verification checklist without full /build lifecycle'; + } + if (key == 'skills/explain-code') { + return 'Always included — explain-only walkthrough of code paths and stack behavior'; + } + if (key.contains('api-client')) { + return 'api_docs.format is set'; + } + if (key.contains('realtime')) { + return 'features.special contains realtime'; + } + if (key == 'root/.cursorignore') { + return 'Root ignore patterns for Cursor indexing'; + } + if (key == 'root/tool/cursor_audit.sh') { + return 'Maintenance script for rule drift checks'; + } + if (key == 'onboarding/ONBOARDING') { + return 'Team onboarding for Cursor layout and slash skills'; + } + if (key.startsWith('commands/')) { + return 'Project slash command → skill mapping'; + } + if (key == 'config/mcp-json') { + return 'integrations.mcp.enabled: true in project-brief.yaml'; + } + if (key == 'telemetry/gitignore' || + key == 'telemetry/log-sh' || + key == 'rules/telemetry/usage-logging') { + return 'telemetry_opt_in: true — local usage logging helpers'; + } + if (key.startsWith('root/')) { + return 'Repo-level companion files'; + } return 'Included'; } } diff --git a/flutter-cursor-templates/generator/src/validator.dart b/flutter-cursor-templates/generator/src/validator.dart index 209a9fb..cff24ec 100644 --- a/flutter-cursor-templates/generator/src/validator.dart +++ b/flutter-cursor-templates/generator/src/validator.dart @@ -52,6 +52,7 @@ class Validator { }; static const _validApiFormats = {'openapi', 'postman', 'markdown', 'none'}; static const _validThemeVariants = {'light', 'dark', 'high_contrast'}; + static const _validMcpPresets = {'auto', 'minimal'}; static Future validateFile(String path) async { final file = File(path); @@ -178,6 +179,18 @@ class Validator { } } + final integrations = yaml['integrations'] as YamlMap?; + if (integrations != null) { + final mcp = integrations['mcp'] as YamlMap?; + if (mcp != null) { + final preset = mcp['preset']?.toString(); + if (preset != null && !_validMcpPresets.contains(preset)) { + warnings.add( + 'integrations.mcp.preset "$preset" is not valid. Use: ${_validMcpPresets.join(", ")}'); + } + } + } + return ValidationResult( isValid: errors.isEmpty, errors: errors, @@ -214,6 +227,10 @@ class Validator { if (brief.rolesEnabled && brief.roleNames.isEmpty) { warnings.add('roles_enabled is true but role_names is empty'); } + if (!_validMcpPresets.contains(brief.mcpPreset)) { + warnings.add( + 'integrations.mcp.preset "${brief.mcpPreset}" is not valid. Use: ${_validMcpPresets.join(", ")}'); + } return ValidationResult( isValid: errors.isEmpty, errors: errors, warnings: warnings); diff --git a/flutter-cursor-templates/generator/templates/commands/build.md.tmpl b/flutter-cursor-templates/generator/templates/commands/build.md.tmpl new file mode 100644 index 0000000..4dc2925 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/commands/build.md.tmpl @@ -0,0 +1 @@ +End-to-end feature implementation (research, TDD, integration tests, verification). Follow the workflow and constraints in `@file:.cursor/skills/build/SKILL.md`. Use `project-brief.yaml` as the source of truth for stack and platforms. diff --git a/flutter-cursor-templates/generator/templates/commands/debug-issue.md.tmpl b/flutter-cursor-templates/generator/templates/commands/debug-issue.md.tmpl new file mode 100644 index 0000000..21a4fbb --- /dev/null +++ b/flutter-cursor-templates/generator/templates/commands/debug-issue.md.tmpl @@ -0,0 +1 @@ +Structured bug triage and evidence-first debugging. Follow `@file:.cursor/skills/debug-issue/SKILL.md`. Gather reproduction steps, logs, and failing commands before proposing fixes. diff --git a/flutter-cursor-templates/generator/templates/commands/explain-code.md.tmpl b/flutter-cursor-templates/generator/templates/commands/explain-code.md.tmpl new file mode 100644 index 0000000..40b10cc --- /dev/null +++ b/flutter-cursor-templates/generator/templates/commands/explain-code.md.tmpl @@ -0,0 +1 @@ +Explain-only walkthrough of code paths and stack behavior (no edits). Follow `@file:.cursor/skills/explain-code/SKILL.md`. Do not modify source files unless the user explicitly asks. diff --git a/flutter-cursor-templates/generator/templates/commands/verify-change.md.tmpl b/flutter-cursor-templates/generator/templates/commands/verify-change.md.tmpl new file mode 100644 index 0000000..a2e8bb5 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/commands/verify-change.md.tmpl @@ -0,0 +1 @@ +Pre-PR verification checklist (analyze, tests, hooks) without full /build lifecycle. Follow `@file:.cursor/skills/verify-change/SKILL.md` for the change in scope. diff --git a/flutter-cursor-templates/generator/templates/onboarding/ONBOARDING.md.tmpl b/flutter-cursor-templates/generator/templates/onboarding/ONBOARDING.md.tmpl new file mode 100644 index 0000000..04bb2ba --- /dev/null +++ b/flutter-cursor-templates/generator/templates/onboarding/ONBOARDING.md.tmpl @@ -0,0 +1,20 @@ +# Cursor — {{PROJECT_NAME}} + +## Quick start + +1. Open this repo in **Cursor** so `.cursor/rules/` and `.cursor/skills/` load automatically. +2. Read **`rules/universal/rule-authoring.mdc`** — how we maintain AI rules. +3. Stack and product context live in **`project-brief.yaml`** at the repo root; regenerate `.cursor/` with `cursor_gen` after changing it. +4. **Slash skills** (primary workflows): open the Command Palette and use the project commands, or reference: + - `.cursor/skills/build/SKILL.md` — end-to-end feature implementation + - `.cursor/skills/debug-issue/SKILL.md` — structured debugging + - `.cursor/skills/verify-change/SKILL.md` — pre-PR verification + - `.cursor/skills/explain-code/SKILL.md` — explain-only walkthroughs +5. **Agents** (`.cursor/agents/*.mdc`) are reusable reviewer personas — attach when asking for code review or focused passes. +6. **Custom overrides**: files under `.cursor/custom/` and `CURSOR:CUSTOM` blocks in generated files are preserved on `cursor_gen --refresh` (see generator docs). + +## Optional + +- **`integrations.mcp.enabled`** in `project-brief.yaml` — generates `.cursor/mcp.json` with env-based server entries (no secrets in git). +- **`telemetry_opt_in: true`** — emits local telemetry helpers under `.cursor/telemetry/` (never commit usage logs if you enable logging). +- Run **`bash tool/cursor_audit.sh`** from the project root periodically to catch stale feature rules and overly broad `alwaysApply` usage. diff --git a/flutter-cursor-templates/generator/templates/root/.cursorignore.tmpl b/flutter-cursor-templates/generator/templates/root/.cursorignore.tmpl new file mode 100644 index 0000000..c4e056e --- /dev/null +++ b/flutter-cursor-templates/generator/templates/root/.cursorignore.tmpl @@ -0,0 +1,27 @@ +# Build artefacts +build/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +*.g.dart +*.freezed.dart +*.gr.dart +*.config.dart + +# Secrets +.env +.env.* +firebase_options.dart +google-services.json +GoogleService-Info.plist + +# Large binary assets +assets/fonts/ +assets/videos/ +*.aab +*.apk +*.ipa + +# IDE +.idea/ +*.iml diff --git a/flutter-cursor-templates/generator/templates/root/AGENTS.md.tmpl b/flutter-cursor-templates/generator/templates/root/AGENTS.md.tmpl index f903927..3384b6f 100644 --- a/flutter-cursor-templates/generator/templates/root/AGENTS.md.tmpl +++ b/flutter-cursor-templates/generator/templates/root/AGENTS.md.tmpl @@ -3,4 +3,5 @@ Repo-level notes for AI assistants. Authoritative stack and conventions are in `project-brief.yaml` and `.cursor/` (regenerate with `cursor_gen` from the project root). - **Package:** `{{PACKAGE_ID}}` -- **Slash skills** (see `.cursor/skills/`): `/build`, `/debug`, `/verify`, `/explain` +- **Onboarding:** `.cursor/ONBOARDING.md` — layout, slash commands → skills, MCP opt-in, audits +- **Slash skills** (see `.cursor/skills/`): `/build`, `/debug`, `/verify`, `/explain` (see `.cursor/commands/*.md`) diff --git a/flutter-cursor-templates/generator/templates/root/lefthook.yaml.tmpl b/flutter-cursor-templates/generator/templates/root/lefthook.yaml.tmpl index 2709629..84da03d 100644 --- a/flutter-cursor-templates/generator/templates/root/lefthook.yaml.tmpl +++ b/flutter-cursor-templates/generator/templates/root/lefthook.yaml.tmpl @@ -1,4 +1,5 @@ # {{PROJECT_NAME}} — generated by cursor_gen; adjust commands to your repo +# Optional: run `bash tool/cursor_audit.sh` after changing features.modules or rule files. pre-commit: commands: flutter-analyze: diff --git a/flutter-cursor-templates/generator/templates/root/tool/cursor_audit.sh.tmpl b/flutter-cursor-templates/generator/templates/root/tool/cursor_audit.sh.tmpl new file mode 100644 index 0000000..0095e12 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/root/tool/cursor_audit.sh.tmpl @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# {{PROJECT_NAME}} — Cursor rule hygiene (generated by cursor_gen) +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CURSOR="${ROOT}/.cursor" +RULES="${CURSOR}/rules" + +echo "== cursor_audit (${ROOT}) ==" + +if [[ ! -d "$CURSOR" ]]; then + echo "ERROR: missing ${CURSOR}" + exit 1 +fi + +if [[ -d "$RULES" ]]; then + ac="$(grep -R "alwaysApply: true" "$RULES" --include='*.mdc' 2>/dev/null | wc -l | tr -d ' ')" + echo "alwaysApply: true occurrences in .cursor/rules: ${ac}" + echo " (expect a small number — meta/safety; prefer scoped globs for domain rules)" +else + echo "WARN: no ${RULES}" +fi + +if [[ -d "$RULES/features" ]]; then + shopt -s nullglob + for f in "$RULES/features"/*.mdc; do + base="$(basename "$f" .mdc)" + if [[ ! -d "${ROOT}/lib/features/${base}" ]] && [[ ! -d "${ROOT}/lib/feature_${base}" ]]; then + echo "WARN: rules/features/${base}.mdc has no obvious lib/features/${base} folder — update features.modules or lib layout" + fi + done + shopt -u nullglob +fi + +echo "OK — review warnings above after brief or folder renames" diff --git a/flutter-cursor-templates/generator/templates/rules/cicd/cicd.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/cicd/cicd.mdc.tmpl new file mode 100644 index 0000000..fc7fd7a --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/cicd/cicd.mdc.tmpl @@ -0,0 +1,29 @@ +--- +description: CI/CD, flavours, and quality gates for {{PROJECT_NAME}} +globs: [".github/**", "codemagic.yaml", "Makefile", "pubspec.yaml", "fastlane/**"] +alwaysApply: false +--- + +# CI/CD & flavours — {{PROJECT_NAME}} + +## Context +Pipeline and flavour setup must stay aligned with how the app is built and released. + +## Flavours +- Documented in `project-brief.yaml`: **{{FLAVORS_LIST}}** +- Use `--flavor` / `-t lib/main_.dart` (or your project’s entrypoints) consistently across local, CI, and store builds +- Configuration via `--dart-define` / `--dart-define-from-file` — **never** hardcode secrets in source + +## Quality gates (recommended stages) +- **Lint:** `dart format --set-exit-if-changed .` and `flutter analyze` +- **Unit + widget:** `flutter test` (with coverage if the team tracks it) +- **Goldens:** fixed device/locale; CI fails on drift unless intentionally updated +- **Smoke build:** e.g. `flutter build apk` or `flutter build ios --no-codesign` for the primary flavour +- **E2E:** when `testing.depth` includes e2e — {{E2E_TOOL}} on CI devices or Firebase Test Lab + +## Cursor integration +- After substantive edits: run analyze and relevant tests before PR +- For release-oriented tasks, use the **deploy** skill at `.cursor/skills/deploy/SKILL.md` when present + +## CI/CD tool +- Selected in brief: **{{CICD_TOOL}}** (`{{CICD_RAW}}`) diff --git a/flutter-cursor-templates/generator/templates/rules/features/_stub.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/features/_stub.mdc.tmpl new file mode 100644 index 0000000..a6961f1 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/features/_stub.mdc.tmpl @@ -0,0 +1,21 @@ +--- +description: "Feature module {{FEATURE_MODULE}} — contracts, boundaries, and ownership (fill after design)" +globs: ["lib/**/{{FEATURE_MODULE}}/**", "test/**/{{FEATURE_MODULE}}/**"] +alwaysApply: false +--- + +# Feature — {{FEATURE_MODULE_TITLE}} + +## Context +This stub was generated from `features.modules` in `project-brief.yaml`. Use it to capture **public contracts** (routes, DTOs, events) and **dependencies** for `{{FEATURE_MODULE}}` so agents do not invent cross-feature wiring. + +## Constraints +- List external dependencies (other features, packages, backend endpoints) explicitly +- Document invariants (auth required, idempotency, offline behavior) when known +- Update or delete this file when the module is removed or renamed — run `bash tool/cursor_audit.sh` to catch drift + +## Patterns +- Link to key entry points: primary screen(s), state holder(s), repository interface(s) + +## Anti-patterns +- Empty file left forever — either fill it or delete the module entry from the brief diff --git a/flutter-cursor-templates/generator/templates/rules/i18n/localization.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/i18n/localization.mdc.tmpl index dff5105..d646f1a 100644 --- a/flutter-cursor-templates/generator/templates/rules/i18n/localization.mdc.tmpl +++ b/flutter-cursor-templates/generator/templates/rules/i18n/localization.mdc.tmpl @@ -1,6 +1,7 @@ --- description: "Localization / i18n conventions for {{PROJECT_NAME}}" -alwaysApply: true +globs: ["lib/l10n/**", "lib/**/*.dart", "test/**/*.dart"] +alwaysApply: false --- # Localization Standards — {{PROJECT_NAME}} diff --git a/flutter-cursor-templates/generator/templates/rules/integrations/push-deeplink.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/integrations/push-deeplink.mdc.tmpl new file mode 100644 index 0000000..aa8f82a --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/integrations/push-deeplink.mdc.tmpl @@ -0,0 +1,21 @@ +--- +description: Push notifications and deep linking for {{PROJECT_NAME}} +globs: ["lib/**/*.dart", "android/**", "ios/**", "test/**/*.dart"] +alwaysApply: false +--- + +# Push & deep linking — {{PROJECT_NAME}} + +## Context +Special capabilities from `project-brief.yaml`: **{{SPECIAL_FEATURES}}**. Native and server configuration must stay consistent. + +## Constraints +- **Push:** request permissions through the platform flow; handle denial gracefully; no silent failures on token registration +- **Routing:** deep links and notification taps MUST go through **{{ROUTING}}** (no ad-hoc `Navigator` stacks for inbound links) +- **Payloads:** map FCM/APNs/Supabase payloads to domain models in the data layer — no JSON parsing scattered in widgets +- **iOS:** exercise push on a **physical device** when possible (simulator limitations) +- **Android:** declare required permissions explicitly; target API behaviour for POST_NOTIFICATIONS where applicable + +## Testing +- Unit-test payload → domain mapping and routing targets +- Integration/E2E: cold start, background, and foreground tap-to-open flows when `testing.depth` allows diff --git a/flutter-cursor-templates/generator/templates/rules/telemetry/usage-logging.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/telemetry/usage-logging.mdc.tmpl new file mode 100644 index 0000000..547ffd0 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/telemetry/usage-logging.mdc.tmpl @@ -0,0 +1,19 @@ +--- +description: "Policy for optional local AI usage logs under .cursor/telemetry/" +globs: [".cursor/telemetry/**"] +alwaysApply: false +--- + +# Telemetry — {{PROJECT_NAME}} + +## Context +When `telemetry_opt_in: true`, this repo may record **local-only** generation or usage notes under `.cursor/telemetry/`. This is **not** production analytics. + +## Constraints +- **Never** commit secrets, tokens, or PII into JSONL or shell history +- Prefer redacted summaries over raw prompts +- Add `.cursor/telemetry/*.jsonl` to `.gitignore` unless your team explicitly version-controls sanitized samples + +## Patterns +- Append one JSON object per line (JSONL) with ISO timestamps and event type +- Rotate or truncate files if they grow beyond a few MB diff --git a/flutter-cursor-templates/generator/templates/rules/theming/theming.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/theming/theming.mdc.tmpl new file mode 100644 index 0000000..971983c --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/theming/theming.mdc.tmpl @@ -0,0 +1,20 @@ +--- +description: Semantic colours and theme extensions for {{PROJECT_NAME}} +globs: ["lib/**/*.dart", "test/**/*.dart"] +alwaysApply: false +--- + +# Theming — {{PROJECT_NAME}} + +## Context +Theme variants from brief: **{{THEME_SUMMARY}}**.{{HIGH_CONTRAST_NOTE}} + +## Constraints +- Prefer **`ThemeExtension`** (or design-system equivalents) for app-specific colours and radii — avoid raw `Color(0xFF...)` in feature code except in central token definitions +- Use **`Theme.of(context).colorScheme`** / `TextTheme` for Material-aligned roles where appropriate +- **High contrast:** when supported, verify WCAG contrast for text and controls; never rely on colour alone for state (pair with icon or label){{HIGH_CONTRAST_UX_LINE}} +- Touch targets: respect platform minimums (e.g. ~48 logical pixels) for interactive widgets + +## Anti-patterns +- Hard-coded `Colors.*` scattered across feature widgets instead of theme tokens +- Ignoring `MediaQuery.of(context).platformBrightness` / high-contrast modes when the product claims support diff --git a/flutter-cursor-templates/generator/templates/rules/universal/flutter-core.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/universal/flutter-core.mdc.tmpl index bcafe6a..647e438 100644 --- a/flutter-cursor-templates/generator/templates/rules/universal/flutter-core.mdc.tmpl +++ b/flutter-cursor-templates/generator/templates/rules/universal/flutter-core.mdc.tmpl @@ -1,6 +1,7 @@ --- -description: "Core Flutter conventions for {{PROJECT_NAME}} — always applied" -alwaysApply: true +description: "Core Flutter conventions for {{PROJECT_NAME}}" +globs: ["lib/**/*.dart", "test/**/*.dart", "integration_test/**/*.dart"] +alwaysApply: false --- # Flutter Core Standards — {{PROJECT_NAME}} @@ -29,10 +30,7 @@ alwaysApply: true - Private members: `_camelCase` ## Imports -- Order: dart: → package: → relative -- Use relative imports within a feature; absolute for cross-feature -- Never import a feature's internal files from outside that feature - +{{IMPORT_POLICY_BLOCK}} ## Code quality - Max function length: 40 lines. Extract widgets and helpers aggressively - No `print()` in production code — use a logging package diff --git a/flutter-cursor-templates/generator/templates/rules/universal/project-context.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/universal/project-context.mdc.tmpl index 7d2f89c..5901ad4 100644 --- a/flutter-cursor-templates/generator/templates/rules/universal/project-context.mdc.tmpl +++ b/flutter-cursor-templates/generator/templates/rules/universal/project-context.mdc.tmpl @@ -1,6 +1,7 @@ --- -description: "Project context for {{PROJECT_NAME}} — always applied" -alwaysApply: true +description: "Stack summary and product context for {{PROJECT_NAME}}" +globs: ["project-brief.yaml", ".cursor/**/*.md", ".cursor/**/*.mdc", "pubspec.yaml"] +alwaysApply: false --- # Project Context — {{PROJECT_NAME}} diff --git a/flutter-cursor-templates/generator/templates/rules/universal/rule-authoring.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/universal/rule-authoring.mdc.tmpl new file mode 100644 index 0000000..1b1e520 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/universal/rule-authoring.mdc.tmpl @@ -0,0 +1,24 @@ +--- +description: How Cursor rules in this repository must be written and maintained. +globs: [".cursor/rules/**/*.mdc"] +alwaysApply: true +--- + +# Rule authoring — {{PROJECT_NAME}} + +## Context +Rules are version-controlled contracts for AI assistants. Poor rules waste context and silently steer every edit. + +## Constraints +- One focused concern per file; split broad topics instead of one mega-rule +- Every rule MUST have a clear `description` in frontmatter (one sentence) +- Prefer `alwaysApply: false` with **narrow** `globs` for domain rules — reserve `alwaysApply: true` for meta and safety +- `globs` must be as specific as possible — never `["**/*"]` unless tooling requires it +- Code samples in rules MUST be valid for this project (Dart/Flutter/YAML as appropriate) +- Deprecated guidance is removed, not left commented out +- Each substantive rule includes **Context** (why), **Constraints** (must/must not), and where helpful **Patterns** / **Anti-patterns** + +## Anti-patterns +- Domain rules (testing, l10n, a feature) with `alwaysApply: true` — burns context +- Rules with no concrete examples when the topic is code-facing +- Stale feature rules after modules are removed — run `tool/cursor_audit.sh` periodically diff --git a/flutter-cursor-templates/generator/templates/rules/universal/ui-ux-standards.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/universal/ui-ux-standards.mdc.tmpl index bfd07bb..2cde1db 100644 --- a/flutter-cursor-templates/generator/templates/rules/universal/ui-ux-standards.mdc.tmpl +++ b/flutter-cursor-templates/generator/templates/rules/universal/ui-ux-standards.mdc.tmpl @@ -1,6 +1,7 @@ --- -description: "UI/UX standards for {{PROJECT_NAME}} — always applied" -alwaysApply: true +description: "UI/UX standards for {{PROJECT_NAME}}" +globs: ["lib/**/*.dart", "test/**/*.dart", "integration_test/**/*.dart"] +alwaysApply: false --- # UI / UX Standards — {{PROJECT_NAME}} diff --git a/flutter-cursor-templates/generator/templates/telemetry/gitignore.tmpl b/flutter-cursor-templates/generator/templates/telemetry/gitignore.tmpl new file mode 100644 index 0000000..6e3f01a --- /dev/null +++ b/flutter-cursor-templates/generator/templates/telemetry/gitignore.tmpl @@ -0,0 +1,2 @@ +*.jsonl +*.log diff --git a/flutter-cursor-templates/generator/templates/telemetry/log.sh.tmpl b/flutter-cursor-templates/generator/templates/telemetry/log.sh.tmpl new file mode 100644 index 0000000..1910fc2 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/telemetry/log.sh.tmpl @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Append a JSONL usage line (local only). Requires jq when passing structured payload. +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +LOG="${ROOT}/telemetry/usage.jsonl" +mkdir -p "$(dirname "$LOG")" +echo "{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"event\":\"${1:-note}\",\"detail\":\"${2:-}\"}" >>"$LOG" diff --git a/flutter-cursor-templates/generator/test/generator_test.dart b/flutter-cursor-templates/generator/test/generator_test.dart index a71025b..61d381a 100644 --- a/flutter-cursor-templates/generator/test/generator_test.dart +++ b/flutter-cursor-templates/generator/test/generator_test.dart @@ -8,6 +8,7 @@ import '../src/resolver.dart'; import '../src/renderer.dart'; import '../src/validator.dart'; import '../src/models.dart'; +import '../src/mcp_json.dart'; void main() { // ─── Resolver tests ───────────────────────────────────────────────────────── @@ -17,10 +18,26 @@ void main() { final files = Resolver.resolve(kBlocCleanFirebaseBrief); // Universal — always present + expect(files.first, equals('rules/universal/rule-authoring')); expect(files, contains('rules/universal/flutter-core')); expect(files, contains('rules/universal/ui-ux-standards')); expect(files, contains('rules/universal/project-context')); + // CI/CD rule when cicd set or multiple flavors + expect(files, contains('rules/cicd/cicd')); + + // Feature stubs from brief + expect(files, contains('rules/features/auth')); + expect(files, contains('rules/features/home')); + expect(files, contains('rules/features/products')); + + // Cursor workspace extras + expect(files, contains('root/.cursorignore')); + expect(files, contains('root/tool/cursor_audit.sh')); + expect(files, contains('onboarding/ONBOARDING')); + expect(files, contains('commands/build')); + expect(files, contains('commands/debug-issue')); + // Security — always present (Pillar 5) expect(files, contains('rules/security/security-standards')); @@ -57,6 +74,9 @@ void main() { expect(files, containsNot('rules/architecture/feature_first')); expect(files, containsNot('rules/routing/getx_nav')); + expect(files, contains('commands/verify-change')); + expect(files, contains('commands/explain-code')); + expect(files, containsNot('config/mcp-json')); expect(files, contains('root/AGENTS.md')); expect(files, contains('root/lefthook.yaml')); }); @@ -72,6 +92,175 @@ void main() { expect(files, containsNot('agents/migration-agent')); }); + test('featureRuleKey sanitizes module names', () { + expect(Resolver.featureRuleKey('My Cart'), 'rules/features/my_cart'); + expect(Resolver.featureRuleKey('auth'), 'rules/features/auth'); + expect(Resolver.featureRuleKey(' '), isNull); + }); + + test('MCP template key when integrations.mcp.enabled', () { + final brief = ProjectBrief( + projectName: 'McpApp', + packageId: 'com.test.mcp', + description: '', + scale: 'small', + stateManagement: 'bloc', + routing: 'gorouter', + architecture: 'clean', + backends: ['firebase'], + auth: 'none', + platforms: ['ios'], + codegenTools: [], + flavors: ['dev'], + cicd: 'none', + testingDepth: 'unit_widget', + e2eTool: 'patrol', + designSource: 'none', + figmaUrl: '', + apiDocsFormat: 'none', + apiDocsPath: '', + referenceRepos: [], + localPaths: [], + featureModules: [], + specialFeatures: [], + i18nEnabled: false, + locales: ['en'], + mcpConfigEnabled: true, + ); + expect(Resolver.resolve(brief), contains('config/mcp-json')); + }); + + test('Push/deeplink rule when special features request it', () { + final brief = ProjectBrief( + projectName: 'PushApp', + packageId: 'com.test.push', + description: '', + scale: 'small', + stateManagement: 'bloc', + routing: 'gorouter', + architecture: 'clean', + backends: ['rest'], + auth: 'none', + platforms: ['ios'], + codegenTools: [], + flavors: ['dev'], + cicd: 'none', + testingDepth: 'unit_widget', + e2eTool: 'patrol', + designSource: 'none', + figmaUrl: '', + apiDocsFormat: 'none', + apiDocsPath: '', + referenceRepos: [], + localPaths: [], + featureModules: [], + specialFeatures: ['push_notifications'], + i18nEnabled: false, + locales: ['en'], + ); + expect(Resolver.resolve(brief), contains('rules/integrations/push-deeplink')); + }); + + test('Theming rule when high contrast requested', () { + final brief = ProjectBrief( + projectName: 'A11yApp', + packageId: 'com.test.a11y', + description: '', + scale: 'small', + stateManagement: 'bloc', + routing: 'gorouter', + architecture: 'clean', + backends: ['rest'], + auth: 'none', + platforms: ['ios'], + codegenTools: [], + flavors: ['dev'], + cicd: 'none', + testingDepth: 'unit_widget', + e2eTool: 'patrol', + designSource: 'none', + figmaUrl: '', + apiDocsFormat: 'none', + apiDocsPath: '', + referenceRepos: [], + localPaths: [], + featureModules: [], + specialFeatures: [], + i18nEnabled: false, + locales: ['en'], + themeVariants: const ['light', 'dark', 'high_contrast'], + ); + expect(Resolver.resolve(brief), contains('rules/theming/theming')); + }); + + test('Telemetry helpers when telemetry_opt_in', () { + final brief = ProjectBrief( + projectName: 'TelApp', + packageId: 'com.test.tel', + description: '', + scale: 'small', + stateManagement: 'bloc', + routing: 'gorouter', + architecture: 'clean', + backends: ['rest'], + auth: 'none', + platforms: ['ios'], + codegenTools: [], + flavors: ['dev'], + cicd: 'none', + testingDepth: 'unit_widget', + e2eTool: 'patrol', + designSource: 'none', + figmaUrl: '', + apiDocsFormat: 'none', + apiDocsPath: '', + referenceRepos: [], + localPaths: [], + featureModules: [], + specialFeatures: [], + i18nEnabled: false, + locales: ['en'], + telemetryOptIn: true, + ); + final files = Resolver.resolve(brief); + expect(files, contains('telemetry/gitignore')); + expect(files, contains('telemetry/log-sh')); + expect(files, contains('rules/telemetry/usage-logging')); + }); + + test('McpJsonBuilder minimal preset emits empty mcpServers', () { + const brief = ProjectBrief( + projectName: 'M', + packageId: 'com.m.m', + description: '', + scale: 'small', + stateManagement: 'bloc', + routing: 'gorouter', + architecture: 'clean', + backends: ['rest'], + auth: 'none', + platforms: ['ios'], + codegenTools: [], + flavors: ['dev'], + cicd: 'none', + testingDepth: 'unit_widget', + e2eTool: 'patrol', + designSource: 'none', + figmaUrl: '', + apiDocsFormat: 'none', + apiDocsPath: '', + referenceRepos: [], + localPaths: [], + featureModules: [], + specialFeatures: [], + i18nEnabled: false, + locales: ['en'], + mcpConfigEnabled: true, + mcpPreset: 'minimal', + ); + expect(McpJsonBuilder.build(brief), contains('"mcpServers": {}')); + }); + test('GetX + MVC includes migration-agent', () { final files = Resolver.resolve(kGetxMvcRestBrief); expect(files, contains('agents/migration-agent')); @@ -245,6 +434,51 @@ void main() { 'Default template dir should resolve real arch-guard.ts.tmpl'); expect(content, contains('arch-guard')); }); + + test('mcp.json renders from config/mcp-json key', () async { + final templateDir = _templateDir(); + if (!Directory(templateDir).existsSync()) { + markTestSkipped('Template directory not found'); + return; + } + final brief = ProjectBrief( + projectName: 'McpApp', + packageId: 'com.test.mcp', + description: '', + scale: 'small', + stateManagement: 'bloc', + routing: 'gorouter', + architecture: 'clean', + backends: const ['firebase'], + auth: 'none', + platforms: const ['ios'], + codegenTools: const [], + flavors: const ['dev'], + cicd: 'none', + testingDepth: 'unit_widget', + e2eTool: 'patrol', + designSource: 'none', + figmaUrl: '', + apiDocsFormat: 'none', + apiDocsPath: '', + referenceRepos: const [], + localPaths: const [], + featureModules: const [], + specialFeatures: const [], + i18nEnabled: false, + locales: const ['en'], + mcpConfigEnabled: true, + ); + final rendered = await Renderer.render( + brief: brief, + templateFiles: const ['config/mcp-json'], + templateSrc: templateDir, + ); + final json = rendered['mcp.json']!; + expect(json, contains('mcpServers')); + expect(json, contains('filesystem')); + expect(json, contains('firebase')); + }); }); // ─── Validator tests ───────────────────────────────────────────────────────── diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/ONBOARDING.md b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/ONBOARDING.md new file mode 100644 index 0000000..0eaa929 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/ONBOARDING.md @@ -0,0 +1,20 @@ +# Cursor — TestApp + +## Quick start + +1. Open this repo in **Cursor** so `.cursor/rules/` and `.cursor/skills/` load automatically. +2. Read **`rules/universal/rule-authoring.mdc`** — how we maintain AI rules. +3. Stack and product context live in **`project-brief.yaml`** at the repo root; regenerate `.cursor/` with `cursor_gen` after changing it. +4. **Slash skills** (primary workflows): open the Command Palette and use the project commands, or reference: + - `.cursor/skills/build/SKILL.md` — end-to-end feature implementation + - `.cursor/skills/debug-issue/SKILL.md` — structured debugging + - `.cursor/skills/verify-change/SKILL.md` — pre-PR verification + - `.cursor/skills/explain-code/SKILL.md` — explain-only walkthroughs +5. **Agents** (`.cursor/agents/*.mdc`) are reusable reviewer personas — attach when asking for code review or focused passes. +6. **Custom overrides**: files under `.cursor/custom/` and `CURSOR:CUSTOM` blocks in generated files are preserved on `cursor_gen --refresh` (see generator docs). + +## Optional + +- **`integrations.mcp.enabled`** in `project-brief.yaml` — generates `.cursor/mcp.json` with env-based server entries (no secrets in git). +- **`telemetry_opt_in: true`** — emits local telemetry helpers under `.cursor/telemetry/` (never commit usage logs if you enable logging). +- Run **`bash tool/cursor_audit.sh`** from the project root periodically to catch stale feature rules and overly broad `alwaysApply` usage. diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/__root__/.cursorignore b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/__root__/.cursorignore new file mode 100644 index 0000000..c4e056e --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/__root__/.cursorignore @@ -0,0 +1,27 @@ +# Build artefacts +build/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +*.g.dart +*.freezed.dart +*.gr.dart +*.config.dart + +# Secrets +.env +.env.* +firebase_options.dart +google-services.json +GoogleService-Info.plist + +# Large binary assets +assets/fonts/ +assets/videos/ +*.aab +*.apk +*.ipa + +# IDE +.idea/ +*.iml diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/__root__/AGENTS.md b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/__root__/AGENTS.md index 0da27df..10122d3 100644 --- a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/__root__/AGENTS.md +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/__root__/AGENTS.md @@ -3,4 +3,5 @@ Repo-level notes for AI assistants. Authoritative stack and conventions are in `project-brief.yaml` and `.cursor/` (regenerate with `cursor_gen` from the project root). - **Package:** `com.test.testapp` -- **Slash skills** (see `.cursor/skills/`): `/build`, `/debug`, `/verify`, `/explain` +- **Onboarding:** `.cursor/ONBOARDING.md` — layout, slash commands → skills, MCP opt-in, audits +- **Slash skills** (see `.cursor/skills/`): `/build`, `/debug`, `/verify`, `/explain` (see `.cursor/commands/*.md`) diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/__root__/lefthook.yaml b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/__root__/lefthook.yaml index d781613..2bbd562 100644 --- a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/__root__/lefthook.yaml +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/__root__/lefthook.yaml @@ -1,4 +1,5 @@ # TestApp — generated by cursor_gen; adjust commands to your repo +# Optional: run `bash tool/cursor_audit.sh` after changing features.modules or rule files. pre-commit: commands: flutter-analyze: diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/__root__/tool/cursor_audit.sh b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/__root__/tool/cursor_audit.sh new file mode 100644 index 0000000..293a877 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/__root__/tool/cursor_audit.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# TestApp — Cursor rule hygiene (generated by cursor_gen) +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CURSOR="${ROOT}/.cursor" +RULES="${CURSOR}/rules" + +echo "== cursor_audit (${ROOT}) ==" + +if [[ ! -d "$CURSOR" ]]; then + echo "ERROR: missing ${CURSOR}" + exit 1 +fi + +if [[ -d "$RULES" ]]; then + ac="$(grep -R "alwaysApply: true" "$RULES" --include='*.mdc' 2>/dev/null | wc -l | tr -d ' ')" + echo "alwaysApply: true occurrences in .cursor/rules: ${ac}" + echo " (expect a small number — meta/safety; prefer scoped globs for domain rules)" +else + echo "WARN: no ${RULES}" +fi + +if [[ -d "$RULES/features" ]]; then + shopt -s nullglob + for f in "$RULES/features"/*.mdc; do + base="$(basename "$f" .mdc)" + if [[ ! -d "${ROOT}/lib/features/${base}" ]] && [[ ! -d "${ROOT}/lib/feature_${base}" ]]; then + echo "WARN: rules/features/${base}.mdc has no obvious lib/features/${base} folder — update features.modules or lib layout" + fi + done + shopt -u nullglob +fi + +echo "OK — review warnings above after brief or folder renames" diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/commands/build.md b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/commands/build.md new file mode 100644 index 0000000..4dc2925 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/commands/build.md @@ -0,0 +1 @@ +End-to-end feature implementation (research, TDD, integration tests, verification). Follow the workflow and constraints in `@file:.cursor/skills/build/SKILL.md`. Use `project-brief.yaml` as the source of truth for stack and platforms. diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/commands/debug-issue.md b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/commands/debug-issue.md new file mode 100644 index 0000000..21a4fbb --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/commands/debug-issue.md @@ -0,0 +1 @@ +Structured bug triage and evidence-first debugging. Follow `@file:.cursor/skills/debug-issue/SKILL.md`. Gather reproduction steps, logs, and failing commands before proposing fixes. diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/commands/explain-code.md b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/commands/explain-code.md new file mode 100644 index 0000000..40b10cc --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/commands/explain-code.md @@ -0,0 +1 @@ +Explain-only walkthrough of code paths and stack behavior (no edits). Follow `@file:.cursor/skills/explain-code/SKILL.md`. Do not modify source files unless the user explicitly asks. diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/commands/verify-change.md b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/commands/verify-change.md new file mode 100644 index 0000000..a2e8bb5 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/commands/verify-change.md @@ -0,0 +1 @@ +Pre-PR verification checklist (analyze, tests, hooks) without full /build lifecycle. Follow `@file:.cursor/skills/verify-change/SKILL.md` for the change in scope. diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/cicd/cicd.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/cicd/cicd.mdc new file mode 100644 index 0000000..1798fd4 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/cicd/cicd.mdc @@ -0,0 +1,29 @@ +--- +description: CI/CD, flavours, and quality gates for TestApp +globs: [".github/**", "codemagic.yaml", "Makefile", "pubspec.yaml", "fastlane/**"] +alwaysApply: false +--- + +# CI/CD & flavours — TestApp + +## Context +Pipeline and flavour setup must stay aligned with how the app is built and released. + +## Flavours +- Documented in `project-brief.yaml`: **dev, prod** +- Use `--flavor` / `-t lib/main_.dart` (or your project’s entrypoints) consistently across local, CI, and store builds +- Configuration via `--dart-define` / `--dart-define-from-file` — **never** hardcode secrets in source + +## Quality gates (recommended stages) +- **Lint:** `dart format --set-exit-if-changed .` and `flutter analyze` +- **Unit + widget:** `flutter test` (with coverage if the team tracks it) +- **Goldens:** fixed device/locale; CI fails on drift unless intentionally updated +- **Smoke build:** e.g. `flutter build apk` or `flutter build ios --no-codesign` for the primary flavour +- **E2E:** when `testing.depth` includes e2e — patrol on CI devices or Firebase Test Lab + +## Cursor integration +- After substantive edits: run analyze and relevant tests before PR +- For release-oriented tasks, use the **deploy** skill at `.cursor/skills/deploy/SKILL.md` when present + +## CI/CD tool +- Selected in brief: **GitHub Actions** (`github_actions`) diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/features/auth.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/features/auth.mdc new file mode 100644 index 0000000..090ec1c --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/features/auth.mdc @@ -0,0 +1,21 @@ +--- +description: "Feature module auth — contracts, boundaries, and ownership (fill after design)" +globs: ["lib/**/auth/**", "test/**/auth/**"] +alwaysApply: false +--- + +# Feature — Auth + +## Context +This stub was generated from `features.modules` in `project-brief.yaml`. Use it to capture **public contracts** (routes, DTOs, events) and **dependencies** for `auth` so agents do not invent cross-feature wiring. + +## Constraints +- List external dependencies (other features, packages, backend endpoints) explicitly +- Document invariants (auth required, idempotency, offline behavior) when known +- Update or delete this file when the module is removed or renamed — run `bash tool/cursor_audit.sh` to catch drift + +## Patterns +- Link to key entry points: primary screen(s), state holder(s), repository interface(s) + +## Anti-patterns +- Empty file left forever — either fill it or delete the module entry from the brief diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/features/home.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/features/home.mdc new file mode 100644 index 0000000..662dc84 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/features/home.mdc @@ -0,0 +1,21 @@ +--- +description: "Feature module home — contracts, boundaries, and ownership (fill after design)" +globs: ["lib/**/home/**", "test/**/home/**"] +alwaysApply: false +--- + +# Feature — Home + +## Context +This stub was generated from `features.modules` in `project-brief.yaml`. Use it to capture **public contracts** (routes, DTOs, events) and **dependencies** for `home` so agents do not invent cross-feature wiring. + +## Constraints +- List external dependencies (other features, packages, backend endpoints) explicitly +- Document invariants (auth required, idempotency, offline behavior) when known +- Update or delete this file when the module is removed or renamed — run `bash tool/cursor_audit.sh` to catch drift + +## Patterns +- Link to key entry points: primary screen(s), state holder(s), repository interface(s) + +## Anti-patterns +- Empty file left forever — either fill it or delete the module entry from the brief diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/features/products.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/features/products.mdc new file mode 100644 index 0000000..679a565 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/features/products.mdc @@ -0,0 +1,21 @@ +--- +description: "Feature module products — contracts, boundaries, and ownership (fill after design)" +globs: ["lib/**/products/**", "test/**/products/**"] +alwaysApply: false +--- + +# Feature — Products + +## Context +This stub was generated from `features.modules` in `project-brief.yaml`. Use it to capture **public contracts** (routes, DTOs, events) and **dependencies** for `products` so agents do not invent cross-feature wiring. + +## Constraints +- List external dependencies (other features, packages, backend endpoints) explicitly +- Document invariants (auth required, idempotency, offline behavior) when known +- Update or delete this file when the module is removed or renamed — run `bash tool/cursor_audit.sh` to catch drift + +## Patterns +- Link to key entry points: primary screen(s), state holder(s), repository interface(s) + +## Anti-patterns +- Empty file left forever — either fill it or delete the module entry from the brief diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/flutter-core.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/flutter-core.mdc index 332717b..6870a55 100644 --- a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/flutter-core.mdc +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/flutter-core.mdc @@ -1,6 +1,7 @@ --- -description: "Core Flutter conventions for TestApp — always applied" -alwaysApply: true +description: "Core Flutter conventions for TestApp" +globs: ["lib/**/*.dart", "test/**/*.dart", "integration_test/**/*.dart"] +alwaysApply: false --- # Flutter Core Standards — TestApp @@ -29,9 +30,9 @@ alwaysApply: true - Private members: `_camelCase` ## Imports -- Order: dart: → package: → relative -- Use relative imports within a feature; absolute for cross-feature -- Never import a feature's internal files from outside that feature +### Imports (strict — `conventions.strict_package_imports: true`) +- Use `package:/...` imports everywhere in `lib/` and `test/` — **no** relative `../` across feature boundaries (the brief `project.package` id is often the app bundle id; prefer the **pubspec.yaml `name`** for Dart imports) +- Barrel files (`index.dart`) at feature roots; do not wildcard re-export third-party packages ## Code quality - Max function length: 40 lines. Extract widgets and helpers aggressively diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/project-context.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/project-context.mdc index 97ea332..b2f6f95 100644 --- a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/project-context.mdc +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/project-context.mdc @@ -1,6 +1,7 @@ --- -description: "Project context for TestApp — always applied" -alwaysApply: true +description: "Stack summary and product context for TestApp" +globs: ["project-brief.yaml", ".cursor/**/*.md", ".cursor/**/*.mdc", "pubspec.yaml"] +alwaysApply: false --- # Project Context — TestApp diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/rule-authoring.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/rule-authoring.mdc new file mode 100644 index 0000000..1b2fc41 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/rule-authoring.mdc @@ -0,0 +1,24 @@ +--- +description: How Cursor rules in this repository must be written and maintained. +globs: [".cursor/rules/**/*.mdc"] +alwaysApply: true +--- + +# Rule authoring — TestApp + +## Context +Rules are version-controlled contracts for AI assistants. Poor rules waste context and silently steer every edit. + +## Constraints +- One focused concern per file; split broad topics instead of one mega-rule +- Every rule MUST have a clear `description` in frontmatter (one sentence) +- Prefer `alwaysApply: false` with **narrow** `globs` for domain rules — reserve `alwaysApply: true` for meta and safety +- `globs` must be as specific as possible — never `["**/*"]` unless tooling requires it +- Code samples in rules MUST be valid for this project (Dart/Flutter/YAML as appropriate) +- Deprecated guidance is removed, not left commented out +- Each substantive rule includes **Context** (why), **Constraints** (must/must not), and where helpful **Patterns** / **Anti-patterns** + +## Anti-patterns +- Domain rules (testing, l10n, a feature) with `alwaysApply: true` — burns context +- Rules with no concrete examples when the topic is code-facing +- Stale feature rules after modules are removed — run `tool/cursor_audit.sh` periodically diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/ui-ux-standards.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/ui-ux-standards.mdc index 3929020..5c1894a 100644 --- a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/ui-ux-standards.mdc +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/ui-ux-standards.mdc @@ -1,6 +1,7 @@ --- -description: "UI/UX standards for TestApp — always applied" -alwaysApply: true +description: "UI/UX standards for TestApp" +globs: ["lib/**/*.dart", "test/**/*.dart", "integration_test/**/*.dart"] +alwaysApply: false --- # UI / UX Standards — TestApp diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/ONBOARDING.md b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/ONBOARDING.md new file mode 100644 index 0000000..eb257f6 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/ONBOARDING.md @@ -0,0 +1,20 @@ +# Cursor — LegacyApp + +## Quick start + +1. Open this repo in **Cursor** so `.cursor/rules/` and `.cursor/skills/` load automatically. +2. Read **`rules/universal/rule-authoring.mdc`** — how we maintain AI rules. +3. Stack and product context live in **`project-brief.yaml`** at the repo root; regenerate `.cursor/` with `cursor_gen` after changing it. +4. **Slash skills** (primary workflows): open the Command Palette and use the project commands, or reference: + - `.cursor/skills/build/SKILL.md` — end-to-end feature implementation + - `.cursor/skills/debug-issue/SKILL.md` — structured debugging + - `.cursor/skills/verify-change/SKILL.md` — pre-PR verification + - `.cursor/skills/explain-code/SKILL.md` — explain-only walkthroughs +5. **Agents** (`.cursor/agents/*.mdc`) are reusable reviewer personas — attach when asking for code review or focused passes. +6. **Custom overrides**: files under `.cursor/custom/` and `CURSOR:CUSTOM` blocks in generated files are preserved on `cursor_gen --refresh` (see generator docs). + +## Optional + +- **`integrations.mcp.enabled`** in `project-brief.yaml` — generates `.cursor/mcp.json` with env-based server entries (no secrets in git). +- **`telemetry_opt_in: true`** — emits local telemetry helpers under `.cursor/telemetry/` (never commit usage logs if you enable logging). +- Run **`bash tool/cursor_audit.sh`** from the project root periodically to catch stale feature rules and overly broad `alwaysApply` usage. diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/__root__/.cursorignore b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/__root__/.cursorignore new file mode 100644 index 0000000..c4e056e --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/__root__/.cursorignore @@ -0,0 +1,27 @@ +# Build artefacts +build/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +*.g.dart +*.freezed.dart +*.gr.dart +*.config.dart + +# Secrets +.env +.env.* +firebase_options.dart +google-services.json +GoogleService-Info.plist + +# Large binary assets +assets/fonts/ +assets/videos/ +*.aab +*.apk +*.ipa + +# IDE +.idea/ +*.iml diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/__root__/AGENTS.md b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/__root__/AGENTS.md index 2637a99..c0e0f6d 100644 --- a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/__root__/AGENTS.md +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/__root__/AGENTS.md @@ -3,4 +3,5 @@ Repo-level notes for AI assistants. Authoritative stack and conventions are in `project-brief.yaml` and `.cursor/` (regenerate with `cursor_gen` from the project root). - **Package:** `com.test.legacy` -- **Slash skills** (see `.cursor/skills/`): `/build`, `/debug`, `/verify`, `/explain` +- **Onboarding:** `.cursor/ONBOARDING.md` — layout, slash commands → skills, MCP opt-in, audits +- **Slash skills** (see `.cursor/skills/`): `/build`, `/debug`, `/verify`, `/explain` (see `.cursor/commands/*.md`) diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/__root__/lefthook.yaml b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/__root__/lefthook.yaml index 892cb74..81733e3 100644 --- a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/__root__/lefthook.yaml +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/__root__/lefthook.yaml @@ -1,4 +1,5 @@ # LegacyApp — generated by cursor_gen; adjust commands to your repo +# Optional: run `bash tool/cursor_audit.sh` after changing features.modules or rule files. pre-commit: commands: flutter-analyze: diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/__root__/tool/cursor_audit.sh b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/__root__/tool/cursor_audit.sh new file mode 100644 index 0000000..7bd1c59 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/__root__/tool/cursor_audit.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# LegacyApp — Cursor rule hygiene (generated by cursor_gen) +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CURSOR="${ROOT}/.cursor" +RULES="${CURSOR}/rules" + +echo "== cursor_audit (${ROOT}) ==" + +if [[ ! -d "$CURSOR" ]]; then + echo "ERROR: missing ${CURSOR}" + exit 1 +fi + +if [[ -d "$RULES" ]]; then + ac="$(grep -R "alwaysApply: true" "$RULES" --include='*.mdc' 2>/dev/null | wc -l | tr -d ' ')" + echo "alwaysApply: true occurrences in .cursor/rules: ${ac}" + echo " (expect a small number — meta/safety; prefer scoped globs for domain rules)" +else + echo "WARN: no ${RULES}" +fi + +if [[ -d "$RULES/features" ]]; then + shopt -s nullglob + for f in "$RULES/features"/*.mdc; do + base="$(basename "$f" .mdc)" + if [[ ! -d "${ROOT}/lib/features/${base}" ]] && [[ ! -d "${ROOT}/lib/feature_${base}" ]]; then + echo "WARN: rules/features/${base}.mdc has no obvious lib/features/${base} folder — update features.modules or lib layout" + fi + done + shopt -u nullglob +fi + +echo "OK — review warnings above after brief or folder renames" diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/commands/build.md b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/commands/build.md new file mode 100644 index 0000000..4dc2925 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/commands/build.md @@ -0,0 +1 @@ +End-to-end feature implementation (research, TDD, integration tests, verification). Follow the workflow and constraints in `@file:.cursor/skills/build/SKILL.md`. Use `project-brief.yaml` as the source of truth for stack and platforms. diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/commands/debug-issue.md b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/commands/debug-issue.md new file mode 100644 index 0000000..21a4fbb --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/commands/debug-issue.md @@ -0,0 +1 @@ +Structured bug triage and evidence-first debugging. Follow `@file:.cursor/skills/debug-issue/SKILL.md`. Gather reproduction steps, logs, and failing commands before proposing fixes. diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/commands/explain-code.md b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/commands/explain-code.md new file mode 100644 index 0000000..40b10cc --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/commands/explain-code.md @@ -0,0 +1 @@ +Explain-only walkthrough of code paths and stack behavior (no edits). Follow `@file:.cursor/skills/explain-code/SKILL.md`. Do not modify source files unless the user explicitly asks. diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/commands/verify-change.md b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/commands/verify-change.md new file mode 100644 index 0000000..a2e8bb5 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/commands/verify-change.md @@ -0,0 +1 @@ +Pre-PR verification checklist (analyze, tests, hooks) without full /build lifecycle. Follow `@file:.cursor/skills/verify-change/SKILL.md` for the change in scope. diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/mcp.json b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/mcp.json new file mode 100644 index 0000000..96c63b5 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/mcp.json @@ -0,0 +1,57 @@ +{ + "mcpServers": { + "filesystem": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem", + "." + ], + "description": "Read/write project files under the workspace root" + }, + "flutter-docs": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-fetch" + ], + "env": { + "BASE_URL": "https://api.flutter.dev/flutter" + }, + "description": "Flutter API documentation lookup" + }, + "dart-pub": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-fetch" + ], + "env": { + "BASE_URL": "https://pub.dev/api" + }, + "description": "Pub.dev package metadata and versions" + }, + "github": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-github" + ], + "env": { + "GITHUB_TOKEN": "${GITHUB_TOKEN}" + }, + "description": "GitHub issues and PR context (requires GITHUB_TOKEN)" + }, + "openapi-ref": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-fetch" + ], + "env": { + "OPENAPI_SPEC_URL": "${OPENAPI_SPEC_URL}" + }, + "description": "Optional fetch MCP — point OPENAPI_SPEC_URL at a hosted spec or file:// URL you expose locally" + } + } +} \ No newline at end of file diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/cicd/cicd.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/cicd/cicd.mdc new file mode 100644 index 0000000..487dbbe --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/cicd/cicd.mdc @@ -0,0 +1,29 @@ +--- +description: CI/CD, flavours, and quality gates for LegacyApp +globs: [".github/**", "codemagic.yaml", "Makefile", "pubspec.yaml", "fastlane/**"] +alwaysApply: false +--- + +# CI/CD & flavours — LegacyApp + +## Context +Pipeline and flavour setup must stay aligned with how the app is built and released. + +## Flavours +- Documented in `project-brief.yaml`: **dev, prod** +- Use `--flavor` / `-t lib/main_.dart` (or your project’s entrypoints) consistently across local, CI, and store builds +- Configuration via `--dart-define` / `--dart-define-from-file` — **never** hardcode secrets in source + +## Quality gates (recommended stages) +- **Lint:** `dart format --set-exit-if-changed .` and `flutter analyze` +- **Unit + widget:** `flutter test` (with coverage if the team tracks it) +- **Goldens:** fixed device/locale; CI fails on drift unless intentionally updated +- **Smoke build:** e.g. `flutter build apk` or `flutter build ios --no-codesign` for the primary flavour +- **E2E:** when `testing.depth` includes e2e — patrol on CI devices or Firebase Test Lab + +## Cursor integration +- After substantive edits: run analyze and relevant tests before PR +- For release-oriented tasks, use the **deploy** skill at `.cursor/skills/deploy/SKILL.md` when present + +## CI/CD tool +- Selected in brief: **Codemagic** (`codemagic`) diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/features/auth.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/features/auth.mdc new file mode 100644 index 0000000..090ec1c --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/features/auth.mdc @@ -0,0 +1,21 @@ +--- +description: "Feature module auth — contracts, boundaries, and ownership (fill after design)" +globs: ["lib/**/auth/**", "test/**/auth/**"] +alwaysApply: false +--- + +# Feature — Auth + +## Context +This stub was generated from `features.modules` in `project-brief.yaml`. Use it to capture **public contracts** (routes, DTOs, events) and **dependencies** for `auth` so agents do not invent cross-feature wiring. + +## Constraints +- List external dependencies (other features, packages, backend endpoints) explicitly +- Document invariants (auth required, idempotency, offline behavior) when known +- Update or delete this file when the module is removed or renamed — run `bash tool/cursor_audit.sh` to catch drift + +## Patterns +- Link to key entry points: primary screen(s), state holder(s), repository interface(s) + +## Anti-patterns +- Empty file left forever — either fill it or delete the module entry from the brief diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/features/dashboard.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/features/dashboard.mdc new file mode 100644 index 0000000..8bef3c1 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/features/dashboard.mdc @@ -0,0 +1,21 @@ +--- +description: "Feature module dashboard — contracts, boundaries, and ownership (fill after design)" +globs: ["lib/**/dashboard/**", "test/**/dashboard/**"] +alwaysApply: false +--- + +# Feature — Dashboard + +## Context +This stub was generated from `features.modules` in `project-brief.yaml`. Use it to capture **public contracts** (routes, DTOs, events) and **dependencies** for `dashboard` so agents do not invent cross-feature wiring. + +## Constraints +- List external dependencies (other features, packages, backend endpoints) explicitly +- Document invariants (auth required, idempotency, offline behavior) when known +- Update or delete this file when the module is removed or renamed — run `bash tool/cursor_audit.sh` to catch drift + +## Patterns +- Link to key entry points: primary screen(s), state holder(s), repository interface(s) + +## Anti-patterns +- Empty file left forever — either fill it or delete the module entry from the brief diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/flutter-core.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/flutter-core.mdc index d54135b..966d627 100644 --- a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/flutter-core.mdc +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/flutter-core.mdc @@ -1,6 +1,7 @@ --- -description: "Core Flutter conventions for LegacyApp — always applied" -alwaysApply: true +description: "Core Flutter conventions for LegacyApp" +globs: ["lib/**/*.dart", "test/**/*.dart", "integration_test/**/*.dart"] +alwaysApply: false --- # Flutter Core Standards — LegacyApp @@ -29,9 +30,10 @@ alwaysApply: true - Private members: `_camelCase` ## Imports -- Order: dart: → package: → relative -- Use relative imports within a feature; absolute for cross-feature -- Never import a feature's internal files from outside that feature +### Imports (default) +- Order: `dart:` → `package:` → relative +- Relative imports are allowed **within** the same feature directory; use `package:` imports for cross-feature code +- Never import another feature's internals from outside that feature ## Code quality - Max function length: 40 lines. Extract widgets and helpers aggressively diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/project-context.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/project-context.mdc index 3b188bd..7405f5e 100644 --- a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/project-context.mdc +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/project-context.mdc @@ -1,6 +1,7 @@ --- -description: "Project context for LegacyApp — always applied" -alwaysApply: true +description: "Stack summary and product context for LegacyApp" +globs: ["project-brief.yaml", ".cursor/**/*.md", ".cursor/**/*.mdc", "pubspec.yaml"] +alwaysApply: false --- # Project Context — LegacyApp diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/rule-authoring.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/rule-authoring.mdc new file mode 100644 index 0000000..3b488ad --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/rule-authoring.mdc @@ -0,0 +1,24 @@ +--- +description: How Cursor rules in this repository must be written and maintained. +globs: [".cursor/rules/**/*.mdc"] +alwaysApply: true +--- + +# Rule authoring — LegacyApp + +## Context +Rules are version-controlled contracts for AI assistants. Poor rules waste context and silently steer every edit. + +## Constraints +- One focused concern per file; split broad topics instead of one mega-rule +- Every rule MUST have a clear `description` in frontmatter (one sentence) +- Prefer `alwaysApply: false` with **narrow** `globs` for domain rules — reserve `alwaysApply: true` for meta and safety +- `globs` must be as specific as possible — never `["**/*"]` unless tooling requires it +- Code samples in rules MUST be valid for this project (Dart/Flutter/YAML as appropriate) +- Deprecated guidance is removed, not left commented out +- Each substantive rule includes **Context** (why), **Constraints** (must/must not), and where helpful **Patterns** / **Anti-patterns** + +## Anti-patterns +- Domain rules (testing, l10n, a feature) with `alwaysApply: true` — burns context +- Rules with no concrete examples when the topic is code-facing +- Stale feature rules after modules are removed — run `tool/cursor_audit.sh` periodically diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/ui-ux-standards.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/ui-ux-standards.mdc index cc118d2..f8bdafb 100644 --- a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/ui-ux-standards.mdc +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/ui-ux-standards.mdc @@ -1,6 +1,7 @@ --- -description: "UI/UX standards for LegacyApp — always applied" -alwaysApply: true +description: "UI/UX standards for LegacyApp" +globs: ["lib/**/*.dart", "test/**/*.dart", "integration_test/**/*.dart"] +alwaysApply: false --- # UI / UX Standards — LegacyApp diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/ONBOARDING.md b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/ONBOARDING.md new file mode 100644 index 0000000..ca33cb6 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/ONBOARDING.md @@ -0,0 +1,20 @@ +# Cursor — TaskFlow + +## Quick start + +1. Open this repo in **Cursor** so `.cursor/rules/` and `.cursor/skills/` load automatically. +2. Read **`rules/universal/rule-authoring.mdc`** — how we maintain AI rules. +3. Stack and product context live in **`project-brief.yaml`** at the repo root; regenerate `.cursor/` with `cursor_gen` after changing it. +4. **Slash skills** (primary workflows): open the Command Palette and use the project commands, or reference: + - `.cursor/skills/build/SKILL.md` — end-to-end feature implementation + - `.cursor/skills/debug-issue/SKILL.md` — structured debugging + - `.cursor/skills/verify-change/SKILL.md` — pre-PR verification + - `.cursor/skills/explain-code/SKILL.md` — explain-only walkthroughs +5. **Agents** (`.cursor/agents/*.mdc`) are reusable reviewer personas — attach when asking for code review or focused passes. +6. **Custom overrides**: files under `.cursor/custom/` and `CURSOR:CUSTOM` blocks in generated files are preserved on `cursor_gen --refresh` (see generator docs). + +## Optional + +- **`integrations.mcp.enabled`** in `project-brief.yaml` — generates `.cursor/mcp.json` with env-based server entries (no secrets in git). +- **`telemetry_opt_in: true`** — emits local telemetry helpers under `.cursor/telemetry/` (never commit usage logs if you enable logging). +- Run **`bash tool/cursor_audit.sh`** from the project root periodically to catch stale feature rules and overly broad `alwaysApply` usage. diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/__root__/.cursorignore b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/__root__/.cursorignore new file mode 100644 index 0000000..c4e056e --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/__root__/.cursorignore @@ -0,0 +1,27 @@ +# Build artefacts +build/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +*.g.dart +*.freezed.dart +*.gr.dart +*.config.dart + +# Secrets +.env +.env.* +firebase_options.dart +google-services.json +GoogleService-Info.plist + +# Large binary assets +assets/fonts/ +assets/videos/ +*.aab +*.apk +*.ipa + +# IDE +.idea/ +*.iml diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/__root__/AGENTS.md b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/__root__/AGENTS.md index 471a0cb..1e7bbb3 100644 --- a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/__root__/AGENTS.md +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/__root__/AGENTS.md @@ -3,4 +3,5 @@ Repo-level notes for AI assistants. Authoritative stack and conventions are in `project-brief.yaml` and `.cursor/` (regenerate with `cursor_gen` from the project root). - **Package:** `com.test.taskflow` -- **Slash skills** (see `.cursor/skills/`): `/build`, `/debug`, `/verify`, `/explain` +- **Onboarding:** `.cursor/ONBOARDING.md` — layout, slash commands → skills, MCP opt-in, audits +- **Slash skills** (see `.cursor/skills/`): `/build`, `/debug`, `/verify`, `/explain` (see `.cursor/commands/*.md`) diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/__root__/lefthook.yaml b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/__root__/lefthook.yaml index 73d2045..baced12 100644 --- a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/__root__/lefthook.yaml +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/__root__/lefthook.yaml @@ -1,4 +1,5 @@ # TaskFlow — generated by cursor_gen; adjust commands to your repo +# Optional: run `bash tool/cursor_audit.sh` after changing features.modules or rule files. pre-commit: commands: flutter-analyze: diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/__root__/tool/cursor_audit.sh b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/__root__/tool/cursor_audit.sh new file mode 100644 index 0000000..1742bf3 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/__root__/tool/cursor_audit.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# TaskFlow — Cursor rule hygiene (generated by cursor_gen) +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CURSOR="${ROOT}/.cursor" +RULES="${CURSOR}/rules" + +echo "== cursor_audit (${ROOT}) ==" + +if [[ ! -d "$CURSOR" ]]; then + echo "ERROR: missing ${CURSOR}" + exit 1 +fi + +if [[ -d "$RULES" ]]; then + ac="$(grep -R "alwaysApply: true" "$RULES" --include='*.mdc' 2>/dev/null | wc -l | tr -d ' ')" + echo "alwaysApply: true occurrences in .cursor/rules: ${ac}" + echo " (expect a small number — meta/safety; prefer scoped globs for domain rules)" +else + echo "WARN: no ${RULES}" +fi + +if [[ -d "$RULES/features" ]]; then + shopt -s nullglob + for f in "$RULES/features"/*.mdc; do + base="$(basename "$f" .mdc)" + if [[ ! -d "${ROOT}/lib/features/${base}" ]] && [[ ! -d "${ROOT}/lib/feature_${base}" ]]; then + echo "WARN: rules/features/${base}.mdc has no obvious lib/features/${base} folder — update features.modules or lib layout" + fi + done + shopt -u nullglob +fi + +echo "OK — review warnings above after brief or folder renames" diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/commands/build.md b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/commands/build.md new file mode 100644 index 0000000..4dc2925 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/commands/build.md @@ -0,0 +1 @@ +End-to-end feature implementation (research, TDD, integration tests, verification). Follow the workflow and constraints in `@file:.cursor/skills/build/SKILL.md`. Use `project-brief.yaml` as the source of truth for stack and platforms. diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/commands/debug-issue.md b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/commands/debug-issue.md new file mode 100644 index 0000000..21a4fbb --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/commands/debug-issue.md @@ -0,0 +1 @@ +Structured bug triage and evidence-first debugging. Follow `@file:.cursor/skills/debug-issue/SKILL.md`. Gather reproduction steps, logs, and failing commands before proposing fixes. diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/commands/explain-code.md b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/commands/explain-code.md new file mode 100644 index 0000000..40b10cc --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/commands/explain-code.md @@ -0,0 +1 @@ +Explain-only walkthrough of code paths and stack behavior (no edits). Follow `@file:.cursor/skills/explain-code/SKILL.md`. Do not modify source files unless the user explicitly asks. diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/commands/verify-change.md b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/commands/verify-change.md new file mode 100644 index 0000000..a2e8bb5 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/commands/verify-change.md @@ -0,0 +1 @@ +Pre-PR verification checklist (analyze, tests, hooks) without full /build lifecycle. Follow `@file:.cursor/skills/verify-change/SKILL.md` for the change in scope. diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/cicd/cicd.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/cicd/cicd.mdc new file mode 100644 index 0000000..23939ef --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/cicd/cicd.mdc @@ -0,0 +1,29 @@ +--- +description: CI/CD, flavours, and quality gates for TaskFlow +globs: [".github/**", "codemagic.yaml", "Makefile", "pubspec.yaml", "fastlane/**"] +alwaysApply: false +--- + +# CI/CD & flavours — TaskFlow + +## Context +Pipeline and flavour setup must stay aligned with how the app is built and released. + +## Flavours +- Documented in `project-brief.yaml`: **dev, prod** +- Use `--flavor` / `-t lib/main_.dart` (or your project’s entrypoints) consistently across local, CI, and store builds +- Configuration via `--dart-define` / `--dart-define-from-file` — **never** hardcode secrets in source + +## Quality gates (recommended stages) +- **Lint:** `dart format --set-exit-if-changed .` and `flutter analyze` +- **Unit + widget:** `flutter test` (with coverage if the team tracks it) +- **Goldens:** fixed device/locale; CI fails on drift unless intentionally updated +- **Smoke build:** e.g. `flutter build apk` or `flutter build ios --no-codesign` for the primary flavour +- **E2E:** when `testing.depth` includes e2e — patrol on CI devices or Firebase Test Lab + +## Cursor integration +- After substantive edits: run analyze and relevant tests before PR +- For release-oriented tasks, use the **deploy** skill at `.cursor/skills/deploy/SKILL.md` when present + +## CI/CD tool +- Selected in brief: **GitHub Actions** (`github_actions`) diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/features/auth.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/features/auth.mdc new file mode 100644 index 0000000..090ec1c --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/features/auth.mdc @@ -0,0 +1,21 @@ +--- +description: "Feature module auth — contracts, boundaries, and ownership (fill after design)" +globs: ["lib/**/auth/**", "test/**/auth/**"] +alwaysApply: false +--- + +# Feature — Auth + +## Context +This stub was generated from `features.modules` in `project-brief.yaml`. Use it to capture **public contracts** (routes, DTOs, events) and **dependencies** for `auth` so agents do not invent cross-feature wiring. + +## Constraints +- List external dependencies (other features, packages, backend endpoints) explicitly +- Document invariants (auth required, idempotency, offline behavior) when known +- Update or delete this file when the module is removed or renamed — run `bash tool/cursor_audit.sh` to catch drift + +## Patterns +- Link to key entry points: primary screen(s), state holder(s), repository interface(s) + +## Anti-patterns +- Empty file left forever — either fill it or delete the module entry from the brief diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/features/profile.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/features/profile.mdc new file mode 100644 index 0000000..378cd74 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/features/profile.mdc @@ -0,0 +1,21 @@ +--- +description: "Feature module profile — contracts, boundaries, and ownership (fill after design)" +globs: ["lib/**/profile/**", "test/**/profile/**"] +alwaysApply: false +--- + +# Feature — Profile + +## Context +This stub was generated from `features.modules` in `project-brief.yaml`. Use it to capture **public contracts** (routes, DTOs, events) and **dependencies** for `profile` so agents do not invent cross-feature wiring. + +## Constraints +- List external dependencies (other features, packages, backend endpoints) explicitly +- Document invariants (auth required, idempotency, offline behavior) when known +- Update or delete this file when the module is removed or renamed — run `bash tool/cursor_audit.sh` to catch drift + +## Patterns +- Link to key entry points: primary screen(s), state holder(s), repository interface(s) + +## Anti-patterns +- Empty file left forever — either fill it or delete the module entry from the brief diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/features/tasks.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/features/tasks.mdc new file mode 100644 index 0000000..cfcc07f --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/features/tasks.mdc @@ -0,0 +1,21 @@ +--- +description: "Feature module tasks — contracts, boundaries, and ownership (fill after design)" +globs: ["lib/**/tasks/**", "test/**/tasks/**"] +alwaysApply: false +--- + +# Feature — Tasks + +## Context +This stub was generated from `features.modules` in `project-brief.yaml`. Use it to capture **public contracts** (routes, DTOs, events) and **dependencies** for `tasks` so agents do not invent cross-feature wiring. + +## Constraints +- List external dependencies (other features, packages, backend endpoints) explicitly +- Document invariants (auth required, idempotency, offline behavior) when known +- Update or delete this file when the module is removed or renamed — run `bash tool/cursor_audit.sh` to catch drift + +## Patterns +- Link to key entry points: primary screen(s), state holder(s), repository interface(s) + +## Anti-patterns +- Empty file left forever — either fill it or delete the module entry from the brief diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/i18n/localization.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/i18n/localization.mdc index 32c795b..f38b437 100644 --- a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/i18n/localization.mdc +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/i18n/localization.mdc @@ -1,6 +1,7 @@ --- description: "Localization / i18n conventions for TaskFlow" -alwaysApply: true +globs: ["lib/l10n/**", "lib/**/*.dart", "test/**/*.dart"] +alwaysApply: false --- # Localization Standards — TaskFlow diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/theming/theming.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/theming/theming.mdc new file mode 100644 index 0000000..3d5d22c --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/theming/theming.mdc @@ -0,0 +1,23 @@ +--- +description: Semantic colours and theme extensions for TaskFlow +globs: ["lib/**/*.dart", "test/**/*.dart"] +alwaysApply: false +--- + +# Theming — TaskFlow + +## Context +Theme variants from brief: **light, dark, high contrast**. +- **High contrast:** validate contrast, borders, and focus in the high-contrast theme alongside light/dark (WCAG). + + +## Constraints +- Prefer **`ThemeExtension`** (or design-system equivalents) for app-specific colours and radii — avoid raw `Color(0xFF...)` in feature code except in central token definitions +- Use **`Theme.of(context).colorScheme`** / `TextTheme` for Material-aligned roles where appropriate +- **High contrast:** when supported, verify WCAG contrast for text and controls; never rely on colour alone for state (pair with icon or label) +- **High contrast theme:** validate loading, empty, and error states; never rely on color alone for meaning (use icons/text/semantics). +- Touch targets: respect platform minimums (e.g. ~48 logical pixels) for interactive widgets + +## Anti-patterns +- Hard-coded `Colors.*` scattered across feature widgets instead of theme tokens +- Ignoring `MediaQuery.of(context).platformBrightness` / high-contrast modes when the product claims support diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/flutter-core.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/flutter-core.mdc index 46b3ba2..08e96ce 100644 --- a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/flutter-core.mdc +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/flutter-core.mdc @@ -1,6 +1,7 @@ --- -description: "Core Flutter conventions for TaskFlow — always applied" -alwaysApply: true +description: "Core Flutter conventions for TaskFlow" +globs: ["lib/**/*.dart", "test/**/*.dart", "integration_test/**/*.dart"] +alwaysApply: false --- # Flutter Core Standards — TaskFlow @@ -29,9 +30,10 @@ alwaysApply: true - Private members: `_camelCase` ## Imports -- Order: dart: → package: → relative -- Use relative imports within a feature; absolute for cross-feature -- Never import a feature's internal files from outside that feature +### Imports (default) +- Order: `dart:` → `package:` → relative +- Relative imports are allowed **within** the same feature directory; use `package:` imports for cross-feature code +- Never import another feature's internals from outside that feature ## Code quality - Max function length: 40 lines. Extract widgets and helpers aggressively diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/project-context.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/project-context.mdc index d818d32..71b4e4f 100644 --- a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/project-context.mdc +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/project-context.mdc @@ -1,6 +1,7 @@ --- -description: "Project context for TaskFlow — always applied" -alwaysApply: true +description: "Stack summary and product context for TaskFlow" +globs: ["project-brief.yaml", ".cursor/**/*.md", ".cursor/**/*.mdc", "pubspec.yaml"] +alwaysApply: false --- # Project Context — TaskFlow @@ -42,10 +43,12 @@ _No Git repository URLs listed._ Add entries under `references.repos` in project _No local paths listed._ Add monorepo packages or sibling folders under `references.local_paths` in project-brief.yaml when relevant. ## Product UX / themes & roles -- **Theme variants:** light, dark +- **Theme variants:** light, dark, high contrast - **Roles:** Not enabled (`app_context.roles_enabled: false`). +- **High contrast:** validate contrast, borders, and focus in the high-contrast theme alongside light/dark (WCAG). + ## Reviews — which rule owns what - **Theme, colors, typography, spacing/radius tokens** → `ui-ux-standards.mdc` (widgets read `Theme.of(context)` only) - **User-visible copy & locales** → `localization.mdc` (ARB / `AppLocalizations`; no UI string literals) diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/rule-authoring.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/rule-authoring.mdc new file mode 100644 index 0000000..7c23180 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/rule-authoring.mdc @@ -0,0 +1,24 @@ +--- +description: How Cursor rules in this repository must be written and maintained. +globs: [".cursor/rules/**/*.mdc"] +alwaysApply: true +--- + +# Rule authoring — TaskFlow + +## Context +Rules are version-controlled contracts for AI assistants. Poor rules waste context and silently steer every edit. + +## Constraints +- One focused concern per file; split broad topics instead of one mega-rule +- Every rule MUST have a clear `description` in frontmatter (one sentence) +- Prefer `alwaysApply: false` with **narrow** `globs` for domain rules — reserve `alwaysApply: true` for meta and safety +- `globs` must be as specific as possible — never `["**/*"]` unless tooling requires it +- Code samples in rules MUST be valid for this project (Dart/Flutter/YAML as appropriate) +- Deprecated guidance is removed, not left commented out +- Each substantive rule includes **Context** (why), **Constraints** (must/must not), and where helpful **Patterns** / **Anti-patterns** + +## Anti-patterns +- Domain rules (testing, l10n, a feature) with `alwaysApply: true` — burns context +- Rules with no concrete examples when the topic is code-facing +- Stale feature rules after modules are removed — run `tool/cursor_audit.sh` periodically diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/ui-ux-standards.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/ui-ux-standards.mdc index 0929436..0951f97 100644 --- a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/ui-ux-standards.mdc +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/ui-ux-standards.mdc @@ -1,6 +1,7 @@ --- -description: "UI/UX standards for TaskFlow — always applied" -alwaysApply: true +description: "UI/UX standards for TaskFlow" +globs: ["lib/**/*.dart", "test/**/*.dart", "integration_test/**/*.dart"] +alwaysApply: false --- # UI / UX Standards — TaskFlow @@ -46,3 +47,4 @@ alwaysApply: true - Minimum contrast ratio: 4.5:1 (WCAG AA) - Test with TalkBack / VoiceOver before each release +- **High contrast theme:** validate loading, empty, and error states; never rely on color alone for meaning (use icons/text/semantics). diff --git a/flutter-cursor-templates/generator/test/golden_briefs.dart b/flutter-cursor-templates/generator/test/golden_briefs.dart new file mode 100644 index 0000000..5953173 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden_briefs.dart @@ -0,0 +1,91 @@ +// Shared ProjectBrief fixtures for golden tests and tool/refresh_goldens.dart + +import '../src/models.dart'; + +final kBlocCleanFirebaseBrief = ProjectBrief( + projectName: 'TestApp', + packageId: 'com.test.testapp', + description: 'Test app for golden tests', + scale: 'medium', + stateManagement: 'bloc', + routing: 'gorouter', + architecture: 'clean', + backends: ['firebase'], + auth: 'firebase_auth', + platforms: ['ios', 'android'], + codegenTools: ['freezed'], + flavors: ['dev', 'prod'], + cicd: 'github_actions', + testingDepth: 'unit_widget', + e2eTool: 'patrol', + designSource: 'none', + figmaUrl: '', + apiDocsFormat: 'none', + apiDocsPath: '', + referenceRepos: [], + localPaths: [], + featureModules: ['auth', 'home', 'products'], + specialFeatures: [], + i18nEnabled: false, + locales: ['en'], + strictPackageImports: true, +); + +final kRiverpodFfSupabaseBrief = ProjectBrief( + projectName: 'TaskFlow', + packageId: 'com.test.taskflow', + description: 'Task management app', + scale: 'small', + stateManagement: 'riverpod', + routing: 'gorouter', + architecture: 'feature_first', + backends: ['supabase'], + auth: 'supabase_auth', + platforms: ['ios', 'android', 'web'], + codegenTools: ['freezed', 'json_serializable'], + flavors: ['dev', 'prod'], + cicd: 'github_actions', + testingDepth: 'unit_widget', + e2eTool: 'patrol', + designSource: 'none', + figmaUrl: '', + apiDocsFormat: 'none', + apiDocsPath: '', + referenceRepos: [], + localPaths: [], + featureModules: ['auth', 'tasks', 'profile'], + specialFeatures: [], + i18nEnabled: true, + locales: ['en', 'fr'], + themeVariants: const ['light', 'dark', 'high_contrast'], +); + +final kGetxMvcRestBrief = ProjectBrief( + projectName: 'LegacyApp', + packageId: 'com.test.legacy', + description: 'Legacy GetX app', + scale: 'medium', + stateManagement: 'getx', + routing: 'getx_nav', + architecture: 'mvc', + backends: ['rest'], + auth: 'jwt_rest', + platforms: ['ios', 'android'], + codegenTools: [], + flavors: ['dev', 'prod'], + cicd: 'codemagic', + testingDepth: 'unit_widget', + e2eTool: 'patrol', + designSource: 'none', + figmaUrl: '', + apiDocsFormat: 'openapi', + apiDocsPath: 'docs/api.yaml', + referenceRepos: [], + localPaths: [], + featureModules: ['auth', 'dashboard'], + specialFeatures: [], + i18nEnabled: false, + locales: ['en'], + mcpConfigEnabled: true, + mcpPreset: 'auto', +); diff --git a/flutter-cursor-templates/generator/tool/cursor_audit.sh b/flutter-cursor-templates/generator/tool/cursor_audit.sh new file mode 100755 index 0000000..c5e4257 --- /dev/null +++ b/flutter-cursor-templates/generator/tool/cursor_audit.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# cursor_gen — run from any Flutter repo: bash path/to/generator/tool/cursor_audit.sh [ROOT] +set -euo pipefail + +ROOT="$(cd "${1:-.}" && pwd)" +CURSOR="${ROOT}/.cursor" +RULES="${CURSOR}/rules" + +echo "== cursor_audit (${ROOT}) ==" + +if [[ ! -d "$CURSOR" ]]; then + echo "ERROR: missing ${CURSOR}" + exit 1 +fi + +if [[ -d "$RULES" ]]; then + ac="$(grep -R "alwaysApply: true" "$RULES" --include='*.mdc' 2>/dev/null | wc -l | tr -d ' ')" + echo "alwaysApply: true occurrences in .cursor/rules: ${ac}" + echo " (expect a small number — meta/safety; prefer scoped globs for domain rules)" +else + echo "WARN: no ${RULES}" +fi + +if [[ -d "$RULES/features" ]]; then + shopt -s nullglob + for f in "$RULES/features"/*.mdc; do + base="$(basename "$f" .mdc)" + if [[ ! -d "${ROOT}/lib/features/${base}" ]] && [[ ! -d "${ROOT}/lib/feature_${base}" ]]; then + echo "WARN: rules/features/${base}.mdc has no obvious lib/features/${base} folder — update features.modules or lib layout" + fi + done + shopt -u nullglob +fi + +echo "OK — review warnings above after brief or folder renames" diff --git a/flutter-cursor-templates/generator/tool/refresh_goldens.dart b/flutter-cursor-templates/generator/tool/refresh_goldens.dart new file mode 100644 index 0000000..caefe81 --- /dev/null +++ b/flutter-cursor-templates/generator/tool/refresh_goldens.dart @@ -0,0 +1,47 @@ +// Regenerates test/golden/* after intentional template changes. +// Run from generator/: dart run tool/refresh_goldens.dart + +import 'dart:io'; + +import 'package:path/path.dart' as p; + +import '../src/models.dart'; +import '../src/renderer.dart'; +import '../src/resolver.dart'; +import '../test/golden_briefs.dart'; + +Future main() async { + final templateSrc = _templateDir(); + if (!Directory(templateSrc).existsSync()) { + stderr.writeln('Template dir not found: $templateSrc'); + exitCode = 1; + return; + } + + final cases = { + 'bloc-clean-firebase': kBlocCleanFirebaseBrief, + 'riverpod-ff-supabase': kRiverpodFfSupabaseBrief, + 'getx-mvc-rest': kGetxMvcRestBrief, + }; + + for (final e in cases.entries) { + final profile = e.key; + final brief = e.value; + final rendered = await Renderer.render( + brief: brief, + templateFiles: Resolver.resolve(brief), + templateSrc: templateSrc, + ); + for (final out in rendered.entries) { + final file = File('test/golden/$profile/${out.key}'); + await file.parent.create(recursive: true); + await file.writeAsString(out.value); + stdout.writeln('Wrote ${file.path}'); + } + } +} + +String _templateDir() { + final scriptDir = File(Platform.script.toFilePath()).parent; + return p.normalize(p.join(scriptDir.path, '..', '..', 'templates')); +} diff --git a/flutter-cursor-templates/templates/commands/build.md.tmpl b/flutter-cursor-templates/templates/commands/build.md.tmpl new file mode 100644 index 0000000..4dc2925 --- /dev/null +++ b/flutter-cursor-templates/templates/commands/build.md.tmpl @@ -0,0 +1 @@ +End-to-end feature implementation (research, TDD, integration tests, verification). Follow the workflow and constraints in `@file:.cursor/skills/build/SKILL.md`. Use `project-brief.yaml` as the source of truth for stack and platforms. diff --git a/flutter-cursor-templates/templates/commands/debug-issue.md.tmpl b/flutter-cursor-templates/templates/commands/debug-issue.md.tmpl new file mode 100644 index 0000000..21a4fbb --- /dev/null +++ b/flutter-cursor-templates/templates/commands/debug-issue.md.tmpl @@ -0,0 +1 @@ +Structured bug triage and evidence-first debugging. Follow `@file:.cursor/skills/debug-issue/SKILL.md`. Gather reproduction steps, logs, and failing commands before proposing fixes. diff --git a/flutter-cursor-templates/templates/commands/explain-code.md.tmpl b/flutter-cursor-templates/templates/commands/explain-code.md.tmpl new file mode 100644 index 0000000..40b10cc --- /dev/null +++ b/flutter-cursor-templates/templates/commands/explain-code.md.tmpl @@ -0,0 +1 @@ +Explain-only walkthrough of code paths and stack behavior (no edits). Follow `@file:.cursor/skills/explain-code/SKILL.md`. Do not modify source files unless the user explicitly asks. diff --git a/flutter-cursor-templates/templates/commands/verify-change.md.tmpl b/flutter-cursor-templates/templates/commands/verify-change.md.tmpl new file mode 100644 index 0000000..a2e8bb5 --- /dev/null +++ b/flutter-cursor-templates/templates/commands/verify-change.md.tmpl @@ -0,0 +1 @@ +Pre-PR verification checklist (analyze, tests, hooks) without full /build lifecycle. Follow `@file:.cursor/skills/verify-change/SKILL.md` for the change in scope. diff --git a/flutter-cursor-templates/templates/onboarding/ONBOARDING.md.tmpl b/flutter-cursor-templates/templates/onboarding/ONBOARDING.md.tmpl new file mode 100644 index 0000000..04bb2ba --- /dev/null +++ b/flutter-cursor-templates/templates/onboarding/ONBOARDING.md.tmpl @@ -0,0 +1,20 @@ +# Cursor — {{PROJECT_NAME}} + +## Quick start + +1. Open this repo in **Cursor** so `.cursor/rules/` and `.cursor/skills/` load automatically. +2. Read **`rules/universal/rule-authoring.mdc`** — how we maintain AI rules. +3. Stack and product context live in **`project-brief.yaml`** at the repo root; regenerate `.cursor/` with `cursor_gen` after changing it. +4. **Slash skills** (primary workflows): open the Command Palette and use the project commands, or reference: + - `.cursor/skills/build/SKILL.md` — end-to-end feature implementation + - `.cursor/skills/debug-issue/SKILL.md` — structured debugging + - `.cursor/skills/verify-change/SKILL.md` — pre-PR verification + - `.cursor/skills/explain-code/SKILL.md` — explain-only walkthroughs +5. **Agents** (`.cursor/agents/*.mdc`) are reusable reviewer personas — attach when asking for code review or focused passes. +6. **Custom overrides**: files under `.cursor/custom/` and `CURSOR:CUSTOM` blocks in generated files are preserved on `cursor_gen --refresh` (see generator docs). + +## Optional + +- **`integrations.mcp.enabled`** in `project-brief.yaml` — generates `.cursor/mcp.json` with env-based server entries (no secrets in git). +- **`telemetry_opt_in: true`** — emits local telemetry helpers under `.cursor/telemetry/` (never commit usage logs if you enable logging). +- Run **`bash tool/cursor_audit.sh`** from the project root periodically to catch stale feature rules and overly broad `alwaysApply` usage. diff --git a/flutter-cursor-templates/templates/root/.cursorignore.tmpl b/flutter-cursor-templates/templates/root/.cursorignore.tmpl new file mode 100644 index 0000000..c4e056e --- /dev/null +++ b/flutter-cursor-templates/templates/root/.cursorignore.tmpl @@ -0,0 +1,27 @@ +# Build artefacts +build/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +*.g.dart +*.freezed.dart +*.gr.dart +*.config.dart + +# Secrets +.env +.env.* +firebase_options.dart +google-services.json +GoogleService-Info.plist + +# Large binary assets +assets/fonts/ +assets/videos/ +*.aab +*.apk +*.ipa + +# IDE +.idea/ +*.iml diff --git a/flutter-cursor-templates/templates/root/AGENTS.md.tmpl b/flutter-cursor-templates/templates/root/AGENTS.md.tmpl index f903927..3384b6f 100644 --- a/flutter-cursor-templates/templates/root/AGENTS.md.tmpl +++ b/flutter-cursor-templates/templates/root/AGENTS.md.tmpl @@ -3,4 +3,5 @@ Repo-level notes for AI assistants. Authoritative stack and conventions are in `project-brief.yaml` and `.cursor/` (regenerate with `cursor_gen` from the project root). - **Package:** `{{PACKAGE_ID}}` -- **Slash skills** (see `.cursor/skills/`): `/build`, `/debug`, `/verify`, `/explain` +- **Onboarding:** `.cursor/ONBOARDING.md` — layout, slash commands → skills, MCP opt-in, audits +- **Slash skills** (see `.cursor/skills/`): `/build`, `/debug`, `/verify`, `/explain` (see `.cursor/commands/*.md`) diff --git a/flutter-cursor-templates/templates/root/lefthook.yaml.tmpl b/flutter-cursor-templates/templates/root/lefthook.yaml.tmpl index 2709629..84da03d 100644 --- a/flutter-cursor-templates/templates/root/lefthook.yaml.tmpl +++ b/flutter-cursor-templates/templates/root/lefthook.yaml.tmpl @@ -1,4 +1,5 @@ # {{PROJECT_NAME}} — generated by cursor_gen; adjust commands to your repo +# Optional: run `bash tool/cursor_audit.sh` after changing features.modules or rule files. pre-commit: commands: flutter-analyze: diff --git a/flutter-cursor-templates/templates/root/tool/cursor_audit.sh.tmpl b/flutter-cursor-templates/templates/root/tool/cursor_audit.sh.tmpl new file mode 100644 index 0000000..0095e12 --- /dev/null +++ b/flutter-cursor-templates/templates/root/tool/cursor_audit.sh.tmpl @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# {{PROJECT_NAME}} — Cursor rule hygiene (generated by cursor_gen) +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CURSOR="${ROOT}/.cursor" +RULES="${CURSOR}/rules" + +echo "== cursor_audit (${ROOT}) ==" + +if [[ ! -d "$CURSOR" ]]; then + echo "ERROR: missing ${CURSOR}" + exit 1 +fi + +if [[ -d "$RULES" ]]; then + ac="$(grep -R "alwaysApply: true" "$RULES" --include='*.mdc' 2>/dev/null | wc -l | tr -d ' ')" + echo "alwaysApply: true occurrences in .cursor/rules: ${ac}" + echo " (expect a small number — meta/safety; prefer scoped globs for domain rules)" +else + echo "WARN: no ${RULES}" +fi + +if [[ -d "$RULES/features" ]]; then + shopt -s nullglob + for f in "$RULES/features"/*.mdc; do + base="$(basename "$f" .mdc)" + if [[ ! -d "${ROOT}/lib/features/${base}" ]] && [[ ! -d "${ROOT}/lib/feature_${base}" ]]; then + echo "WARN: rules/features/${base}.mdc has no obvious lib/features/${base} folder — update features.modules or lib layout" + fi + done + shopt -u nullglob +fi + +echo "OK — review warnings above after brief or folder renames" diff --git a/flutter-cursor-templates/templates/rules/cicd/cicd.mdc.tmpl b/flutter-cursor-templates/templates/rules/cicd/cicd.mdc.tmpl new file mode 100644 index 0000000..fc7fd7a --- /dev/null +++ b/flutter-cursor-templates/templates/rules/cicd/cicd.mdc.tmpl @@ -0,0 +1,29 @@ +--- +description: CI/CD, flavours, and quality gates for {{PROJECT_NAME}} +globs: [".github/**", "codemagic.yaml", "Makefile", "pubspec.yaml", "fastlane/**"] +alwaysApply: false +--- + +# CI/CD & flavours — {{PROJECT_NAME}} + +## Context +Pipeline and flavour setup must stay aligned with how the app is built and released. + +## Flavours +- Documented in `project-brief.yaml`: **{{FLAVORS_LIST}}** +- Use `--flavor` / `-t lib/main_.dart` (or your project’s entrypoints) consistently across local, CI, and store builds +- Configuration via `--dart-define` / `--dart-define-from-file` — **never** hardcode secrets in source + +## Quality gates (recommended stages) +- **Lint:** `dart format --set-exit-if-changed .` and `flutter analyze` +- **Unit + widget:** `flutter test` (with coverage if the team tracks it) +- **Goldens:** fixed device/locale; CI fails on drift unless intentionally updated +- **Smoke build:** e.g. `flutter build apk` or `flutter build ios --no-codesign` for the primary flavour +- **E2E:** when `testing.depth` includes e2e — {{E2E_TOOL}} on CI devices or Firebase Test Lab + +## Cursor integration +- After substantive edits: run analyze and relevant tests before PR +- For release-oriented tasks, use the **deploy** skill at `.cursor/skills/deploy/SKILL.md` when present + +## CI/CD tool +- Selected in brief: **{{CICD_TOOL}}** (`{{CICD_RAW}}`) diff --git a/flutter-cursor-templates/templates/rules/features/_stub.mdc.tmpl b/flutter-cursor-templates/templates/rules/features/_stub.mdc.tmpl new file mode 100644 index 0000000..a6961f1 --- /dev/null +++ b/flutter-cursor-templates/templates/rules/features/_stub.mdc.tmpl @@ -0,0 +1,21 @@ +--- +description: "Feature module {{FEATURE_MODULE}} — contracts, boundaries, and ownership (fill after design)" +globs: ["lib/**/{{FEATURE_MODULE}}/**", "test/**/{{FEATURE_MODULE}}/**"] +alwaysApply: false +--- + +# Feature — {{FEATURE_MODULE_TITLE}} + +## Context +This stub was generated from `features.modules` in `project-brief.yaml`. Use it to capture **public contracts** (routes, DTOs, events) and **dependencies** for `{{FEATURE_MODULE}}` so agents do not invent cross-feature wiring. + +## Constraints +- List external dependencies (other features, packages, backend endpoints) explicitly +- Document invariants (auth required, idempotency, offline behavior) when known +- Update or delete this file when the module is removed or renamed — run `bash tool/cursor_audit.sh` to catch drift + +## Patterns +- Link to key entry points: primary screen(s), state holder(s), repository interface(s) + +## Anti-patterns +- Empty file left forever — either fill it or delete the module entry from the brief diff --git a/flutter-cursor-templates/templates/rules/i18n/localization.mdc.tmpl b/flutter-cursor-templates/templates/rules/i18n/localization.mdc.tmpl index dff5105..d646f1a 100644 --- a/flutter-cursor-templates/templates/rules/i18n/localization.mdc.tmpl +++ b/flutter-cursor-templates/templates/rules/i18n/localization.mdc.tmpl @@ -1,6 +1,7 @@ --- description: "Localization / i18n conventions for {{PROJECT_NAME}}" -alwaysApply: true +globs: ["lib/l10n/**", "lib/**/*.dart", "test/**/*.dart"] +alwaysApply: false --- # Localization Standards — {{PROJECT_NAME}} diff --git a/flutter-cursor-templates/templates/rules/integrations/push-deeplink.mdc.tmpl b/flutter-cursor-templates/templates/rules/integrations/push-deeplink.mdc.tmpl new file mode 100644 index 0000000..aa8f82a --- /dev/null +++ b/flutter-cursor-templates/templates/rules/integrations/push-deeplink.mdc.tmpl @@ -0,0 +1,21 @@ +--- +description: Push notifications and deep linking for {{PROJECT_NAME}} +globs: ["lib/**/*.dart", "android/**", "ios/**", "test/**/*.dart"] +alwaysApply: false +--- + +# Push & deep linking — {{PROJECT_NAME}} + +## Context +Special capabilities from `project-brief.yaml`: **{{SPECIAL_FEATURES}}**. Native and server configuration must stay consistent. + +## Constraints +- **Push:** request permissions through the platform flow; handle denial gracefully; no silent failures on token registration +- **Routing:** deep links and notification taps MUST go through **{{ROUTING}}** (no ad-hoc `Navigator` stacks for inbound links) +- **Payloads:** map FCM/APNs/Supabase payloads to domain models in the data layer — no JSON parsing scattered in widgets +- **iOS:** exercise push on a **physical device** when possible (simulator limitations) +- **Android:** declare required permissions explicitly; target API behaviour for POST_NOTIFICATIONS where applicable + +## Testing +- Unit-test payload → domain mapping and routing targets +- Integration/E2E: cold start, background, and foreground tap-to-open flows when `testing.depth` allows diff --git a/flutter-cursor-templates/templates/rules/telemetry/usage-logging.mdc.tmpl b/flutter-cursor-templates/templates/rules/telemetry/usage-logging.mdc.tmpl new file mode 100644 index 0000000..547ffd0 --- /dev/null +++ b/flutter-cursor-templates/templates/rules/telemetry/usage-logging.mdc.tmpl @@ -0,0 +1,19 @@ +--- +description: "Policy for optional local AI usage logs under .cursor/telemetry/" +globs: [".cursor/telemetry/**"] +alwaysApply: false +--- + +# Telemetry — {{PROJECT_NAME}} + +## Context +When `telemetry_opt_in: true`, this repo may record **local-only** generation or usage notes under `.cursor/telemetry/`. This is **not** production analytics. + +## Constraints +- **Never** commit secrets, tokens, or PII into JSONL or shell history +- Prefer redacted summaries over raw prompts +- Add `.cursor/telemetry/*.jsonl` to `.gitignore` unless your team explicitly version-controls sanitized samples + +## Patterns +- Append one JSON object per line (JSONL) with ISO timestamps and event type +- Rotate or truncate files if they grow beyond a few MB diff --git a/flutter-cursor-templates/templates/rules/theming/theming.mdc.tmpl b/flutter-cursor-templates/templates/rules/theming/theming.mdc.tmpl new file mode 100644 index 0000000..971983c --- /dev/null +++ b/flutter-cursor-templates/templates/rules/theming/theming.mdc.tmpl @@ -0,0 +1,20 @@ +--- +description: Semantic colours and theme extensions for {{PROJECT_NAME}} +globs: ["lib/**/*.dart", "test/**/*.dart"] +alwaysApply: false +--- + +# Theming — {{PROJECT_NAME}} + +## Context +Theme variants from brief: **{{THEME_SUMMARY}}**.{{HIGH_CONTRAST_NOTE}} + +## Constraints +- Prefer **`ThemeExtension`** (or design-system equivalents) for app-specific colours and radii — avoid raw `Color(0xFF...)` in feature code except in central token definitions +- Use **`Theme.of(context).colorScheme`** / `TextTheme` for Material-aligned roles where appropriate +- **High contrast:** when supported, verify WCAG contrast for text and controls; never rely on colour alone for state (pair with icon or label){{HIGH_CONTRAST_UX_LINE}} +- Touch targets: respect platform minimums (e.g. ~48 logical pixels) for interactive widgets + +## Anti-patterns +- Hard-coded `Colors.*` scattered across feature widgets instead of theme tokens +- Ignoring `MediaQuery.of(context).platformBrightness` / high-contrast modes when the product claims support diff --git a/flutter-cursor-templates/templates/rules/universal/flutter-core.mdc.tmpl b/flutter-cursor-templates/templates/rules/universal/flutter-core.mdc.tmpl index bcafe6a..647e438 100644 --- a/flutter-cursor-templates/templates/rules/universal/flutter-core.mdc.tmpl +++ b/flutter-cursor-templates/templates/rules/universal/flutter-core.mdc.tmpl @@ -1,6 +1,7 @@ --- -description: "Core Flutter conventions for {{PROJECT_NAME}} — always applied" -alwaysApply: true +description: "Core Flutter conventions for {{PROJECT_NAME}}" +globs: ["lib/**/*.dart", "test/**/*.dart", "integration_test/**/*.dart"] +alwaysApply: false --- # Flutter Core Standards — {{PROJECT_NAME}} @@ -29,10 +30,7 @@ alwaysApply: true - Private members: `_camelCase` ## Imports -- Order: dart: → package: → relative -- Use relative imports within a feature; absolute for cross-feature -- Never import a feature's internal files from outside that feature - +{{IMPORT_POLICY_BLOCK}} ## Code quality - Max function length: 40 lines. Extract widgets and helpers aggressively - No `print()` in production code — use a logging package diff --git a/flutter-cursor-templates/templates/rules/universal/project-context.mdc.tmpl b/flutter-cursor-templates/templates/rules/universal/project-context.mdc.tmpl index 7d2f89c..5901ad4 100644 --- a/flutter-cursor-templates/templates/rules/universal/project-context.mdc.tmpl +++ b/flutter-cursor-templates/templates/rules/universal/project-context.mdc.tmpl @@ -1,6 +1,7 @@ --- -description: "Project context for {{PROJECT_NAME}} — always applied" -alwaysApply: true +description: "Stack summary and product context for {{PROJECT_NAME}}" +globs: ["project-brief.yaml", ".cursor/**/*.md", ".cursor/**/*.mdc", "pubspec.yaml"] +alwaysApply: false --- # Project Context — {{PROJECT_NAME}} diff --git a/flutter-cursor-templates/templates/rules/universal/rule-authoring.mdc.tmpl b/flutter-cursor-templates/templates/rules/universal/rule-authoring.mdc.tmpl new file mode 100644 index 0000000..1b1e520 --- /dev/null +++ b/flutter-cursor-templates/templates/rules/universal/rule-authoring.mdc.tmpl @@ -0,0 +1,24 @@ +--- +description: How Cursor rules in this repository must be written and maintained. +globs: [".cursor/rules/**/*.mdc"] +alwaysApply: true +--- + +# Rule authoring — {{PROJECT_NAME}} + +## Context +Rules are version-controlled contracts for AI assistants. Poor rules waste context and silently steer every edit. + +## Constraints +- One focused concern per file; split broad topics instead of one mega-rule +- Every rule MUST have a clear `description` in frontmatter (one sentence) +- Prefer `alwaysApply: false` with **narrow** `globs` for domain rules — reserve `alwaysApply: true` for meta and safety +- `globs` must be as specific as possible — never `["**/*"]` unless tooling requires it +- Code samples in rules MUST be valid for this project (Dart/Flutter/YAML as appropriate) +- Deprecated guidance is removed, not left commented out +- Each substantive rule includes **Context** (why), **Constraints** (must/must not), and where helpful **Patterns** / **Anti-patterns** + +## Anti-patterns +- Domain rules (testing, l10n, a feature) with `alwaysApply: true` — burns context +- Rules with no concrete examples when the topic is code-facing +- Stale feature rules after modules are removed — run `tool/cursor_audit.sh` periodically diff --git a/flutter-cursor-templates/templates/rules/universal/ui-ux-standards.mdc.tmpl b/flutter-cursor-templates/templates/rules/universal/ui-ux-standards.mdc.tmpl index bfd07bb..2cde1db 100644 --- a/flutter-cursor-templates/templates/rules/universal/ui-ux-standards.mdc.tmpl +++ b/flutter-cursor-templates/templates/rules/universal/ui-ux-standards.mdc.tmpl @@ -1,6 +1,7 @@ --- -description: "UI/UX standards for {{PROJECT_NAME}} — always applied" -alwaysApply: true +description: "UI/UX standards for {{PROJECT_NAME}}" +globs: ["lib/**/*.dart", "test/**/*.dart", "integration_test/**/*.dart"] +alwaysApply: false --- # UI / UX Standards — {{PROJECT_NAME}} diff --git a/flutter-cursor-templates/templates/telemetry/gitignore.tmpl b/flutter-cursor-templates/templates/telemetry/gitignore.tmpl new file mode 100644 index 0000000..6e3f01a --- /dev/null +++ b/flutter-cursor-templates/templates/telemetry/gitignore.tmpl @@ -0,0 +1,2 @@ +*.jsonl +*.log diff --git a/flutter-cursor-templates/templates/telemetry/log.sh.tmpl b/flutter-cursor-templates/templates/telemetry/log.sh.tmpl new file mode 100644 index 0000000..1910fc2 --- /dev/null +++ b/flutter-cursor-templates/templates/telemetry/log.sh.tmpl @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Append a JSONL usage line (local only). Requires jq when passing structured payload. +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +LOG="${ROOT}/telemetry/usage.jsonl" +mkdir -p "$(dirname "$LOG")" +echo "{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"event\":\"${1:-note}\",\"detail\":\"${2:-}\"}" >>"$LOG" diff --git a/flutter-cursor-templates/tool/cursor_audit.sh b/flutter-cursor-templates/tool/cursor_audit.sh new file mode 100644 index 0000000..57dea0b --- /dev/null +++ b/flutter-cursor-templates/tool/cursor_audit.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Cursor rule hygiene — run from the Flutter app repository root. +# Usage: bash tool/cursor_audit.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +RULES_DIR="$REPO_ROOT/.cursor/rules" + +if [[ ! -d "$RULES_DIR" ]]; then + echo "No .cursor/rules at $RULES_DIR — run cursor_gen first." + exit 0 +fi + +echo "=== Cursor rule audit ===" +echo "Rules without fenced code blocks (\`\`\`):" +missing=0 +while IFS= read -r -d '' f; do + if ! grep -q '```' "$f" 2>/dev/null; then + echo " $f" + missing=1 + fi +done < <(find "$RULES_DIR" -name '*.mdc' -print0) + +if [[ "$missing" -eq 0 ]]; then + echo " (none — every .mdc contains at least one \`\`\` fence)" +fi + +echo "" +echo "Rules with alwaysApply: true (should be few):" +grep -rl 'alwaysApply: true' "$RULES_DIR" 2>/dev/null | while read -r p; do echo " $p"; done || true + +echo "" +feat_dir="$RULES_DIR/features" +if [[ -d "$feat_dir" ]]; then + echo "Stale feature rules (no lib/features/):" + stale=0 + for f in "$feat_dir"/*.mdc; do + [[ -e "$f" ]] || continue + name="$(basename "$f" .mdc)" + if [[ ! -d "$REPO_ROOT/lib/features/$name" ]]; then + echo " STALE: $f" + stale=1 + fi + done + if [[ "$stale" -eq 0 ]]; then + echo " (none)" + fi +else + echo "No rules/features/ directory." +fi + +echo "=== Done ==="