You've already forked tribes-plugin-aether
forked from tribes/tribes-plugin-template
3bc16c7cb5
Clarify that plugin workflows should use devenv when available and the repo-local Guix wrapper when devenv is unavailable.
670 lines
30 KiB
Markdown
670 lines
30 KiB
Markdown
# Tribes Aether Plugin Agent Guide
|
||
|
||
This repository is the Tribes Aether plugin. Keep changes scoped to the plugin
|
||
contract, migrations, assets, and tests owned by this repo.
|
||
|
||
## Required Workflow
|
||
|
||
- Use `plugin` for plugin-aware commands inside the repo development shell: `plugin validate`,
|
||
`plugin test`, `plugin precommit`, and `plugin smoke`. Outside the development
|
||
shell, use the local `scripts/plugin` wrapper.
|
||
- Prefer `devenv shell -- <command>` when running repo commands from outside the
|
||
development shell. If `devenv` is unavailable, use `./guix/guix-dev -- <command>`
|
||
rather than raw `guix shell` so pinned channels from `guix/channels.scm` are used.
|
||
- Raw `mix test` is not the normal entrypoint for this repo; `plugin test`
|
||
invokes `mix raw_test` with the host database/services and host
|
||
plugin-manager paths.
|
||
- Keep browser assets under the plugin asset pipeline and avoid host app
|
||
internals unless the plugin API requires them.
|
||
- For AshPostgres resource changes, generate migrations with
|
||
`plugin ash.codegen <name>`. Use `plugin ecto.migration <name>` only for rare
|
||
manual schema migrations that are not derived from Ash resources.
|
||
- Do not edit an existing migration after it may have run; add a new migration
|
||
instead.
|
||
|
||
## Plugin Contract
|
||
|
||
- `manifest.json` `entry_module` must point at `TribeOne.TribesPlugin.Aether.Plugin`.
|
||
- Keep runtime spec fields aligned with `manifest.json`.
|
||
- Treat host integrations as capability-based plugin contracts.
|
||
|
||
|
||
<!-- usage-rules-start -->
|
||
<!-- usage_rules-start -->
|
||
## 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-end -->
|
||
<!-- usage_rules:elixir-start -->
|
||
## usage_rules:elixir usage
|
||
# Elixir Core Usage Rules
|
||
|
||
## Pattern Matching
|
||
- Use pattern matching over conditional logic when possible
|
||
- Prefer to match on function heads instead of using `if`/`else` or `case` in function bodies
|
||
- `%{}` matches ANY map, not just empty maps. Use `map_size(map) == 0` guard to check for truly empty maps
|
||
|
||
## Error Handling
|
||
- Use `{:ok, result}` and `{:error, reason}` tuples for operations that can fail
|
||
- Avoid raising exceptions for control flow
|
||
- Use `with` for chaining operations that return `{:ok, _}` or `{:error, _}`
|
||
|
||
## Common Mistakes to Avoid
|
||
- Elixir has no `return` statement, nor early returns. The last expression in a block is always returned.
|
||
- Don't use `Enum` functions on large collections when `Stream` is more appropriate
|
||
- Avoid nested `case` statements - refactor to a single `case`, `with` or separate functions
|
||
- Don't use `String.to_atom/1` on user input (memory leak risk)
|
||
- Lists and enumerables cannot be indexed with brackets. Use pattern matching or `Enum` functions
|
||
- Prefer `Enum` functions like `Enum.reduce` over recursion
|
||
- When recursion is necessary, prefer to use pattern matching in function heads for base case detection
|
||
- Using the process dictionary is typically a sign of unidiomatic code
|
||
- Only use macros if explicitly requested
|
||
- There are many useful standard library functions, prefer to use them where possible
|
||
|
||
## Function Design
|
||
- Use guard clauses: `when is_binary(name) and byte_size(name) > 0`
|
||
- Prefer multiple function clauses over complex conditional logic
|
||
- Name functions descriptively: `calculate_total_price/2` not `calc/2`
|
||
- Predicate function names should not start with `is` and should end in a question mark.
|
||
- Names like `is_thing` should be reserved for guards
|
||
|
||
## Data Structures
|
||
- Use structs over maps when the shape is known: `defstruct [:name, :age]`
|
||
- Prefer keyword lists for options: `[timeout: 5000, retries: 3]`
|
||
- Use maps for dynamic key-value data
|
||
- Prefer to prepend to lists `[new | list]` not `list ++ [new]`
|
||
|
||
## Mix Tasks
|
||
|
||
- Use `mix help` to list available mix tasks
|
||
- Use `mix help task_name` to get docs for an individual task
|
||
- Read the docs and options fully before using tasks
|
||
|
||
## Testing
|
||
- Run tests in a specific file with `mix test test/my_test.exs` and a specific test with the line number `mix test path/to/test.exs:123`
|
||
- Limit the number of failed tests with `mix test --max-failures n`
|
||
- Use `@tag` to tag specific tests, and `mix test --only tag` to run only those tests
|
||
- Use `assert_raise` for testing expected exceptions: `assert_raise ArgumentError, fn -> invalid_function() end`
|
||
- Use `mix help test` to for full documentation on running tests
|
||
|
||
## Debugging
|
||
|
||
- Use `dbg/1` to print values while debugging. This will display the formatted value and other relevant information in the console.
|
||
|
||
<!-- usage_rules:elixir-end -->
|
||
<!-- usage_rules:otp-start -->
|
||
## usage_rules:otp usage
|
||
# OTP Usage Rules
|
||
|
||
## GenServer Best Practices
|
||
- Keep state simple and serializable
|
||
- Handle all expected messages explicitly
|
||
- Use `handle_continue/2` for post-init work
|
||
- Implement proper cleanup in `terminate/2` when necessary
|
||
|
||
## Process Communication
|
||
- Use `GenServer.call/3` for synchronous requests expecting replies
|
||
- Use `GenServer.cast/2` for fire-and-forget messages.
|
||
- When in doubt, use `call` over `cast`, to ensure back-pressure
|
||
- Set appropriate timeouts for `call/3` operations
|
||
|
||
## Fault Tolerance
|
||
- Set up processes such that they can handle crashing and being restarted by supervisors
|
||
- Use `:max_restarts` and `:max_seconds` to prevent restart loops
|
||
|
||
## Task and Async
|
||
- Use `Task.Supervisor` for better fault tolerance
|
||
- Handle task failures with `Task.yield/2` or `Task.shutdown/2`
|
||
- Set appropriate task timeouts
|
||
- Use `Task.async_stream/3` for concurrent enumeration with back-pressure
|
||
|
||
<!-- usage_rules:otp-end -->
|
||
<!-- phoenix:ecto-start -->
|
||
## phoenix:ecto usage
|
||
## Ecto Guidelines
|
||
|
||
- **Always** preload Ecto associations in queries when they'll be accessed in templates, ie a message that needs to reference the `message.user.email`
|
||
- Remember `import Ecto.Query` and other supporting modules when you write `seeds.exs`
|
||
- `Ecto.Schema` fields always use the `:string` type, even for `:text`, columns, ie: `field :name, :string`
|
||
- `Ecto.Changeset.validate_number/2` **DOES NOT SUPPORT the `:allow_nil` option**. By default, Ecto validations only run if a change for the given field exists and the change value is not nil, so such as option is never needed
|
||
- You **must** use `Ecto.Changeset.get_field(changeset, :field)` to access changeset fields
|
||
- Fields which are set programmatically, such as `user_id`, must not be listed in `cast` calls or similar for security purposes. Instead they must be explicitly set when creating the struct
|
||
- **Always** invoke `mix ecto.gen.migration migration_name_using_underscores` when generating migration files, so the correct timestamp and conventions are applied
|
||
|
||
<!-- phoenix:ecto-end -->
|
||
<!-- phoenix:html-start -->
|
||
## phoenix:html usage
|
||
## Phoenix HTML guidelines
|
||
|
||
- Phoenix templates **always** use `~H` or .html.heex files (known as HEEx), **never** use `~E`
|
||
- **Always** use the imported `Phoenix.Component.form/1` and `Phoenix.Component.inputs_for/1` function to build forms. **Never** use `Phoenix.HTML.form_for` or `Phoenix.HTML.inputs_for` as they are outdated
|
||
- When building forms **always** use the already imported `Phoenix.Component.to_form/2` (`assign(socket, form: to_form(...))` and `<.form for={@form} id="msg-form">`), then access those forms in the template via `@form[:field]`
|
||
- **Always** add unique DOM IDs to key elements (like forms, buttons, etc) when writing templates, these IDs can later be used in tests (`<.form for={@form} id="product-form">`)
|
||
- For "app wide" template imports, you can import/alias into the `my_app_web.ex`'s `html_helpers` block, so they will be available to all LiveViews, LiveComponent's, and all modules that do `use MyAppWeb, :html` (replace "my_app" by the actual app name)
|
||
|
||
- Elixir supports `if/else` but **does NOT support `if/else if` or `if/elsif`**. **Never use `else if` or `elseif` in Elixir**, **always** use `cond` or `case` for multiple conditionals.
|
||
|
||
**Never do this (invalid)**:
|
||
|
||
<%= if condition do %>
|
||
...
|
||
<% else if other_condition %>
|
||
...
|
||
<% end %>
|
||
|
||
Instead **always** do this:
|
||
|
||
<%= cond do %>
|
||
<% condition -> %>
|
||
...
|
||
<% condition2 -> %>
|
||
...
|
||
<% true -> %>
|
||
...
|
||
<% end %>
|
||
|
||
- HEEx require special tag annotation if you want to insert literal curly's like `{` or `}`. If you want to show a textual code snippet on the page in a `<pre>` or `<code>` block you *must* annotate the parent tag with `phx-no-curly-interpolation`:
|
||
|
||
<code phx-no-curly-interpolation>
|
||
let obj = {key: "val"}
|
||
</code>
|
||
|
||
Within `phx-no-curly-interpolation` annotated tags, you can use `{` and `}` without escaping them, and dynamic Elixir expressions can still be used with `<%= ... %>` syntax
|
||
|
||
- HEEx class attrs support lists, but you must **always** use list `[...]` syntax. You can use the class list syntax to conditionally add classes, **always do this for multiple class values**:
|
||
|
||
<a class={[
|
||
"px-2 text-white",
|
||
@some_flag && "py-5",
|
||
if(@other_condition, do: "border-red-500", else: "border-blue-100"),
|
||
...
|
||
]}>Text</a>
|
||
|
||
and **always** wrap `if`'s inside `{...}` expressions with parens, like done above (`if(@other_condition, do: "...", else: "...")`)
|
||
|
||
and **never** do this, since it's invalid (note the missing `[` and `]`):
|
||
|
||
<a class={
|
||
"px-2 text-white",
|
||
@some_flag && "py-5"
|
||
}> ...
|
||
=> Raises compile syntax error on invalid HEEx attr syntax
|
||
|
||
- **Never** use `<% Enum.each %>` or non-for comprehensions for generating template content, instead **always** use `<%= for item <- @collection do %>`
|
||
- HEEx HTML comments use `<%!-- comment --%>`. **Always** use the HEEx HTML comment syntax for template comments (`<%!-- comment --%>`)
|
||
- HEEx allows interpolation via `{...}` and `<%= ... %>`, but the `<%= %>` **only** works within tag bodies. **Always** use the `{...}` syntax for interpolation within tag attributes, and for interpolation of values within tag bodies. **Always** interpolate block constructs (if, cond, case, for) within tag bodies using `<%= ... %>`.
|
||
|
||
**Always** do this:
|
||
|
||
<div id={@id}>
|
||
{@my_assign}
|
||
<%= if @some_block_condition do %>
|
||
{@another_assign}
|
||
<% end %>
|
||
</div>
|
||
|
||
and **Never** do this – the program will terminate with a syntax error:
|
||
|
||
<%!-- THIS IS INVALID NEVER EVER DO THIS --%>
|
||
<div id="<%= @invalid_interpolation %>">
|
||
{if @invalid_block_construct do}
|
||
{end}
|
||
</div>
|
||
|
||
<!-- phoenix:html-end -->
|
||
<!-- phoenix:liveview-start -->
|
||
## phoenix:liveview usage
|
||
## Phoenix LiveView guidelines
|
||
|
||
- **Never** use the deprecated `live_redirect` and `live_patch` functions, instead **always** use the `<.link navigate={href}>` and `<.link patch={href}>` in templates, and `push_navigate` and `push_patch` functions LiveViews
|
||
- **Avoid LiveComponent's** unless you have a strong, specific need for them
|
||
- LiveViews should be named like `AppWeb.WeatherLive`, with a `Live` suffix. When you go to add LiveView routes to the router, the default `:browser` scope is **already aliased** with the `AppWeb` module, so you can just do `live "/weather", WeatherLive`
|
||
|
||
### LiveView streams
|
||
|
||
- **Always** use LiveView streams for collections for assigning regular lists to avoid memory ballooning and runtime termination with the following operations:
|
||
- basic append of N items - `stream(socket, :messages, [new_msg])`
|
||
- resetting stream with new items - `stream(socket, :messages, [new_msg], reset: true)` (e.g. for filtering items)
|
||
- prepend to stream - `stream(socket, :messages, [new_msg], at: -1)`
|
||
- deleting items - `stream_delete(socket, :messages, msg)`
|
||
|
||
- When using the `stream/3` interfaces in the LiveView, the LiveView template must 1) always set `phx-update="stream"` on the parent element, with a DOM id on the parent element like `id="messages"` and 2) consume the `@streams.stream_name` collection and use the id as the DOM id for each child. For a call like `stream(socket, :messages, [new_msg])` in the LiveView, the template would be:
|
||
|
||
<div id="messages" phx-update="stream">
|
||
<div :for={{id, msg} <- @streams.messages} id={id}>
|
||
{msg.text}
|
||
</div>
|
||
</div>
|
||
|
||
- LiveView streams are *not* enumerable, so you cannot use `Enum.filter/2` or `Enum.reject/2` on them. Instead, if you want to filter, prune, or refresh a list of items on the UI, you **must refetch the data and re-stream the entire stream collection, passing reset: true**:
|
||
|
||
def handle_event("filter", %{"filter" => filter}, socket) do
|
||
# re-fetch the messages based on the filter
|
||
messages = list_messages(filter)
|
||
|
||
{:noreply,
|
||
socket
|
||
|> assign(:messages_empty?, messages == [])
|
||
# reset the stream with the new messages
|
||
|> stream(:messages, messages, reset: true)}
|
||
end
|
||
|
||
- LiveView streams *do not support counting or empty states*. If you need to display a count, you must track it using a separate assign. For empty states, you can use Tailwind classes:
|
||
|
||
<div id="tasks" phx-update="stream">
|
||
<div class="hidden only:block">No tasks yet</div>
|
||
<div :for={{id, task} <- @streams.tasks} id={id}>
|
||
{task.name}
|
||
</div>
|
||
</div>
|
||
|
||
The above only works if the empty state is the only HTML block alongside the stream for-comprehension.
|
||
|
||
- When updating an assign that should change content inside any streamed item(s), you MUST re-stream the items
|
||
along with the updated assign:
|
||
|
||
def handle_event("edit_message", %{"message_id" => message_id}, socket) do
|
||
message = Chat.get_message!(message_id)
|
||
edit_form = to_form(Chat.change_message(message, %{content: message.content}))
|
||
|
||
# re-insert message so @editing_message_id toggle logic takes effect for that stream item
|
||
{:noreply,
|
||
socket
|
||
|> stream_insert(:messages, message)
|
||
|> assign(:editing_message_id, String.to_integer(message_id))
|
||
|> assign(:edit_form, edit_form)}
|
||
end
|
||
|
||
And in the template:
|
||
|
||
<div id="messages" phx-update="stream">
|
||
<div :for={{id, message} <- @streams.messages} id={id} class="flex group">
|
||
{message.username}
|
||
<%= if @editing_message_id == message.id do %>
|
||
<%!-- Edit mode --%>
|
||
<.form for={@edit_form} id="edit-form-#{message.id}" phx-submit="save_edit">
|
||
...
|
||
</.form>
|
||
<% end %>
|
||
</div>
|
||
</div>
|
||
|
||
- **Never** use the deprecated `phx-update="append"` or `phx-update="prepend"` for collections
|
||
|
||
### LiveView JavaScript interop
|
||
|
||
- Remember anytime you use `phx-hook="MyHook"` and that JS hook manages its own DOM, you **must** also set the `phx-update="ignore"` attribute
|
||
- **Always** provide an unique DOM id alongside `phx-hook` otherwise a compiler error will be raised
|
||
|
||
LiveView hooks come in two flavors, 1) colocated js hooks for "inline" scripts defined inside HEEx,
|
||
and 2) external `phx-hook` annotations where JavaScript object literals are defined and passed to the `LiveSocket` constructor.
|
||
|
||
#### Inline colocated js hooks
|
||
|
||
**Never** write raw embedded `<script>` tags in heex as they are incompatible with LiveView.
|
||
Instead, **always use a colocated js hook script tag (`:type={Phoenix.LiveView.ColocatedHook}`)
|
||
when writing scripts inside the template**:
|
||
|
||
<input type="text" name="user[phone_number]" id="user-phone-number" phx-hook=".PhoneNumber" />
|
||
<script :type={Phoenix.LiveView.ColocatedHook} name=".PhoneNumber">
|
||
export default {
|
||
mounted() {
|
||
this.el.addEventListener("input", e => {
|
||
let match = this.el.value.replace(/\D/g, "").match(/^(\d{3})(\d{3})(\d{4})$/)
|
||
if(match) {
|
||
this.el.value = `${match[1]}-${match[2]}-${match[3]}`
|
||
}
|
||
})
|
||
}
|
||
}
|
||
</script>
|
||
|
||
- colocated hooks are automatically integrated into the app.js bundle that imports the generated `phoenix-colocated/<otp_app>` module
|
||
- external Tribes plugin OTP apps are not auto-imported by the host `app.js` bundle; use `assets.global_js` plus `window.TribesPluginHooks` for plugin hooks
|
||
- colocated hooks names **MUST ALWAYS** start with a `.` prefix, i.e. `.PhoneNumber`
|
||
|
||
#### External phx-hook
|
||
|
||
External JS hooks (`<div id="myhook" phx-hook="MyHook">`) in Tribes plugins must be registered by a plugin JS bundle listed in `manifest.json` `assets.global_js`; the host merges `window.TribesPluginHooks` into the LiveSocket constructor:
|
||
|
||
window.TribesPluginHooks = window.TribesPluginHooks || {};
|
||
window.TribesPluginHooks.MyHook = {
|
||
mounted() { ... }
|
||
};
|
||
|
||
#### Pushing events between client and server
|
||
|
||
Use LiveView's `push_event/3` when you need to push events/data to the client for a phx-hook to handle.
|
||
**Always** return or rebind the socket on `push_event/3` when pushing events:
|
||
|
||
# re-bind socket so we maintain event state to be pushed
|
||
socket = push_event(socket, "my_event", %{...})
|
||
|
||
# or return the modified socket directly:
|
||
def handle_event("some_event", _, socket) do
|
||
{:noreply, push_event(socket, "my_event", %{...})}
|
||
end
|
||
|
||
Pushed events can then be picked up in a JS hook with `this.handleEvent`:
|
||
|
||
mounted() {
|
||
this.handleEvent("my_event", data => console.log("from server:", data));
|
||
}
|
||
|
||
Clients can also push an event to the server and receive a reply with `this.pushEvent`:
|
||
|
||
mounted() {
|
||
this.el.addEventListener("click", e => {
|
||
this.pushEvent("my_event", { one: 1 }, reply => console.log("got reply from server:", reply));
|
||
})
|
||
}
|
||
|
||
Where the server handled it via:
|
||
|
||
def handle_event("my_event", %{"one" => 1}, socket) do
|
||
{:reply, %{two: 2}, socket}
|
||
end
|
||
|
||
### LiveView tests
|
||
|
||
- `Phoenix.LiveViewTest` module and `LazyHTML` (included) for making your assertions
|
||
- Form tests are driven by `Phoenix.LiveViewTest`'s `render_submit/2` and `render_change/2` functions
|
||
- Come up with a step-by-step test plan that splits major test cases into small, isolated files. You may start with simpler tests that verify content exists, gradually add interaction tests
|
||
- **Always reference the key element IDs you added in the LiveView templates in your tests** for `Phoenix.LiveViewTest` functions like `element/2`, `has_element/2`, selectors, etc
|
||
- **Never** tests again raw HTML, **always** use `element/2`, `has_element/2`, and similar: `assert has_element?(view, "#my-form")`
|
||
- Instead of relying on testing text content, which can change, favor testing for the presence of key elements
|
||
- Focus on testing outcomes rather than implementation details
|
||
- Be aware that `Phoenix.Component` functions like `<.form>` might produce different HTML than expected. Test against the output HTML structure, not your mental model of what you expect it to be
|
||
- When facing test failures with element selectors, add debug statements to print the actual HTML, but use `LazyHTML` selectors to limit the output, ie:
|
||
|
||
html = render(view)
|
||
document = LazyHTML.from_fragment(html)
|
||
matches = LazyHTML.filter(document, "your-complex-selector")
|
||
IO.inspect(matches, label: "Matches")
|
||
|
||
### Form handling
|
||
|
||
#### Creating a form from params
|
||
|
||
If you want to create a form based on `handle_event` params:
|
||
|
||
def handle_event("submitted", params, socket) do
|
||
{:noreply, assign(socket, form: to_form(params))}
|
||
end
|
||
|
||
When you pass a map to `to_form/1`, it assumes said map contains the form params, which are expected to have string keys.
|
||
|
||
You can also specify a name to nest the params:
|
||
|
||
def handle_event("submitted", %{"user" => user_params}, socket) do
|
||
{:noreply, assign(socket, form: to_form(user_params, as: :user))}
|
||
end
|
||
|
||
#### Creating a form from changesets
|
||
|
||
When using changesets, the underlying data, form params, and errors are retrieved from it. The `:as` option is automatically computed too. E.g. if you have a user schema:
|
||
|
||
defmodule MyApp.Users.User do
|
||
use Ecto.Schema
|
||
...
|
||
end
|
||
|
||
And then you create a changeset that you pass to `to_form`:
|
||
|
||
%MyApp.Users.User{}
|
||
|> Ecto.Changeset.change()
|
||
|> to_form()
|
||
|
||
Once the form is submitted, the params will be available under `%{"user" => user_params}`.
|
||
|
||
In the template, the form form assign can be passed to the `<.form>` function component:
|
||
|
||
<.form for={@form} id="todo-form" phx-change="validate" phx-submit="save">
|
||
<.input field={@form[:field]} type="text" />
|
||
</.form>
|
||
|
||
Always give the form an explicit, unique DOM ID, like `id="todo-form"`.
|
||
|
||
#### Avoiding form errors
|
||
|
||
**Always** use a form assigned via `to_form/2` in the LiveView, and the `<.input>` component in the template. In the template **always access forms this**:
|
||
|
||
<%!-- ALWAYS do this (valid) --%>
|
||
<.form for={@form} id="my-form">
|
||
<.input field={@form[:field]} type="text" />
|
||
</.form>
|
||
|
||
And **never** do this:
|
||
|
||
<%!-- NEVER do this (invalid) --%>
|
||
<.form for={@changeset} id="my-form">
|
||
<.input field={@changeset[:field]} type="text" />
|
||
</.form>
|
||
|
||
- You are FORBIDDEN from accessing the changeset in the template as it will cause errors
|
||
- **Never** use `<.form let={f} ...>` in the template, instead **always use `<.form for={@form} ...>`**, then drive all form references from the form assign as in `@form[:field]`. The UI should **always** be driven by a `to_form/2` assigned in the LiveView module that is derived from a changeset
|
||
|
||
<!-- phoenix:liveview-end -->
|
||
<!-- phoenix:phoenix-start -->
|
||
## phoenix:phoenix usage
|
||
## Phoenix guidelines
|
||
|
||
- Remember Phoenix router `scope` blocks include an optional alias which is prefixed for all routes within the scope. **Always** be mindful of this when creating routes within a scope to avoid duplicate module prefixes.
|
||
|
||
- You **never** need to create your own `alias` for route definitions! The `scope` provides the alias, ie:
|
||
|
||
scope "/admin", AppWeb.Admin do
|
||
pipe_through :browser
|
||
|
||
live "/users", UserLive, :index
|
||
end
|
||
|
||
the UserLive route would point to the `AppWeb.Admin.UserLive` module
|
||
|
||
- `Phoenix.View` no longer is needed or included with Phoenix, don't use it
|
||
|
||
<!-- phoenix:phoenix-end -->
|
||
<!-- ash-start -->
|
||
## ash usage
|
||
_A declarative, extensible framework for building Elixir applications._
|
||
|
||
# Rules for working with Ash
|
||
|
||
## Understanding Ash
|
||
|
||
Ash is an opinionated, composable framework for building applications in Elixir. It provides a declarative approach to modeling your domain with resources at the center. Read documentation *before* attempting to use its features. Do not assume that you have prior knowledge of the framework or its conventions.
|
||
|
||
|
||
<!-- ash-end -->
|
||
<!-- ash:actions-start -->
|
||
## ash:actions usage
|
||
[ash:actions usage rules](deps/ash/usage-rules/actions.md)
|
||
<!-- ash:actions-end -->
|
||
<!-- ash:migrations-start -->
|
||
## ash:migrations usage
|
||
[ash:migrations usage rules](deps/ash/usage-rules/migrations.md)
|
||
<!-- ash:migrations-end -->
|
||
<!-- ash:testing-start -->
|
||
## ash:testing usage
|
||
[ash:testing usage rules](deps/ash/usage-rules/testing.md)
|
||
<!-- ash:testing-end -->
|
||
<!-- tribes_plugin_api-start -->
|
||
## 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 plugin modules under an owner-controlled namespace, such as
|
||
`TribeOne.TribesPlugin.<Plugin>.Plugin` for first-party plugins or
|
||
`AcmeCorp.TribesPlugins.Foo.Plugin` for third-party plugins. The entry module
|
||
must end in `.Plugin` and should be the module named by `manifest.json`.
|
||
- `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_plugin_api-end -->
|
||
<!-- tribes-start -->
|
||
## 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 a valid module ending in `.Plugin` under an owner-controlled namespace.
|
||
- Capability versions are discrete breaking-change markers such as `org.tribe-one.caps.ui@1` or
|
||
`org.tribe-one.caps.social@1`, not semver. Framework APIs are part of the `host_api` foundation, not separate manifest capabilities.
|
||
- 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 `org.tribe-one.caps.ui@1` in `manifest.json` `requires`.
|
||
- Consumers should target the `org.tribe-one.caps.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.
|
||
- Register plugin LiveView hooks from `assets.global_js` bundles via
|
||
`window.TribesPluginHooks`. Phoenix colocated hooks are not auto-imported from
|
||
external plugin OTP apps by the host `app.js` bundle.
|
||
- 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.<plugin_name>`.
|
||
- 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.
|
||
|
||
<!-- tribes-end -->
|
||
<!-- usage-rules-end -->
|