diff --git a/.cursor/rules/cursor_rules.mdc b/.cursor/rules/cursor_rules.mdc new file mode 100644 index 00000000..7dfae3de --- /dev/null +++ b/.cursor/rules/cursor_rules.mdc @@ -0,0 +1,53 @@ +--- +description: Guidelines for creating and maintaining Cursor rules to ensure consistency and effectiveness. +globs: .cursor/rules/*.mdc +alwaysApply: true +--- + +- **Required Rule Structure:** + ```markdown + --- + description: Clear, one-line description of what the rule enforces + globs: path/to/files/*.ext, other/path/**/* + alwaysApply: boolean + --- + + - **Main Points in Bold** + - Sub-points with details + - Examples and explanations + ``` + +- **File References:** + - Use `[filename](mdc:path/to/file)` ([filename](mdc:filename)) to reference files + - Example: [prisma.mdc](mdc:.cursor/rules/prisma.mdc) for rule references + - Example: [schema.prisma](mdc:prisma/schema.prisma) for code references + +- **Code Examples:** + - Use language-specific code blocks + ```typescript + // ✅ DO: Show good examples + const goodExample = true; + + // ❌ DON'T: Show anti-patterns + const badExample = false; + ``` + +- **Rule Content Guidelines:** + - Start with high-level overview + - Include specific, actionable requirements + - Show examples of correct implementation + - Reference existing code when possible + - Keep rules DRY by referencing other rules + +- **Rule Maintenance:** + - Update rules when new patterns emerge + - Add examples from actual codebase + - Remove outdated patterns + - Cross-reference related rules + +- **Best Practices:** + - Use bullet points for clarity + - Keep descriptions concise + - Include both DO and DON'T examples + - Reference actual code over theoretical examples + - Use consistent formatting across rules \ No newline at end of file diff --git a/.cursor/rules/general-rules.mdc b/.cursor/rules/general-rules.mdc index 7335792d..0684b649 100644 --- a/.cursor/rules/general-rules.mdc +++ b/.cursor/rules/general-rules.mdc @@ -11,13 +11,11 @@ alwaysApply: true - Read [project-design.mdc](mdc:.cursor/rules/project-design.mdc) to understand the codebase - Read [project-conventions.mdc](mdc:.cursor/rules/project-conventions.mdc) to understand _how_ to write code for the codebase - Read [ui-ux-design-guidelines.mdc](mdc:.cursor/rules/ui-ux-design-guidelines.mdc) to understand how to implement frontend code specifically +- Ignore i18n methods and files. Hardcode strings in English for now to optimize speed of development. ## Prohibited actions -Do not under any circumstance do the following: - - Do not run `rails server` in your responses. - Do not run `touch tmp/restart.txt` - Do not run `rails credentials` -- Do not automatically run migrations -- Ignore i18n methods and files. Hardcode strings in English for now to optimize speed of development. \ No newline at end of file +- Do not automatically run migrations \ No newline at end of file diff --git a/.cursor/rules/project-conventions.mdc b/.cursor/rules/project-conventions.mdc index 2977dc33..2ac70891 100644 --- a/.cursor/rules/project-conventions.mdc +++ b/.cursor/rules/project-conventions.mdc @@ -53,6 +53,7 @@ This codebase adopts a "skinny controller, fat models" convention. Furthermore, - Use Turbo streams to enhance functionality, but do not solely depend on it - Format currencies, numbers, dates, and other values server-side, then pass to Stimulus controllers for display only - Keep client-side code for where it truly shines. For example, @bulk_select_controller.js is a case where server-side solutions would degrade the user experience significantly. When bulk-selecting entries, client-side solutions are the way to go and Stimulus provides the right toolset to achieve this. +- Always use the `icon` helper in [application_helper.rb](mdc:app/helpers/application_helper.rb) for icons. NEVER use `lucide_icon` helper directly. The Hotwire suite (Turbo/Stimulus) works very well with these native elements and we optimize for this. @@ -65,56 +66,8 @@ All code should maximize readability and simplicity. - Example 1: be mindful of loading large data payloads in global layouts - Example 2: Avoid N+1 queries -### Convention 5: Use Minitest + Fixtures for testing, minimize fixtures - -Due to the open-source nature of this project, we have chosen Minitest + Fixtures for testing to maximize familiarity and predictability. - -- Always use Minitest and fixtures for testing. -- Keep fixtures to a minimum. Most models should have 2-3 fixtures maximum that represent the "base cases" for that model. "Edge cases" should be created on the fly, within the context of the test which it is needed. -- For tests that require a large number of fixture records to be created, use Rails helpers such as [entries_test_helper.rb](mdc:test/support/account/entries_test_helper.rb) to act as a "factory" for creating these. For a great example of this, check out [forward_calculator_test.rb](mdc:test/models/account/balance/forward_calculator_test.rb) -- Take a minimal approach to testing—only test the absolutely critical code paths that will significantly increase developer confidence - -#### Convention 5a: Write minimal, effective tests - -- Use system tests sparingly as they increase the time to complete the test suite -- Only write tests for critical and important code paths -- Write tests as you go, when required -- Take a practical approach to testing. Tests are effective when their presence _significantly increases confidence in the codebase_. - -Below are examples of necessary vs. unnecessary tests: - -```rb -# GOOD!! -# Necessary test - in this case, we're testing critical domain business logic -test "syncs balances" do - Account::Holding::Syncer.any_instance.expects(:sync_holdings).returns([]).once - - @account.expects(:start_date).returns(2.days.ago.to_date) - - Account::Balance::ForwardCalculator.any_instance.expects(:calculate).returns( - [ - Account::Balance.new(date: 1.day.ago.to_date, balance: 1000, cash_balance: 1000, currency: "USD"), - Account::Balance.new(date: Date.current, balance: 1000, cash_balance: 1000, currency: "USD") - ] - ) - - assert_difference "@account.balances.count", 2 do - Account::Balance::Syncer.new(@account, strategy: :forward).sync_balances - end -end - -# BAD!! -# Unnecessary test - in this case, this is simply testing ActiveRecord's functionality -test "saves balance" do - balance_record = Account::Balance.new(balance: 100, currency: "USD") - - assert balance_record.save -end -``` - -### Convention 6: Use ActiveRecord for complex validations, DB for simple ones, keep business logic out of DB +### Convention 5: Use ActiveRecord for complex validations, DB for simple ones, keep business logic out of DB - Enforce `null` checks, unique indexes, and other simple validations in the DB - ActiveRecord validations _may_ mirror the DB level ones, but not 100% necessary. These are for convenience when error handling in forms. Always prefer client-side form validation when possible. - Complex validations and business logic should remain in ActiveRecord - diff --git a/.cursor/rules/project-design.mdc b/.cursor/rules/project-design.mdc index 41fa2210..4b60f2f9 100644 --- a/.cursor/rules/project-design.mdc +++ b/.cursor/rules/project-design.mdc @@ -55,29 +55,29 @@ All balances are calculated daily by [balance_calculator.rb](mdc:app/models/acco ### Account Holdings -An account [holding.rb](mdc:app/models/account/holding.rb) applies to [investment.rb](mdc:app/models/investment.rb) type accounts and represents a `qty` of a certain [security.rb](mdc:app/models/security.rb) at a specific `price` on a specific `date`. +An account [holding.rb](mdc:app/models/holding.rb) applies to [investment.rb](mdc:app/models/investment.rb) type accounts and represents a `qty` of a certain [security.rb](mdc:app/models/security.rb) at a specific `price` on a specific `date`. -For investment accounts with holdings, [holding_calculator.rb](mdc:app/models/account/holding_calculator.rb) is used to calculate the daily historical holding quantities and prices, which are then rolled up into a final "Balance" for the account in [balance_calculator.rb](mdc:app/models/account/balance_calculator.rb). +For investment accounts with holdings, [base_calculator.rb](mdc:app/models/holding/base_calculator.rb) is used to calculate the daily historical holding quantities and prices, which are then rolled up into a final "Balance" for the account in [base_calculator.rb](mdc:app/models/account/balance/base_calculator.rb). ### Account Entries -An account [entry.rb](mdc:app/models/account/entry.rb) is also a Rails "delegated type". `Account::Entry` represents any record that _modifies_ an `Account` [balance.rb](mdc:app/models/account/balance.rb) and/or [holding.rb](mdc:app/models/account/holding.rb). Therefore, every entry must have a `date`, `amount`, and `currency`. +An account [entry.rb](mdc:app/models/entry.rb) is also a Rails "delegated type". `Entry` represents any record that _modifies_ an `Account` [balance.rb](mdc:app/models/account/balance.rb) and/or [holding.rb](mdc:app/models/holding.rb). Therefore, every entry must have a `date`, `amount`, and `currency`. -The `amount` of an [entry.rb](mdc:app/models/account/entry.rb) is a signed value. A _negative_ amount is an "inflow" of money to that account. A _positive_ value is an "outflow" of money from that account. For example: +The `amount` of an [entry.rb](mdc:app/models/entry.rb) is a signed value. A _negative_ amount is an "inflow" of money to that account. A _positive_ value is an "outflow" of money from that account. For example: - A negative amount for a credit card account represents a "payment" to that account, which _reduces_ its balance (since it is a `liability`) - A negative amount for a checking account represents an "income" to that account, which _increases_ its balance (since it is an `asset`) - A negative amount for an investment/brokerage trade represents a "sell" transaction, which _increases_ the cash balance of the account -There are 3 entry types, defined as [entryable.rb](mdc:app/models/account/entryable.rb) records: +There are 3 entry types, defined as [entryable.rb](mdc:app/models/entryable.rb) records: -- `Account::Valuation` - an account [valuation.rb](mdc:app/models/account/valuation.rb) is an entry that says, "here is the value of this account on this date". It is an absolute measure of an account value / debt. If there is an `Account::Valuation` of 5,000 for today's date, that means that the account balance will be 5,000 today. -- `Account::Transaction` - an account [transaction.rb](mdc:app/models/account/transaction.rb) is an entry that alters the account balance by the `amount`. This is the most common type of entry and can be thought of as an "income" or "expense". -- `Account::Trade` - an account [trade.rb](mdc:app/models/account/trade.rb) is an entry that only applies to an investment account. This represents a "buy" or "sell" of a holding and has a `qty` and `price`. +- `Valuation` - an account [valuation.rb](mdc:app/models/valuation.rb) is an entry that says, "here is the value of this account on this date". It is an absolute measure of an account value / debt. If there is an `Valuation` of 5,000 for today's date, that means that the account balance will be 5,000 today. +- `Transaction` - an account [transaction.rb](mdc:app/models/transaction.rb) is an entry that alters the account balance by the `amount`. This is the most common type of entry and can be thought of as an "income" or "expense". +- `Trade` - an account [trade.rb](mdc:app/models/trade.rb) is an entry that only applies to an investment account. This represents a "buy" or "sell" of a holding and has a `qty` and `price`. ### Account Transfers -A [transfer.rb](mdc:app/models/transfer.rb) represents a movement of money between two accounts. A transfer has an inflow [transaction.rb](mdc:app/models/account/transaction.rb) and an outflow [transaction.rb](mdc:app/models/account/transaction.rb). The Maybe system auto-matches transfers based on the following criteria: +A [transfer.rb](mdc:app/models/transfer.rb) represents a movement of money between two accounts. A transfer has an inflow [transaction.rb](mdc:app/models/transaction.rb) and an outflow [transaction.rb](mdc:app/models/transaction.rb). The Maybe system auto-matches transfers based on the following criteria: - Must be from different accounts - Must be within 4 days of each other @@ -115,10 +115,10 @@ The most important type of sync is the account sync. It is orchestrated by the - Auto-matches transfer records for the account - Calculates daily [balance.rb](mdc:app/models/account/balance.rb) records for the account from `account.start_date` to `Date.current` using [base_calculator.rb](mdc:app/models/account/balance/base_calculator.rb) - - Balances are dependent on the calculation of [holding.rb](mdc:app/models/account/holding.rb), which uses [base_calculator.rb](mdc:app/models/account/holding/base_calculator.rb) + - Balances are dependent on the calculation of [holding.rb](mdc:app/models/holding.rb), which uses [base_calculator.rb](mdc:app/models/account/holding/base_calculator.rb) - Enriches transaction data if enabled by user -An account sync happens every time an [entry.rb](mdc:app/models/account/entry.rb) is updated. +An account sync happens every time an [entry.rb](mdc:app/models/entry.rb) is updated. ### Plaid Item Syncs @@ -126,7 +126,7 @@ A Plaid Item sync is an ETL (extract, transform, load) operation: 1. [plaid_item.rb](mdc:app/models/plaid_item.rb) fetches data from the external Plaid API 2. [plaid_item.rb](mdc:app/models/plaid_item.rb) creates and loads this data to [plaid_account.rb](mdc:app/models/plaid_account.rb) records -3. [plaid_item.rb](mdc:app/models/plaid_item.rb) and [plaid_account.rb](mdc:app/models/plaid_account.rb) transform and load data to [account.rb](mdc:app/models/account.rb) and [entry.rb](mdc:app/models/account/entry.rb), the internal Maybe representations of the data. +3. [plaid_item.rb](mdc:app/models/plaid_item.rb) and [plaid_account.rb](mdc:app/models/plaid_account.rb) transform and load data to [account.rb](mdc:app/models/account.rb) and [entry.rb](mdc:app/models/entry.rb), the internal Maybe representations of the data. ### Family Syncs @@ -247,10 +247,3 @@ class ConcreteProvider < Provider end end ``` - - - - - - - diff --git a/.cursor/rules/self_improve.mdc b/.cursor/rules/self_improve.mdc new file mode 100644 index 00000000..40b31b6e --- /dev/null +++ b/.cursor/rules/self_improve.mdc @@ -0,0 +1,72 @@ +--- +description: Guidelines for continuously improving Cursor rules based on emerging code patterns and best practices. +globs: **/* +alwaysApply: true +--- + +- **Rule Improvement Triggers:** + - New code patterns not covered by existing rules + - Repeated similar implementations across files + - Common error patterns that could be prevented + - New libraries or tools being used consistently + - Emerging best practices in the codebase + +- **Analysis Process:** + - Compare new code with existing rules + - Identify patterns that should be standardized + - Look for references to external documentation + - Check for consistent error handling patterns + - Monitor test patterns and coverage + +- **Rule Updates:** + - **Add New Rules When:** + - A new technology/pattern is used in 3+ files + - Common bugs could be prevented by a rule + - Code reviews repeatedly mention the same feedback + - New security or performance patterns emerge + + - **Modify Existing Rules When:** + - Better examples exist in the codebase + - Additional edge cases are discovered + - Related rules have been updated + - Implementation details have changed + +- **Example Pattern Recognition:** + ```typescript + // If you see repeated patterns like: + const data = await prisma.user.findMany({ + select: { id: true, email: true }, + where: { status: 'ACTIVE' } + }); + + // Consider adding to [prisma.mdc](mdc:.cursor/rules/prisma.mdc): + // - Standard select fields + // - Common where conditions + // - Performance optimization patterns + ``` + +- **Rule Quality Checks:** + - Rules should be actionable and specific + - Examples should come from actual code + - References should be up to date + - Patterns should be consistently enforced + +- **Continuous Improvement:** + - Monitor code review comments + - Track common development questions + - Update rules after major refactors + - Add links to relevant documentation + - Cross-reference related rules + +- **Rule Deprecation:** + - Mark outdated patterns as deprecated + - Remove rules that no longer apply + - Update references to deprecated rules + - Document migration paths for old patterns + +- **Documentation Updates:** + - Keep examples synchronized with code + - Update references to external docs + - Maintain links between related rules + - Document breaking changes +Follow [cursor_rules.mdc](mdc:.cursor/rules/cursor_rules.mdc) for proper rule formatting and structure. diff --git a/.cursor/rules/stimulus_conventions.mdc b/.cursor/rules/stimulus_conventions.mdc new file mode 100644 index 00000000..73bd16f8 --- /dev/null +++ b/.cursor/rules/stimulus_conventions.mdc @@ -0,0 +1,64 @@ +--- +description: +globs: +alwaysApply: false +--- +This rule describes how to write Stimulus controllers. + +- **Use declarative actions, not imperative event listeners** + - Instead of assigning a Stimulus target and binding it to an event listener in the initializer, always write Controllers + ERB views declaratively by using Stimulus actions in ERB to call methods in the Stimulus JS controller. Below are good vs. bad code. + + BAD code: + + ```js + // BAD!!!! DO NOT DO THIS!! + // Imperative - controller does all the work + export default class extends Controller { + static targets = ["button", "content"] + + connect() { + this.buttonTarget.addEventListener("click", this.toggle.bind(this)) + } + + toggle() { + this.contentTarget.classList.toggle("hidden") + this.buttonTarget.textContent = this.contentTarget.classList.contains("hidden") ? "Show" : "Hide" + } + } + ``` + + GOOD code: + + ```erb + + +