From b4b8c83ddb2ea49bb3a770f27f956bbfbfd30d88 Mon Sep 17 00:00:00 2001 From: Steffen Beyer Date: Wed, 27 May 2026 19:05:51 +0200 Subject: [PATCH] feat: namespace plugin identity Adopt canonical plugin id/slug manifest fields, vendor-prefixed OTP app naming, and fully-qualified capability ids for Aether. --- AGENTS.md | 8 +++---- README.md | 18 ++++++++------- config/config.exs | 2 +- lib/aether/chat.ex | 2 +- lib/aether/chat/channel.ex | 2 +- lib/aether/chat/message.ex | 2 +- lib/aether/chat/participant.ex | 2 +- lib/aether/plugin.ex | 4 ++-- .../live/chat_recipient_picker_component.ex | 2 +- manifest.json | 23 ++++++++++++++----- mix.exs | 2 +- test/aether/manifest_test.exs | 18 +++++++++------ 12 files changed, 51 insertions(+), 34 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 56554c7..7788c11 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -620,8 +620,8 @@ checkout during development. For the full contract, see `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 `ui@1` or - `social@1`, not semver. Framework APIs are part of the `host_api` foundation, not separate manifest capabilities. +- 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 @@ -630,8 +630,8 @@ checkout during development. For the full contract, see ## 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 + `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. diff --git a/README.md b/README.md index d2813f6..9713ac3 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ Tribes node. It provides: -- `social@1` — a local tribe feed backed by Nostr kind `1` notes. -- `chat@1` — reusable chat surfaces for public rooms, embeddable context chat, +- `org.tribe-one.caps.social@1` — a local tribe feed backed by Nostr kind `1` notes. +- `org.tribe-one.caps.chat@1` — reusable chat surfaces for public rooms, embeddable context chat, NIP-17 direct messages, and future chat backends. ## Features @@ -52,13 +52,15 @@ The package pin is kept at `@internet-privacy/marmot-ts@0.5.1`. ```json { - "name": "aether", + "id": "org.tribe-one.plugins.aether", + "slug": "aether", + "display_name": "Aether", "version": "0.2.0", "entry_module": "TribeOne.TribesPlugin.Aether.Plugin", "host_api": "1", - "otp_app": "aether", - "provides": ["social@1", "chat@1"], - "requires": ["ui@1"], + "otp_app": "tribe_one_aether", + "provides": ["org.tribe-one.caps.social@1", "org.tribe-one.caps.chat@1"], + "requires": ["org.tribe-one.caps.ui@1"], "assets": { "global_js": ["aether.js"], "global_css": ["aether.css"] @@ -75,7 +77,7 @@ Runtime contributions from `TribeOne.TribesPlugin.Aether.Plugin` include: - plugin config schema for chat backend defaults and Marmot UI enablement, - global JS/CSS assets. -The `chat@1` provider surface is intentionally small and reusable: +The `org.tribe-one.caps.chat@1` provider surface is intentionally small and reusable: - `TribeOne.TribesPlugin.Aether.Chat.chat_panel_component/0` returns the reusable chat panel component. - `TribeOne.TribesPlugin.Aether.Chat.recipient_picker_component/0` returns the recipient picker. @@ -143,7 +145,7 @@ colocated hooks from external plugin OTP apps are not auto-imported by the host. ## Runtime requirements -- A Tribes host checkout/runtime with `ui@1`. +- A Tribes host checkout/runtime with `org.tribe-one.caps.ui@1`. - Parrhesia available through the host for Nostr event storage and streaming. - `nak` available in the runtime path for the current NIP-17/NIP-59/NIP-04 protocol implementation. diff --git a/config/config.exs b/config/config.exs index aa3e46c..d514d07 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,5 +1,5 @@ import Config -config :aether, ash_domains: [TribeOne.TribesPlugin.Aether.Chat] +config :tribe_one_aether, ash_domains: [TribeOne.TribesPlugin.Aether.Chat] import_config "#{config_env()}.exs" diff --git a/lib/aether/chat.ex b/lib/aether/chat.ex index 7f058da..66a391a 100644 --- a/lib/aether/chat.ex +++ b/lib/aether/chat.ex @@ -4,7 +4,7 @@ defmodule TribeOne.TribesPlugin.Aether.Chat do """ use Ash.Domain, - otp_app: :aether + otp_app: :tribe_one_aether require Ash.Query diff --git a/lib/aether/chat/channel.ex b/lib/aether/chat/channel.ex index 1aebf9d..690f662 100644 --- a/lib/aether/chat/channel.ex +++ b/lib/aether/chat/channel.ex @@ -4,7 +4,7 @@ defmodule TribeOne.TribesPlugin.Aether.Chat.Channel do """ use Ash.Resource, - otp_app: :aether, + otp_app: :tribe_one_aether, domain: TribeOne.TribesPlugin.Aether.Chat, data_layer: AshPostgres.DataLayer, extensions: [AshNostrSync] diff --git a/lib/aether/chat/message.ex b/lib/aether/chat/message.ex index 789df65..74c20a3 100644 --- a/lib/aether/chat/message.ex +++ b/lib/aether/chat/message.ex @@ -4,7 +4,7 @@ defmodule TribeOne.TribesPlugin.Aether.Chat.Message do """ use Ash.Resource, - otp_app: :aether, + otp_app: :tribe_one_aether, domain: TribeOne.TribesPlugin.Aether.Chat, data_layer: AshPostgres.DataLayer, extensions: [AshNostrSync] diff --git a/lib/aether/chat/participant.ex b/lib/aether/chat/participant.ex index ff22bf7..0388591 100644 --- a/lib/aether/chat/participant.ex +++ b/lib/aether/chat/participant.ex @@ -4,7 +4,7 @@ defmodule TribeOne.TribesPlugin.Aether.Chat.Participant do """ use Ash.Resource, - otp_app: :aether, + otp_app: :tribe_one_aether, domain: TribeOne.TribesPlugin.Aether.Chat, data_layer: AshPostgres.DataLayer, extensions: [AshNostrSync] diff --git a/lib/aether/plugin.ex b/lib/aether/plugin.ex index 3e60b96..e91ac0d 100644 --- a/lib/aether/plugin.ex +++ b/lib/aether/plugin.ex @@ -3,7 +3,7 @@ defmodule TribeOne.TribesPlugin.Aether.Plugin do Tribes plugin entry point. """ - use Tribes.Plugin.Base, otp_app: :aether + use Tribes.Plugin.Base, otp_app: :tribe_one_aether @behaviour Tribes.Capabilities.Chat.V1 @@ -94,7 +94,7 @@ defmodule TribeOne.TribesPlugin.Aether.Plugin do TribeOne.TribesPlugin.AetherWeb.ChatPanelComponent, Keyword.get(opts, :id, "aether-chat-panel-#{slug}"), assigns, - capability: "chat@1", + capability: "org.tribe-one.caps.chat@1", provider: __MODULE__ )} end diff --git a/lib/aether_web/live/chat_recipient_picker_component.ex b/lib/aether_web/live/chat_recipient_picker_component.ex index 0cd1400..78650e8 100644 --- a/lib/aether_web/live/chat_recipient_picker_component.ex +++ b/lib/aether_web/live/chat_recipient_picker_component.ex @@ -1,6 +1,6 @@ defmodule TribeOne.TribesPlugin.AetherWeb.ChatRecipientPickerComponent do @moduledoc """ - Recipient picker exposed as part of Aether's `chat@1` UI contract. + Recipient picker exposed as part of Aether's `org.tribe-one.caps.chat@1` UI contract. """ use Phoenix.LiveComponent diff --git a/manifest.json b/manifest.json index 13b0b1f..6c19320 100644 --- a/manifest.json +++ b/manifest.json @@ -1,16 +1,27 @@ { - "name": "aether", + "id": "org.tribe-one.plugins.aether", + "slug": "aether", + "display_name": "Aether", "version": "0.2.0", "description": "Local social feed and chat for Tribes, including NIP-17 direct messages.", "entry_module": "TribeOne.TribesPlugin.Aether.Plugin", "host_api": "1", - "otp_app": "aether", - "provides": ["social@1", "chat@1"], - "requires": ["ui@1"], + "otp_app": "tribe_one_aether", + "provides": [ + "org.tribe-one.caps.social@1", + "org.tribe-one.caps.chat@1" + ], + "requires": [ + "org.tribe-one.caps.ui@1" + ], "enhances_with": [], "assets": { - "global_js": ["aether.js"], - "global_css": ["aether.css"] + "global_js": [ + "aether.js" + ], + "global_css": [ + "aether.css" + ] }, "migrations": true, "children": false diff --git a/mix.exs b/mix.exs index 5bec141..1950161 100644 --- a/mix.exs +++ b/mix.exs @@ -3,7 +3,7 @@ defmodule TribeOne.TribesPlugin.Aether.MixProject do def project do [ - app: :aether, + app: :tribe_one_aether, version: "0.2.0", elixir: "~> 1.18", elixirc_paths: elixirc_paths(Mix.env()), diff --git a/test/aether/manifest_test.exs b/test/aether/manifest_test.exs index 9abedd7..e998c6a 100644 --- a/test/aether/manifest_test.exs +++ b/test/aether/manifest_test.exs @@ -2,6 +2,7 @@ defmodule TribeOne.TribesPlugin.Aether.ManifestTest do use ExUnit.Case, async: true @manifest_path Path.join(__DIR__, "../../manifest.json") |> Path.expand() + @capability_regex ~r/^[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_-]*)+@[1-9][0-9]*$/ setup_all do content = File.read!(@manifest_path) @@ -16,7 +17,9 @@ defmodule TribeOne.TribesPlugin.Aether.ManifestTest do test "has required fields", %{manifest: manifest} do required = [ - "name", + "id", + "slug", + "display_name", "version", "entry_module", "host_api", @@ -31,9 +34,10 @@ defmodule TribeOne.TribesPlugin.Aether.ManifestTest do end end - test "name matches OTP app name", %{manifest: manifest} do - assert manifest["name"] == "aether" - assert manifest["otp_app"] == manifest["name"] + test "identity fields are namespaced", %{manifest: manifest} do + assert manifest["id"] == "org.tribe-one.plugins.aether" + assert manifest["slug"] == "aether" + assert manifest["otp_app"] == "tribe_one_aether" end test "entry_module uses the TribeOne namespace and Plugin suffix", %{manifest: manifest} do @@ -48,20 +52,20 @@ defmodule TribeOne.TribesPlugin.Aether.ManifestTest do test "provides contains valid capability identifiers", %{manifest: manifest} do for cap <- manifest["provides"] do - assert Regex.match?(~r/^[a-z][a-z0-9_]*(@\d+)?$/, cap), + assert Regex.match?(@capability_regex, cap), "invalid capability identifier: #{inspect(cap)}" end end test "requires contains valid capability identifiers", %{manifest: manifest} do for cap <- manifest["requires"] do - assert Regex.match?(~r/^[a-z][a-z0-9_]*(@\d+)?$/, cap), + assert Regex.match?(@capability_regex, cap), "invalid capability identifier: #{inspect(cap)}" end end test "declares the chat capability", %{manifest: manifest} do - assert "chat@1" in manifest["provides"] + assert "org.tribe-one.caps.chat@1" in manifest["provides"] end test "host_api is a string version", %{manifest: manifest} do