From 2e7d2018665e0b431cefecc24f1de135486fe391 Mon Sep 17 00:00:00 2001 From: Steffen Beyer Date: Thu, 7 May 2026 10:08:35 +0200 Subject: [PATCH] chore: curate plugin usage rules Sync AGENTS.md from a focused UsageRules config that includes Tribes and tribes_plugin_api guidance for plugin authors. Keep Ash deep rules linked to reduce context size and make scripts/plugin executable so the documented workflow works directly. --- AGENTS.md | 1622 ++++-------------------------------------------- mix.exs | 16 +- scripts/plugin | 0 3 files changed, 148 insertions(+), 1490 deletions(-) mode change 100644 => 100755 scripts/plugin diff --git a/AGENTS.md b/AGENTS.md index 8a71b87..79cd663 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,7 +55,7 @@ mix usage_rules.docs Enum.zip/1 ## Searching Documentation -You should also consult the documentation of any tools you are using, early and often. The best +You should also consult the documentation of any tools you are using, early and often. The best way to accomplish this is to use the `usage_rules.search_docs` mix task. Once you have found what you are looking for, use the links in the search results to get more detail. For example: @@ -132,55 +132,6 @@ mix usage_rules.search_docs "Enum.zip" --query-by title - Use `dbg/1` to print values while debugging. This will display the formatted value and other relevant information in the console. - -## usage_rules usage -_A config-driven dev tool for Elixir projects to manage AGENTS.md files and agent skills from dependencies_ - -## Using Usage Rules - -Many packages have usage rules, which you should *thoroughly* consult before taking any -action. These usage rules contain guidelines and rules *directly from the package authors*. -They are your best source of knowledge for making decisions. - -## Modules & functions in the current app and dependencies - -When looking for docs for modules & functions that are dependencies of the current project, -or for Elixir itself, use `mix usage_rules.docs` - -``` -# Search a whole module -mix usage_rules.docs Enum - -# Search a specific function -mix usage_rules.docs Enum.zip - -# Search a specific function & arity -mix usage_rules.docs Enum.zip/1 -``` - - -## Searching Documentation - -You should also consult the documentation of any tools you are using, early and often. The best -way to accomplish this is to use the `usage_rules.search_docs` mix task. Once you have -found what you are looking for, use the links in the search results to get more detail. For example: - -``` -# Search docs for all packages in the current application, including Elixir -mix usage_rules.search_docs Enum.zip - -# Search docs for specific packages -mix usage_rules.search_docs Req.get -p req - -# Search docs for multi-word queries -mix usage_rules.search_docs "making requests" -p req - -# Search only in titles (useful for finding specific functions/modules) -mix usage_rules.search_docs "Enum.zip" --query-by title -``` - - - ## usage_rules:otp usage # OTP Usage Rules @@ -221,65 +172,6 @@ mix usage_rules.search_docs "Enum.zip" --query-by title - **Always** invoke `mix ecto.gen.migration migration_name_using_underscores` when generating migration files, so the correct timestamp and conventions are applied - -## phoenix:elixir usage -## Elixir guidelines - -- Elixir lists **do not support index based access via the access syntax** - - **Never do this (invalid)**: - - i = 0 - mylist = ["blue", "green"] - mylist[i] - - Instead, **always** use `Enum.at`, pattern matching, or `List` for index based list access, ie: - - i = 0 - mylist = ["blue", "green"] - Enum.at(mylist, i) - -- Elixir variables are immutable, but can be rebound, so for block expressions like `if`, `case`, `cond`, etc - you *must* bind the result of the expression to a variable if you want to use it and you CANNOT rebind the result inside the expression, ie: - - # INVALID: we are rebinding inside the `if` and the result never gets assigned - if connected?(socket) do - socket = assign(socket, :val, val) - end - - # VALID: we rebind the result of the `if` to a new variable - socket = - if connected?(socket) do - assign(socket, :val, val) - end - -- **Never** nest multiple modules in the same file as it can cause cyclic dependencies and compilation errors -- **Never** use map access syntax (`changeset[:field]`) on structs as they do not implement the Access behaviour by default. For regular structs, you **must** access the fields directly, such as `my_struct.field` or use higher level APIs that are available on the struct if they exist, `Ecto.Changeset.get_field/2` for changesets -- Elixir's standard library has everything necessary for date and time manipulation. Familiarize yourself with the common `Time`, `Date`, `DateTime`, and `Calendar` interfaces by accessing their documentation as necessary. **Never** install additional dependencies unless asked or for date/time parsing (which you can use the `date_time_parser` package) -- Don't use `String.to_atom/1` on user input (memory leak risk) -- Predicate function names should not start with `is_` and should end in a question mark. Names like `is_thing` should be reserved for guards -- Elixir's builtin OTP primitives like `DynamicSupervisor` and `Registry`, require names in the child spec, such as `{DynamicSupervisor, name: MyApp.MyDynamicSup}`, then you can use `DynamicSupervisor.start_child(MyApp.MyDynamicSup, child_spec)` -- Use `Task.async_stream(collection, callback, options)` for concurrent enumeration with back-pressure. The majority of times you will want to pass `timeout: :infinity` as option - -## Mix guidelines - -- Read the docs and options before using tasks (by using `mix help task_name`) -- To debug test failures, run tests in a specific file with `mix test test/my_test.exs` or run all previously failed tests with `mix test --failed` -- `mix deps.clean --all` is **almost never needed**. **Avoid** using it unless you have good reason - -## Test guidelines - -- **Always use `start_supervised!/1`** to start processes in tests as it guarantees cleanup between tests -- **Avoid** `Process.sleep/1` and `Process.alive?/1` in tests - - Instead of sleeping to wait for a process to finish, **always** use `Process.monitor/1` and assert on the DOWN message: - - ref = Process.monitor(pid) - assert_receive {:DOWN, ^ref, :process, ^pid, :normal} - - - Instead of sleeping to synchronize before the next call, **always** use `_ = :sys.get_state/1` to ensure the process has handled prior messages - - - ## phoenix:html usage ## Phoenix HTML guidelines @@ -628,1391 +520,143 @@ Ash is an opinionated, composable framework for building applications in Elixir. ## ash:actions usage -# Actions - -- Create specific, well-named actions rather than generic ones -- Put all business logic inside action definitions -- Use hooks like `Ash.Changeset.after_action/2`, `Ash.Changeset.before_action/2` to add additional logic - inside the same transaction. -- Use hooks like `Ash.Changeset.after_transaction/2`, `Ash.Changeset.before_transaction/2` to add additional logic - outside the transaction. -- Use action arguments for inputs that need validation -- Use preparations to modify queries before execution -- Preparations support `where` clauses for conditional execution -- Use `only_when_valid?` to skip preparations when the query is invalid -- Use changes to modify changesets before execution -- Use validations to validate changesets before execution -- Prefer domain code interfaces to call actions instead of directly building queries/changesets and calling functions in the `Ash` module -- A resource could be *only generic actions*. This can be useful when you are using a resource only to model behavior. -- Instead of defining functions in the domain, you should be defining actions and exposing them through code interface calls in the domain. Use standard actions when they fit what you're doing and generic actions when you need arbitrary functionality. - -## Error Handling - -Functions to call actions, like `Ash.create` and code interfaces like `MyApp.Accounts.register_user` all return ok/error tuples. All have `!` variations, like `Ash.create!` and `MyApp.Accounts.register_user!`. Use the `!` variations when you want to "let it crash", like if looking something up that should definitely exist, or calling an action that should always succeed. Always prefer the raising `!` variation over something like `{:ok, user} = MyApp.Accounts.register_user(...)`. - -All Ash code returns errors in the form of `{:error, error_class}`. Ash categorizes errors into four main classes: - -1. **Forbidden** (`Ash.Error.Forbidden`) - Occurs when a user attempts an action they don't have permission to perform -2. **Invalid** (`Ash.Error.Invalid`) - Occurs when input data doesn't meet validation requirements -3. **Framework** (`Ash.Error.Framework`) - Occurs when there's an issue with how Ash is being used -4. **Unknown** (`Ash.Error.Unknown`) - Occurs for unexpected errors that don't fit the other categories - -These error classes help you catch and handle errors at an appropriate level of granularity. An error class will always be the "worst" (highest in the above list) error class from above. Each error class can contain multiple underlying errors, accessible via the `errors` field on the exception. - -## Using Validations - -Validations ensure that data meets your business requirements before it gets processed by an action. Unlike changes, validations cannot modify the changeset - they can only validate it or add errors. - -Validations work on both changesets and queries. Built-in validations that support queries include: -- `action_is`, `argument_does_not_equal`, `argument_equals`, `argument_in` -- `compare`, `confirm`, `match`, `negate`, `one_of`, `present`, `string_length` -- Custom validations that implement the `supports/1` callback - -Common validation patterns: - -```elixir -# Built-in validations with custom messages -validate compare(:age, greater_than_or_equal_to: 18) do - message "You must be at least 18 years old" -end -validate match(:email, "@") -validate one_of(:status, [:active, :inactive, :pending]) - -# Conditional validations with where clauses -validate present(:phone_number) do - where present(:contact_method) and eq(:contact_method, "phone") -end - -# only_when_valid? - skip validation if prior validations failed -validate expensive_validation() do - only_when_valid? true -end - -# Action-specific vs global validations -actions do - create :sign_up do - validate present([:email, :password]) # Only for this action - end - - read :search do - argument :email, :string - validate match(:email, ~r/^[^\s]+@[^\s]+\.[^\s]+$/) # Validates query arguments - end -end - -validations do - validate present([:title, :body]), on: [:create, :update] # Multiple actions -end -``` - -- Create **custom validation modules** for complex validation logic: - ```elixir - defmodule MyApp.Validations.UniqueUsername do - use Ash.Resource.Validation - - @impl true - def init(opts), do: {:ok, opts} - - @impl true - def validate(changeset, _opts, _context) do - # Validation logic here - # Return :ok or {:error, message} - end - end - - # Usage in resource: - validate {MyApp.Validations.UniqueUsername, []} - ``` - -- Make validations **atomic** when possible to ensure they work correctly with direct database operations by implementing the `atomic/3` callback in custom validation modules. - - ```elixir - defmodule MyApp.Validations.IsEven do - # transform and validate opts - - use Ash.Resource.Validation - - @impl true - def init(opts) do - if is_atom(opts[:attribute]) do - {:ok, opts} - else - {:error, "attribute must be an atom!"} - end - end - - @impl true - # This is optional, but useful to have in addition to validation - # so you get early feedback for validations that can otherwise - # only run in the datalayer - def validate(changeset, opts, _context) do - value = Ash.Changeset.get_attribute(changeset, opts[:attribute]) - - if is_nil(value) || (is_number(value) && rem(value, 2) == 0) do - :ok - else - {:error, field: opts[:attribute], message: "must be an even number"} - end - end - - @impl true - def atomic(changeset, opts, context) do - {:atomic, - # the list of attributes that are involved in the validation - [opts[:attribute]], - # the condition that should cause the error - # here we refer to the new value or the current value - expr(rem(^atomic_ref(opts[:attribute]), 2) != 0), - # the error expression - expr( - error(^InvalidAttribute, %{ - field: ^opts[:attribute], - # the value that caused the error - value: ^atomic_ref(opts[:attribute]), - # the message to display - message: ^(context.message || "%{field} must be an even number"), - vars: %{field: ^opts[:attribute]} - }) - ) - } - end - end - ``` - -- **Avoid redundant validations** - Don't add validations that duplicate attribute constraints: - ```elixir - # WRONG - redundant validation - attribute :name, :string do - allow_nil? false - constraints min_length: 1 - end - - validate present(:name) do # Redundant! allow_nil? false already handles this - message "Name is required" - end - - validate attribute_does_not_equal(:name, "") do # Redundant! min_length: 1 already handles this - message "Name cannot be empty" - end - - # CORRECT - let attribute constraints handle basic validation - attribute :name, :string do - allow_nil? false - constraints min_length: 1 - end - ``` - -## Using Preparations - -Preparations modify queries before they're executed. They are used to add filters, sorts, or other query modifications based on the query context. - -Common preparation patterns: - -```elixir -# Built-in preparations -prepare build(sort: [created_at: :desc]) -prepare build(filter: [active: true]) - -# Conditional preparations with where clauses -prepare build(filter: [visible: true]) do - where argument_equals(:include_hidden, false) -end - -# only_when_valid? - skip preparation if prior validations failed -prepare expensive_preparation() do - only_when_valid? true -end - -# Action-specific vs global preparations -actions do - read :recent do - prepare build(sort: [created_at: :desc], limit: 10) - end -end - -preparations do - prepare build(filter: [deleted: false]), on: [:read, :update] -end -``` - -## Using Changes - -Changes allow you to modify the changeset before it gets processed by an action. Unlike validations, changes can manipulate attribute values, add attributes, or perform other data transformations. - -Common change patterns: - -```elixir -# Built-in changes with conditions -change set_attribute(:status, "pending") -change relate_actor(:creator) do - where present(:actor) -end -change atomic_update(:counter, expr(^counter + 1)) - -# Action-specific vs global changes -actions do - create :sign_up do - change set_attribute(:joined_at, expr(now())) # Only for this action - end -end - -changes do - change set_attribute(:updated_at, expr(now())), on: :update # Multiple actions - change manage_relationship(:items, type: :append), on: [:create, :update] -end -``` - -- Create **custom change modules** for reusable transformation logic: - ```elixir - defmodule MyApp.Changes.SlugifyTitle do - use Ash.Resource.Change - - def change(changeset, _opts, _context) do - title = Ash.Changeset.get_attribute(changeset, :title) - - if title do - slug = title |> String.downcase() |> String.replace(~r/[^a-z0-9]+/, "-") - Ash.Changeset.change_attribute(changeset, :slug, slug) - else - changeset - end - end - end - - # Usage in resource: - change {MyApp.Changes.SlugifyTitle, []} - ``` - -- Create a **change module with lifecycle hooks** to handle complex multi-step operations: - - ```elixir - defmodule MyApp.Changes.ProcessOrder do - use Ash.Resource.Change - - def change(changeset, _opts, context) do - changeset - |> Ash.Changeset.before_transaction(fn changeset -> - # Runs before the transaction starts - # Use for external API calls, logging, etc. - MyApp.ExternalService.reserve_inventory(changeset, scope: context) - changeset - end) - |> Ash.Changeset.before_action(fn changeset -> - # Runs inside the transaction before the main action - # Use for related database changes in the same transaction - Ash.Changeset.change_attribute(changeset, :processed_at, DateTime.utc_now()) - end) - |> Ash.Changeset.after_action(fn changeset, result -> - # Runs inside the transaction after the main action, only on success - # Use for related database changes that depend on the result - MyApp.Inventory.update_stock_levels(result, scope: context) - {changeset, result} - end) - |> Ash.Changeset.after_transaction(fn changeset, - {:ok, result} -> - # Runs after the transaction completes (success or failure) - # Use for notifications, external systems, etc. - MyApp.Mailer.send_order_confirmation(result, scope: context) - {changeset, result} - - {:error, error} -> - # Runs after the transaction completes (success or failure) - # Use for notifications, external systems, etc. - MyApp.Mailer.send_order_issue_notice(result, scope: context) - {:error, error} - end) - end - end - - # Usage in resource: - change {MyApp.Changes.ProcessOrder, []} - ``` - -## Atomic Changes - -Atomic changes execute directly in the database as part of the update query, without requiring the record to be loaded first. This provides better performance and correct behavior under concurrent updates. - -**Why atomic matters:** -- Avoids race conditions (e.g., incrementing a counter) -- Better performance (no round-trip to load the record) -- Required for bulk operations to work efficiently - -**Built-in atomic changes:** -```elixir -# Increment a counter atomically -change atomic_update(:view_count, expr(view_count + 1)) - -# Set a value using an expression -change set_attribute(:updated_at, expr(now())) -``` - -**Making custom changes atomic:** -Implement the `atomic/3` callback to support atomic execution: - -```elixir -defmodule MyApp.Changes.IncrementVersion do - use Ash.Resource.Change - - @impl true - def change(changeset, _opts, _context) do - # Fallback for non-atomic execution - current = Ash.Changeset.get_attribute(changeset, :version) || 0 - Ash.Changeset.change_attribute(changeset, :version, current + 1) - end - - @impl true - def atomic(_changeset, _opts, _context) do - # Atomic implementation - runs in the database - {:atomic, %{version: expr(coalesce(version, 0) + 1)}} - end -end -``` - -## Using `require_atomic? false` - -By default, update and destroy actions require all changes and validations to support atomic execution. If they don't, the action will raise an error. - -**IMPORTANT:** When you see `require_atomic? false` on an action, carefully consider whether it is truly necessary. This option should be used sparingly. - -**When `require_atomic? false` is needed:** -- The action has `before_action` or `around_action` hooks that need to read or modify the record -- A change reads the current record state (e.g., `Ash.Changeset.get_data/2`) and cannot be rewritten atomically -- Complex validations that cannot be expressed as database expressions - -**When `require_atomic? false` is NOT needed:** -- Simple attribute transformations (these can usually be made atomic) -- Setting timestamps or default values (use `expr(now())` instead) -- Incrementing counters (use `atomic_update/2`) -- After-action hooks (these don't prevent atomic execution) -- After-transaction hooks (these don't prevent atomic execution) - -```elixir -actions do - update :update do - # AVOID unless truly necessary - require_atomic? false - end - - update :increment_views do - # GOOD - fully atomic, no need to disable - change atomic_update(:view_count, expr(view_count + 1)) - end -end -``` - -If you find yourself adding `require_atomic? false`, first check if your changes and validations can be rewritten with `atomic/3` callbacks. Only disable atomic requirements when the action genuinely needs to read or manipulate the record in hooks. - -## Custom Modules vs. Anonymous Functions - -Prefer to put code in its own module and refer to that in changes, preparations, validations etc. - -For example, prefer this: - -```elixir -defmodule MyApp.MyDomain.MyResource.Changes.SlugifyName do - use Ash.Resource.Change - - def change(changeset, _, _) do - Ash.Changeset.before_action(changeset, fn changeset, _ -> - slug = MyApp.Slug.get() - Ash.Changeset.force_change_attribute(changeset, :slug, slug) - end) - end -end - -change MyApp.MyDomain.MyResource.Changes.SlugifyName -``` - -## Action Types - -- **Read**: For retrieving records -- **Create**: For creating records -- **Update**: For changing records -- **Destroy**: For removing records -- **Generic**: For custom operations that don't fit the other types - - - +[ash:actions usage rules](deps/ash/usage-rules/actions.md) - -## ash:aggregates usage -# Aggregates - -Aggregates allow you to retrieve summary information over groups of related data, like counts, sums, or averages. Define aggregates in the `aggregates` block of a resource. - -Aggregates can work over relationships or directly over unrelated resources: - -```elixir -aggregates do - # Related aggregates - use relationship path - count :published_post_count, :posts do - filter expr(published == true) - end - - sum :total_sales, :orders, :amount - - exists :is_admin, :roles do - filter expr(name == "admin") - end - - # Unrelated aggregates - use resource module directly - count :matching_profiles_count, Profile do - filter expr(name == parent(name)) - end - - sum :total_report_score, Report, :score do - filter expr(author_name == parent(name)) - end - - exists :has_reports, Report do - filter expr(author_name == parent(name)) - end -end -``` - -For unrelated aggregates, use `parent/1` to reference fields from the source resource. - -## Aggregate Types - -- **count**: Counts related items meeting criteria -- **sum**: Sums a field across related items -- **exists**: Returns boolean indicating if matching related items exist (also supports unrelated resources) -- **first**: Gets the first related value matching criteria -- **list**: Lists the related values for a specific field -- **max**: Gets the maximum value of a field -- **min**: Gets the minimum value of a field -- **avg**: Gets the average value of a field - -## Using Aggregates - -```elixir -# Using code interface options (preferred) -users = MyDomain.list_users!( - load: [:published_post_count, :total_sales], - query: [ - filter: [published_post_count: [greater_than: 5]], - sort: [published_post_count: :desc] - ] -) - -# Manual query building (for complex cases) -User |> Ash.Query.filter(published_post_count > 5) |> Ash.read!() - -# Loading on existing records -Ash.load!(users, :published_post_count) -``` - -### Join Filters - -For complex aggregates involving multiple relationships, use join filters: - -```elixir -aggregates do - sum :redeemed_deal_amount, [:redeems, :deal], :amount do - # Filter on the aggregate as a whole - filter expr(redeems.redeemed == true) - - # Apply filters to specific relationship steps - join_filter :redeems, expr(redeemed == true) - join_filter [:redeems, :deal], expr(active == parent(require_active)) - end -end -``` - -## Inline Aggregates - -Use aggregates inline within expressions: - -```elixir -# Related inline aggregates -calculate :grade_percentage, :decimal, expr( - count(answers, query: [filter: expr(correct == true)]) * 100 / - count(answers) -) - -# Unrelated inline aggregates -calculate :profile_count, :integer, expr( - count(Profile, filter: expr(name == parent(name))) -) - -calculate :stats, :map, expr(%{ - profiles: count(Profile, filter: expr(active == true)), - reports: count(Report, filter: expr(author_name == parent(name))), - has_active_profile: exists(Profile, active == true and name == parent(name)) -}) -``` - - - - -## ash:authorization usage -# Authorization - -- When performing administrative actions, you can bypass authorization with `authorize?: false` -- To run actions as a particular user, look that user up and pass it as the `actor` option -- Always set the actor on the query/changeset/input, not when calling the action -- Use policies to define authorization rules - -```elixir -# Good -Post -|> Ash.Query.for_read(:read, %{}, actor: current_user) -|> Ash.read!() - -# BAD, DO NOT DO THIS -Post -|> Ash.Query.for_read(:read, %{}) -|> Ash.read!(actor: current_user) -``` - -## Policies - -To use policies, add the `Ash.Policy.Authorizer` to your resource: - -```elixir -defmodule MyApp.Post do - use Ash.Resource, - domain: MyApp.Blog, - authorizers: [Ash.Policy.Authorizer] - - # Rest of resource definition... -end -``` - -## Policy Basics - -Policies determine what actions on a resource are permitted for a given actor. Define policies in the `policies` block: - -```elixir -policies do - # A simple policy that applies to all read actions - policy action_type(:read) do - # Authorize if record is public - authorize_if expr(public == true) - - # Authorize if actor is the owner - authorize_if relates_to_actor_via(:owner) - end - - # A policy for create actions - policy action_type(:create) do - # Only allow active users to create records - forbid_unless actor_attribute_equals(:active, true) - - # Ensure the record being created relates to the actor - authorize_if relating_to_actor(:owner) - end -end -``` - -## Policy Evaluation Flow - -Policies evaluate from top to bottom with the following logic: - -1. All policies that apply to an action must pass for the action to be allowed -2. Within each policy, checks evaluate from top to bottom -3. The first check that produces a decision determines the policy result -4. If no check produces a decision, the policy defaults to forbidden - -## IMPORTANT: Policy Check Logic - -**the first check that yields a result determines the policy outcome** - -```elixir -# WRONG - This is OR logic, not AND logic! -policy action_type(:update) do - authorize_if actor_attribute_equals(:admin?, true) # If this passes, policy passes - authorize_if relates_to_actor_via(:owner) # Only checked if first fails -end -``` - -To require BOTH conditions in that example, you would use `forbid_unless` for the first condition: - -```elixir -# CORRECT - This requires BOTH conditions -policy action_type(:update) do - forbid_unless actor_attribute_equals(:admin?, true) # Must be admin - authorize_if relates_to_actor_via(:owner) # AND must be owner -end -``` - -Alternative patterns for AND logic: -- Use multiple separate policies (each must pass independently) -- Use a single complex expression with `expr(condition1 and condition2)` -- Use `forbid_unless` for required conditions, then `authorize_if` for the final check - -## Bypass Policies - -Use bypass policies to allow certain actors to bypass other policy restrictions. This should be used almost exclusively for admin bypasses. - -```elixir -policies do - # Bypass policy for admins - if this passes, other policies don't need to pass - bypass actor_attribute_equals(:admin, true) do - authorize_if always() - end - - # Regular policies follow... - policy action_type(:read) do - # ... - end -end -``` - -## Field Policies - -Field policies control access to specific fields (attributes, calculations, aggregates): - -```elixir -field_policies do - # Only supervisors can see the salary field - field_policy :salary do - authorize_if actor_attribute_equals(:role, :supervisor) - end - - # Allow access to all other fields - field_policy :* do - authorize_if always() - end -end -``` - -## Policy Checks - -There are two main types of checks used in policies: - -1. **Simple checks** - Return true/false answers (e.g., "is the actor an admin?") -2. **Filter checks** - Return filters to apply to data (e.g., "only show records owned by the actor") - -You can use built-in checks or create custom ones: - -```elixir -# Built-in checks -authorize_if actor_attribute_equals(:role, :admin) -authorize_if relates_to_actor_via(:owner) -authorize_if expr(public == true) - -# Custom check module -authorize_if MyApp.Checks.ActorHasPermission -``` - -### Custom Policy Checks - -Create custom checks by implementing `Ash.Policy.SimpleCheck` or `Ash.Policy.FilterCheck`: - -```elixir -# Simple check - returns true/false -defmodule MyApp.Checks.ActorHasRole do - use Ash.Policy.SimpleCheck - - def match?(%{role: actor_role}, _context, opts) do - actor_role == (opts[:role] || :admin) - end - def match?(_, _, _), do: false -end - -# Filter check - returns query filter -defmodule MyApp.Checks.VisibleToUserLevel do - use Ash.Policy.FilterCheck - - def filter(actor, _authorizer, _opts) do - expr(visibility_level <= ^actor.user_level) - end -end - -# Usage -policy action_type(:read) do - authorize_if {MyApp.Checks.ActorHasRole, role: :manager} - authorize_if MyApp.Checks.VisibleToUserLevel -end -``` - - - - -## ash:calculations usage -# Calculations - -Calculations allow you to define derived values based on a resource's attributes or related data. Define calculations in the `calculations` block of a resource: - -```elixir -calculations do - # Simple expression calculation - calculate :full_name, :string, expr(first_name <> " " <> last_name) - - # Expression with conditions - calculate :status_label, :string, expr( - cond do - status == :active -> "Active" - status == :pending -> "Pending Review" - true -> "Inactive" - end - ) - - # Using module calculations for more complex logic - calculate :risk_score, :integer, {MyApp.Calculations.RiskScore, min: 0, max: 100} -end -``` - -## Expression Calculations - -Expression calculations use Ash expressions and can be pushed down to the data layer when possible: - -```elixir -calculations do - # Simple string concatenation - calculate :full_name, :string, expr(first_name <> " " <> last_name) - - # Math operations - calculate :total_with_tax, :decimal, expr(amount * (1 + tax_rate)) - - # Date manipulation - calculate :days_since_created, :integer, expr( - date_diff(^now(), inserted_at, :day) - ) -end -``` - -## Expressions - -In order to use expressions outside of resources, changes, preparations etc. you will need to use `Ash.Expr`. - -It provides both `expr/1` and template helpers like `actor/1` and `arg/1`. - -For example: - -```elixir -import Ash.Expr - -Author -|> Ash.Query.aggregate(:count_of_my_favorited_posts, :count, [:posts], query: [ - filter: expr(favorited_by(user_id: ^actor(:id))) -]) -``` - -See the expressions guide for more information on what is available in expresisons and -how to use them. - -## Module Calculations - -For complex calculations, create a module that implements `Ash.Resource.Calculation`: - -```elixir -defmodule MyApp.Calculations.FullName do - use Ash.Resource.Calculation - - # Validate and transform options - @impl true - def init(opts) do - {:ok, Map.put_new(opts, :separator, " ")} - end - - # Specify what data needs to be loaded - @impl true - def load(_query, _opts, _context) do - [:first_name, :last_name] - end - - # Implement the calculation logic - @impl true - def calculate(records, opts, _context) do - Enum.map(records, fn record -> - [record.first_name, record.last_name] - |> Enum.reject(&is_nil/1) - |> Enum.join(opts.separator) - end) - end -end - -# Usage in a resource -calculations do - calculate :full_name, :string, {MyApp.Calculations.FullName, separator: ", "} -end -``` - -## Calculations with Arguments - -You can define calculations that accept arguments: - -```elixir -calculations do - calculate :full_name, :string, expr(first_name <> ^arg(:separator) <> last_name) do - argument :separator, :string do - allow_nil? false - default " " - constraints [allow_empty?: true, trim?: false] - end - end -end -``` - -## Using Calculations - -```elixir -# Using code interface options (preferred) -users = MyDomain.list_users!(load: [full_name: [separator: ", "]]) - -# Filtering and sorting -users = MyDomain.list_users!( - query: [ - filter: [full_name: [separator: " ", value: "John Doe"]], - sort: [full_name: {[separator: " "], :asc}] - ] -) - -# Manual query building (for complex cases) -User |> Ash.Query.load(full_name: [separator: ", "]) |> Ash.read!() - -# Loading on existing records -Ash.load!(users, :full_name) -``` - -### Code Interface for Calculations - -Define calculation functions on your domain for standalone use: - -```elixir -# In your domain -resource User do - define_calculation :full_name, args: [:first_name, :last_name, {:optional, :separator}] -end - -# Then call it directly -MyDomain.full_name("John", "Doe", ", ") # Returns "John, Doe" -``` - - - - -## ash:code_interfaces usage -# Code Interfaces - -Domains and Resources can define code interfaces. Prefer writing code interfaces instead of regular elixir functions. - -Use code interfaces on domains to define the contract for calling into Ash resources. See the [Code interface guide for more](https://hexdocs.pm/ash/code-interfaces.html). - -Define code interfaces on the domain, like this: - -```elixir -resource ResourceName do - define :fun_name, action: :action_name -end -``` - -For more complex interfaces with custom transformations: - -```elixir -define :custom_action do - action :action_name - args [:arg1, :arg2] - - custom_input :arg1, MyType do - transform do - to :target_field - using &MyModule.transform_function/1 - end - end -end -``` - -Prefer using the primary read action for "get" style code interfaces, and using `get_by` when the field you are looking up by is the primary key or has an `identity` on the resource. - -```elixir -resource ResourceName do - define :get_thing, action: :read, get_by: [:id] -end -``` - -**Avoid direct Ash calls in web modules** - Don't use `Ash.get!/2` and `Ash.load!/2` directly in LiveViews/Controllers, similar to avoiding `Repo.get/2` outside context modules: - -You can also pass additional inputs in to code interfaces before the options: - -```elixir -resource ResourceName do - define :create, action: :action_name, args: [:field1] -end -``` - -```elixir -Domain.create!(field1_value, %{field2: field2_value}, actor: current_user) -``` - -You should generally prefer using this map of extra inputs over defining optional arguments. - -```elixir -# BAD - in LiveView/Controller -group = MyApp.Resource |> Ash.get!(id) |> Ash.load!(rel: [:nested]) - -# GOOD - use code interface with get_by -resource DashboardGroup do - define :get_dashboard_group_by_id, action: :read, get_by: [:id] -end - -# Then call: -MyApp.Domain.get_dashboard_group_by_id!(id, load: [rel: [:nested]]) -``` - -**Code interface options** - Prefer passing options directly to code interface functions rather than building queries manually: - -```elixir -# PREFERRED - Use the query option for filter, sort, limit, etc. -# the query option is passed to `Ash.Query.build/2` -posts = MyApp.Blog.list_posts!( - query: [ - filter: [status: :published], - sort: [published_at: :desc], - limit: 10 - ], - load: [author: :profile, comments: [:author]] -) - -# All query-related options go in the query parameter -users = MyApp.Accounts.list_users!( - query: [filter: [active: true], sort: [created_at: :desc]], - load: [:profile] -) - -# AVOID - Verbose manual query building -query = MyApp.Post |> Ash.Query.filter(...) |> Ash.Query.load(...) -posts = Ash.read!(query) -``` - -Supported options: `load:`, `query:` (which accepts `filter:`, `sort:`, `limit:`, `offset:`, etc.), `page:`, `stream?:` - -**Using Scopes in LiveViews** - When using `Ash.Scope`, the scope will typically be assigned to `scope` in LiveViews and used like so: - -```elixir -# In your LiveView -MyApp.Blog.create_post!("new post", scope: socket.assigns.scope) -``` - -Inside action hooks and callbacks, use the provided `context` parameter as your scope instead: - -```elixir -|> Ash.Changeset.before_transaction(fn changeset, context -> - MyApp.ExternalService.reserve_inventory(changeset, scope: context) - changeset -end) -``` - -## Authorization Functions - -For each action defined in a code interface, Ash automatically generates corresponding authorization check functions: - -- `can_action_name?(actor, params \\ %{}, opts \\ [])` - Returns `true`/`false` for authorization checks -- `can_action_name(actor, params \\ %{}, opts \\ [])` - Returns `{:ok, true/false}` or `{:error, reason}` - -Example usage: -```elixir -# Check if user can create a post -if MyApp.Blog.can_create_post?(current_user) do - # Show create button -end - -# Check if user can update a specific post -if MyApp.Blog.can_update_post?(current_user, post) do - # Show edit button -end - -# Check if user can destroy a specific comment -if MyApp.Blog.can_destroy_comment?(current_user, comment) do - # Show delete button -end -``` - -These functions are particularly useful for conditional rendering of UI elements based on user permissions. - - - - -## ash:code_structure usage -# Code Structure & Organization - -- Organize code around domains and resources -- Each resource should be focused and well-named -- Create domain-specific actions rather than generic CRUD operations -- Put business logic inside actions rather than in external modules -- Use resources to model your domain entities - - - -## ash:data_layers usage -# Data Layers - -Data layers determine how resources are stored and retrieved. Examples of data layers: - -- **Postgres**: For storing resources in PostgreSQL (via `AshPostgres`) -- **ETS**: For in-memory storage (`Ash.DataLayer.Ets`) -- **Mnesia**: For distributed storage (`Ash.DataLayer.Mnesia`) -- **Embedded**: For resources embedded in other resources (`data_layer: :embedded`) (typically JSON under the hood) -- **Ash.DataLayer.Simple**: For resources that aren't persisted at all. Leave off the data layer, as this is the default. - -Specify a data layer when defining a resource: - -```elixir -defmodule MyApp.Post do - use Ash.Resource, - domain: MyApp.Blog, - data_layer: AshPostgres.DataLayer - - postgres do - table "posts" - repo MyApp.Repo - end - - # ... attributes, relationships, etc. -end -``` - -For embedded resources: - -```elixir -defmodule MyApp.Address do - use Ash.Resource, - data_layer: :embedded - - attributes do - attribute :street, :string - attribute :city, :string - attribute :state, :string - attribute :zip, :string - end -end -``` - -Each data layer has its own configuration options and capabilities. Refer to the rules & documentation of the specific data layer package for more details. - - - - -## ash:exist_expressions usage -# Exists Expressions - -Use `exists/2` to check for the existence of records, either through relationships or unrelated resources: - -### Related Exists - -```elixir -# Check if user has any admin roles -Ash.Query.filter(User, exists(roles, name == "admin")) - -# Check if post has comments with high scores -Ash.Query.filter(Post, exists(comments, score > 50)) -``` - -### Unrelated Exists - -```elixir -# Check if any profile exists with the same name -Ash.Query.filter(User, exists(Profile, name == parent(name))) - -# Check if user has any reports -Ash.Query.filter(User, exists(Report, author_name == parent(name))) - -# Complex existence checks -Ash.Query.filter(User, - active == true and - exists(Profile, active == true and name == parent(name)) -) -``` - -Unrelated exists expressions automatically apply authorization using the target resource's primary read action. Use `parent/1` to reference fields from the source resource. - - - -## ash:generating_code usage -# Generating Code - -Use `mix ash.gen.*` tasks as a basis for code generation when possible. Check the task docs with `mix help `. -Be sure to use `--yes` to bypass confirmation prompts. Use `--yes --dry-run` to preview the changes. - - - ## ash:migrations usage -# Migrations and Schema Changes - -After creating or modifying Ash code, run `mix ash.codegen ` to ensure any required additional changes are made (like migrations are generated). The name of the migration should be lower_snake_case. In a longer running dev session it's usually better to use `mix ash.codegen --dev` as you go and at the end run the final codegen with a sensible name describing all the changes made in the session. - - +[ash:migrations usage rules](deps/ash/usage-rules/migrations.md) - -## ash:query_filter usage -# Ash.Query.filter is a macro - -**Important**: You must `require Ash.Query` if you want to use `Ash.Query.filter/2`, as it is a macro. - -If you see errors like the following: - -``` -Ash.Query.filter(MyResource, id == ^id) -error: misplaced operator ^id - -The pin operator ^ is supported only inside matches or inside custom macros... -``` - -``` -iex(3)> Ash.Query.filter(MyResource, something == true) -error: undefined variable "something" -└─ iex:3 -``` - -You are very likely missing a `require Ash.Query` - -## Common Query Operations - -- **Filter**: `Ash.Query.filter(query, field == value)` -- **Sort**: `Ash.Query.sort(query, field: :asc)` -- **Load relationships**: `Ash.Query.load(query, [:author, :comments])` -- **Limit**: `Ash.Query.limit(query, 10)` -- **Offset**: `Ash.Query.offset(query, 20)` - - - - -## ash:querying_data usage -# Querying Data - -Use `Ash.Query` to build queries for reading data from your resources. The query module provides a declarative way to filter, sort, and load data. - - - - -## ash:relationships usage -# Relationships - -Relationships describe connections between resources and are a core component of Ash. Define relationships in the `relationships` block of a resource. - -## Best Practices for Relationships - -- Be descriptive with relationship names (e.g., use `:authored_posts` instead of just `:posts`) -- Configure foreign key constraints in your data layer if they have them (see `references` in AshPostgres) -- Always choose the appropriate relationship type based on your domain model - -### Relationship Types - -- For Polymorphic relationships, you can model them using `Ash.Type.Union`; see the “Polymorphic Relationships” guide for more information. - -```elixir -relationships do - # belongs_to - adds foreign key to source resource - belongs_to :owner, MyApp.User do - allow_nil? false - attribute_type :integer # defaults to :uuid - end - - # has_one - foreign key on destination resource - has_one :profile, MyApp.Profile - - # has_many - foreign key on destination resource, returns list - has_many :posts, MyApp.Post do - filter expr(published == true) - sort published_at: :desc - end - - # many_to_many - requires join resource - many_to_many :tags, MyApp.Tag do - through MyApp.PostTag - source_attribute_on_join_resource :post_id - destination_attribute_on_join_resource :tag_id - end -end -``` - -The join resource must be defined separately: - -```elixir -defmodule MyApp.PostTag do - use Ash.Resource, - data_layer: AshPostgres.DataLayer - - attributes do - uuid_primary_key :id - # Add additional attributes if you need metadata on the relationship - attribute :added_at, :utc_datetime_usec do - default &DateTime.utc_now/0 - end - end - - relationships do - belongs_to :post, MyApp.Post, primary_key?: true, allow_nil?: false - belongs_to :tag, MyApp.Tag, primary_key?: true, allow_nil?: false - end - - actions do - defaults [:read, :destroy, create: :*, update: :*] - end -end -``` - -## Loading Relationships - -```elixir -# Using code interface options (preferred) -post = MyDomain.get_post!(id, load: [:author, comments: [:author]]) - -# Complex loading with filters -posts = MyDomain.list_posts!( - query: [load: [comments: [filter: [is_approved: true], limit: 5]]] -) - -# Manual query building (for complex cases) -MyApp.Post -|> Ash.Query.load(comments: MyApp.Comment |> Ash.Query.filter(is_approved == true)) -|> Ash.read!() - -# Loading on existing records -Ash.load!(post, :author) -``` - -Prefer to use the `strict?` option when loading to only load necessary fields on related data. - -```elixir -MyApp.Post -|> Ash.Query.load([comments: [:title]], strict?: true) -``` - -## Managing Relationships - -There are two primary ways to manage relationships in Ash: - -### 1. Using `change manage_relationship/2-3` in Actions -Use this when input comes from action arguments: - -```elixir -actions do - update :update do - # Define argument for the related data - argument :comments, {:array, :map} do - allow_nil? false - end - - argument :new_tags, {:array, :map} - - # Link argument to relationship management - change manage_relationship(:comments, type: :append) - - # For different argument and relationship names - change manage_relationship(:new_tags, :tags, type: :append) - end -end -``` - -### 2. Using `Ash.Changeset.manage_relationship/3-4` in Custom Changes -Use this when building values programmatically: - -```elixir -defmodule MyApp.Changes.AssignTeamMembers do - use Ash.Resource.Change - - def change(changeset, _opts, context) do - members = determine_team_members(changeset, context.actor) - - Ash.Changeset.manage_relationship( - changeset, - :members, - members, - type: :append_and_remove - ) - end -end -``` - -### Quick Reference - Management Types -- `:append` - Add new related records, ignore existing -- `:append_and_remove` - Add new related records, remove missing -- `:remove` - Remove specified related records -- `:direct_control` - Full CRUD control (create/update/destroy) -- `:create` - Only create new records - -### Quick Reference - Common Options -- `on_lookup: :relate` - Look up and relate existing records -- `on_no_match: :create` - Create if no match found -- `on_match: :update` - Update existing matches -- `on_missing: :destroy` - Delete records not in input -- `value_is_key: :name` - Use field as key for simple values - -For comprehensive documentation, see the [Managing Relationships](https://hexdocs.pm/ash/relationships.html#managing-relationships) section. - -### Examples - -Creating a post with tags: -```elixir -MyDomain.create_post!(%{ - title: "New Post", - body: "Content here...", - tags: [%{name: "elixir"}, %{name: "ash"}] # Creates new tags -}) - -# Updating a post to replace its tags -MyDomain.update_post!(post, %{ - tags: [tag1.id, tag2.id] # Replaces tags with existing ones by ID -}) -``` - - - ## ash:testing usage -# Testing - -When testing resources: -- Test your domain actions through the code interface -- Use test utilities in `Ash.Test` -- Test authorization policies work as expected using `Ash.can?` -- Use `authorize?: false` in tests where authorization is not the focus -- Write generators using `Ash.Generator` -- Prefer to use raising versions of functions whenever possible, as opposed to pattern matching - -## Preventing Deadlocks in Concurrent Tests - -When running tests concurrently, using fixed values for identity attributes can cause deadlock errors. Multiple tests attempting to create records with the same unique values will conflict. - -### Use Globally Unique Values - -Always use globally unique values for identity attributes in tests: - -```elixir -# BAD - Can cause deadlocks in concurrent tests -%{email: "test@example.com", username: "testuser"} - -# GOOD - Use globally unique values -%{ - email: "test-#{System.unique_integer([:positive])}@example.com", - username: "user_#{System.unique_integer([:positive])}", - slug: "post-#{System.unique_integer([:positive])}" -} -``` - -### Creating Reusable Test Generators - -For better organization, create a generator module: - -```elixir -defmodule MyApp.TestGenerators do - use Ash.Generator - - def user(opts \\ []) do - changeset_generator( - User, - :create, - defaults: [ - email: "user-#{System.unique_integer([:positive])}@example.com", - username: "user_#{System.unique_integer([:positive])}" - ], - overrides: opts - ) - end -end - -# In your tests -test "concurrent user creation" do - users = MyApp.TestGenerators.generate_many(user(), 10) - # Each user has unique identity attributes -end -``` - -This applies to ANY field used in identity constraints, not just primary keys. Using globally unique values prevents frustrating intermittent test failures in CI environments. - +[ash:testing usage rules](deps/ash/usage-rules/testing.md) + +## tribes_plugin_api usage +_tribes_plugin_api_ + +# Tribes Plugin API Usage Rules + +These rules apply when implementing the public plugin contract from +`tribes_plugin_api`. For deeper context from a plugin checkout, see +[`../tribes/docs/plugins.md`](../tribes/docs/plugins.md); from this package +source directory, the same document is at `../docs/plugins.md`. + +## Entry Modules + +- Implement the runtime contract with `Tribes.Plugin` or `Tribes.Plugin.Base`. +- Prefer `use Tribes.Plugin.Base, otp_app: :your_plugin` for normal plugins; it + reads `manifest.json` and fills the manifest-backed spec fields. +- Keep the host-facing entry module under `Tribes.Plugins.*.Plugin`. It may be a + thin delegate to the plugin application's own module. +- `register/1` must return a `Tribes.Plugin.Spec` struct or a map/struct that + validates into that spec. + +## Spec Discipline + +- Fields mirrored from `manifest.json` must match exactly after capability + normalization: `name`, `version`, `provider_priority`, `provides`, + `requires`, and `enhances_with`. +- Use `%Tribes.Plugin.Spec.NavItem{}` and `%Tribes.Plugin.Spec.Page{}` or maps + with only the documented keys. Unknown keys fail validation. +- Use atom modules for `live_view`, plug modules, hook modules, `ui_components`, + and `ash_domains`; do not pass module names as strings in the runtime spec. +- Keep `children` as valid supervisor child specs and make plugin processes + restartable under normal OTP supervision rules. +- Run `scripts/plugin validate` or `mix tribes.plugin.validate` before relying + on runtime registration behavior. + +## Pages, Layouts, And Auth + +- Use `Tribes.Plugin.Layouts.app` for pages that should render inside host + chrome. +- Use `Tribes.Plugin.LiveUserAuth` on plugin LiveViews when they need host user + context. +- Avoid `use TribesWeb, :live_view` in standalone release-facing plugin code; + use `Phoenix.LiveView` and public plugin/UI APIs instead. + +## Config Schema + +- `config_schema` is for small admin-editable runtime defaults rendered by the + host UI and stored through `Tribes.ConfigStore`. +- Group IDs and setting keys must use the identifier format accepted by the + validator: lowercase segments with letters, digits, underscores, and dots. +- Supported setting types are `:string`, `:text`, `:boolean`, `:integer`, + `:number`, `:enum`, `:list`, and `:object`. +- Use `options` only with `:enum`, and provide stable stored values rather than + display text as the value. + + + +## tribes usage +_tribes_ + +# Tribes Plugin Development Rules + +These rules are for external Tribes plugin projects that depend on the host +checkout during development. For the full contract, see +[`../tribes/docs/plugins.md`](../tribes/docs/plugins.md) and +[`../tribes/docs/ui.md`](../tribes/docs/ui.md). + +## Plugin Boundaries + +- Treat plugins as separate OTP applications. The host discovers them through + `manifest.json` and a single `Tribes.Plugin` entry module, not by reaching + into plugin internals. +- Keep plugin contributions inside the supported runtime spec fields: + `nav_items`, `pages`, `api_routes`, `plugs`, `children`, `global_js`, + `global_css`, `migrations_path`, `ui_components`, `hooks`, `ash_domains`, and + `config_schema`. +- Do not mutate host routers, host Ash domains, endpoint config, or host + supervision trees directly from plugin code. +- Plugin-owned pages and API routes should live under plugin-owned paths. Avoid + taking over unrelated host sections. + +## Manifest And Runtime Spec + +- `manifest.json` is the static build/runtime contract. Keep `name`, + `version`, `entry_module`, `host_api`, `otp_app`, `provides`, `requires`, and + `enhances_with` aligned with the runtime spec returned by `register/1`. +- `entry_module` must be under `Tribes.Plugins.*.Plugin` and end in `.Plugin`. +- Capability versions are discrete breaking-change markers such as `ui@1` or + `ecto@1`, not semver. +- Use `requires` for hard dependencies and `enhances_with` for optional + integrations that the plugin can run without. +- Run `scripts/plugin validate` after changing `manifest.json` or the runtime + plugin spec. + +## UI And Assets + +- Plugin LiveViews that use host chrome should render with + `Tribes.Plugin.Layouts.app` and keep `ui@1` in `manifest.json` `requires`. +- Consumers should target the `ui@1` facade with `use Tribes.UI` or + `import Tribes.UI.Components`, not a concrete provider module. +- Declare browser assets in `manifest.json` under `assets.global_js` and + `assets.global_css`; the host serves them through the plugin asset surface. +- Keep CSS selectors scoped to the plugin, normally with a plugin-specific root + class. + +## Runtime Config + +- Use plugin OTP app env only for boot-time wiring that is genuinely static. +- For mutable runtime settings, use `Tribes.ConfigStore` and expose small + editable defaults through `config_schema` in the plugin spec. +- `config_schema` is validated by the host and rendered by Tribes in the admin + settings UI. It describes fields only; values are stored as ConfigStore + overrides when an admin saves them. +- If `config_schema.namespace` is omitted, the host uses + `plugin.`. +- Plugins should read ConfigStore values with the same defaults declared in the + schema, because saving a value equal to the default deletes the override. +- Do not expose secrets, node-local paths, ports, database URLs, TLS material, + package selection, or deployment state through `config_schema`. + +## Synced Data + +- If a plugin adds Ash resources that should replicate across the cluster, add + them deliberately to the plugin spec `ash_domains` and document replication + semantics in the plugin contract docs. +- Use `AshNostrSync` only for resources whose persisted state is meant to be + cluster-synced. Do not enable it as a default on every plugin resource. + + diff --git a/mix.exs b/mix.exs index e6fed79..6e9fbbb 100644 --- a/mix.exs +++ b/mix.exs @@ -55,7 +55,21 @@ defmodule MyPlugin.MixProject do defp usage_rules do [ file: "AGENTS.md", - usage_rules: [:elixir, :otp, :phoenix, ~r/^phoenix_/, :ash] + usage_rules: [ + {:usage_rules, sub_rules: []}, + {"usage_rules:elixir", main: false}, + {"usage_rules:otp", main: false}, + "phoenix:ecto", + "phoenix:html", + "phoenix:liveview", + "phoenix:phoenix", + {:ash, sub_rules: []}, + {"ash:actions", link: :markdown, main: false}, + {"ash:migrations", link: :markdown, main: false}, + {"ash:testing", link: :markdown, main: false}, + {:tribes_plugin_api, sub_rules: []}, + {:tribes, sub_rules: []} + ] ] end diff --git a/scripts/plugin b/scripts/plugin old mode 100644 new mode 100755