From d788d12785a32eb54d654b0fa24459b8744b464c Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Wed, 28 Oct 2026 17:55:35 +0100 Subject: [PATCH] docs(phase-20): add pattern map --- .../20-PATTERNS.md | 288 ++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 .planning/phases/20-demo-instance-with-every-shared-plugin/20-PATTERNS.md diff --git a/.planning/phases/20-demo-instance-with-every-shared-plugin/20-PATTERNS.md b/.planning/phases/20-demo-instance-with-every-shared-plugin/20-PATTERNS.md new file mode 100644 index 00000000..2a2cfe8c --- /dev/null +++ b/.planning/phases/20-demo-instance-with-every-shared-plugin/20-PATTERNS.md @@ -0,0 +1,288 @@ +# Phase 20: Demo instance with every shared plugin - Pattern Map + +**Mapped:** 2026-10-28 +**Files analyzed:** 34 (grouped by repo) +**Analogs found:** 31 / 34 + +Paths are relative to the summercms.go repo root. The analogs are tracked source in their own repos. bm's `plugins/golem15/*` trees are nested submodules of sm-bm-app, and `plugins/jz/bm` is its own repo. Edit the shared plugins only in their `../sm-*-plugin` checkouts, after fast-forwarding them (RESEARCH Pitfall 1). Read the bm copies for reference only. + +## File Classification + +| New/Modified File | Role | Data Flow | Closest Analog | Match | +|---|---|---|---|---| +| `../sm-chat-plugin/events/events.go` (+`MessageSaving`) | event type | event-driven (pre-save) | `sm-user-plugin/classes/events.go` (DeactivatingEvent, lines 60-90) | role-match | +| `../sm-chat-plugin/contracts/rejection.go` (new) | contract/error | request-response | none (new type); errors pattern in `chat/api/errors.go:35-45` | partial | +| `../sm-chat-plugin/classes/service.go` (fire seam in `SendMessageWith`, `EditMessage`) | service | CRUD | itself, lines 287-345 | exact | +| `../sm-chat-plugin/api/errors.go` (map Rejection to 422) | api helper | request-response | itself, `writeValidation` lines 39-44 | exact | +| `../sm-forum-plugin/events/events.go` (new pkg, `PostSaving`) | event type | event-driven | chat `events/events.go` | role-match | +| `../sm-forum-plugin/classes/forum_service.go` (fire in CreateThread/Reply/EditPost) | service | CRUD | itself, lines 263-300 (`Bus` field line 57-58, unused) | exact | +| `../sm-forum-plugin/api/errors.go` | api helper | request-response | itself, `writeValidation` line 28, `writeForumError` line 44 | exact | +| `../sm-chatvideo-plugin/go.mod`, `../sm-herald-plugin/go.mod`, `../sm-userfriends-plugin/go.mod` | config | n/a | `sm-bm-app/plugins/golem15/chatbots/go.mod` (commit d0b9fb1 "drop sibling replaces") | exact | +| `../sm-moderation-plugin/plugin.go` | plugin entry | boot | `../sm-sitemap-plugin/plugin.go` lines 1-80 | role-match | +| `../sm-moderation-plugin/admin.go` (HasSettings) | admin | CRUD (settings singleton) | `../sm-sitemap-plugin/admin.go` lines 40-72 | exact | +| `../sm-moderation-plugin/models/settings.go` + `models/settings/fields.yaml` | model | CRUD | `../sm-sitemap-plugin/models/settings.go`, `models/settings/fields.yaml` | exact | +| `../sm-moderation-plugin/classes/filter.go` | service | transform | none | no analog | +| `../sm-moderation-plugin/updates/`, `lang/`, `config/`, `README.md` | migration/lang/config | n/a | `../sm-sitemap-plugin/{updates,lang,config,README.md}` | exact | +| `../sm-demo-app/plugins/demo/chat.go` | host wiring | boot/event-driven | `../sm-bm-app/plugins/jz/bm/chat.go` lines 1-95 | exact | +| `../sm-demo-app/plugins/demo/classes/chat_context.go` (DemoContext, ChannelContext adapter, access resolvers) | model/contract impl | request-response | `../sm-bm-app/plugins/jz/bm/classes/chat_context.go` | exact | +| `../sm-demo-app/plugins/demo/realtime_access.go` | middleware | request-response | `../sm-bm-app/plugins/jz/bm/realtime_access.go` | exact | +| `../sm-demo-app/plugins/demo/routes.go` (mountRealtime guard, chat bootstrap routes) | route | request-response | itself, lines 60-87 | exact | +| `../sm-demo-app/plugins/demo/chat_api.go` (`GET /_demo/api/v1/chat`, `/chat/members`) | controller | request-response | `../sm-bm-app/plugins/jz/bm/controllers/api/chat.go`; style `plugins/demo/quizzes_api.go` | exact | +| `../sm-demo-app/plugins/demo/chatbots.go` (demo commands) | service | request-response | RESEARCH Code Examples "Chatbots command"; contracts `chatbots/contracts/slash_command.go` | partial | +| `../sm-demo-app/plugins/demo/plugin.go` (Requires, Middlewares, Boot calls registerChat) | plugin entry | boot | itself, lines 33-100 | exact | +| `../sm-demo-app/plugins/demo/console/seed_chat.go`, `seed_forum.go`, `seed_friends.go` | console/seed | batch | `plugins/demo/console/seed.go` (`seedQuiz` 309-340, `insertID`/`exists` 177-189) | exact | +| `../sm-demo-app/plugins/demo/console/reset.go` (`resetTables`) | console | batch | itself, lines 21-75 | exact | +| `../sm-demo-app/{summer.yaml,go.mod,go.work,.gitmodules}` | config | n/a | existing entries (summer.yaml plugins list) | exact | +| `../sm-demo-app/go_mod_test.go`, `boot_test.go`, `realtime_test.go`, `reset_test.go`, `seed_test.go` (extend) + new `chat_test.go`, `seed_chat_test.go` | test | n/a | the same files (`TestGoModRequiresTags` :22, `TestBootAllPlugins` :31, `TestRealtimeRoutes` :124) | exact | +| `../sm-demo-app/deploy/centrifugo.json.example` (chat/user/video namespaces) | config | pub-sub | itself lines 41-46 + `../sm-bm-app/deploy/centrifugo/bm-config.json` lines 39+ | exact | +| `../sm-demo-app/deploy/supervisor/summercms-demo-livekit.conf`, `deploy/livekit.yaml.example` | config | n/a | `deploy/supervisor/summercms-demo-centrifugo.conf`; RESEARCH supervisor block | exact | +| `../sm-demo-app/deploy/nginx/demo.summercms.io.conf`, `scripts/{dev.sh,check-deploy.sh,check-live.sh,check-local.sh}`, `DEPLOY.md` | config/script | n/a | the same files (Centrifugo `/ws` sections) | exact | +| `../vue-chat-layer/nuxt.config.ts`, `package.json` | config | n/a | `../vue-faq-layer/nuxt.config.ts`, `package.json` | exact | +| `../vue-chat-layer/app/composables/useChatApi.ts` (auth hook `chatAuth`) | composable | request-response | `../vue-paymentgateway-layer/app/composables/usePaymentGatewayApi.ts` | exact | +| `../vue-chat-layer/app/composables/useChat.ts` (store) | store | event-driven | `../sm-bm-app/frontend/src/stores/chat.ts` (Pinia setup store, convert) | role-match | +| `../vue-chat-layer/app/lib/realtime.ts` (+`onUserEvent`) | utility | pub-sub | `../sm-demo-app/vue-demo-app/app/lib/realtime.ts` (`openChannel` :58, dynamic import :59) | exact | +| `../vue-chat-layer/app/lib/chat{Events,Format,Text,Unread}.ts` + tests, `app/components/chat/*` | utility/component | transform | `../sm-bm-app/frontend/src/lib/chat*.ts`, `frontend/src/components/chat/*` | exact (port) | +| `../vue-chat-layer/app/components/chat/{ChannelList,ThreadPane,PinsPanel,CommandPicker}.vue` | component | request-response | none in bm (bm has one channel, no threads/pins) | no analog | +| `../vue-chatvideo-layer/*` (call page on livekit-client) | component/composable | streaming | none (bm has no video UI) | no analog | +| `../vue-forum-layer/*`, `../vue-userfriends-layer/*` | layer | request-response | `../vue-faq-layer` (structure) + `../vue-paymentgateway-layer` (authenticated client) | role-match | +| `../sm-demo-app/vue-demo-app/{nuxt.config.ts,package.json,app/plugins/auth.ts}` | config/provider | n/a | itself: `extends` line 51, layer deps lines 19-22, auth.ts | exact | +| `../sm-demo-app/vue-demo-app/app/pages/{chat.vue,forum.vue,friends.vue,call/[id].vue}` | page | request-response | `app/pages/quizzes/index.vue` (anonymous register prompt, lines 46, 68) | exact | +| `../sm-demo-app/vue-demo-app/app/pages/index.vue` (tour cards) | page | n/a | itself, `EventName` const. They fire **before** the transaction, so they carry no `Tx`. + +**Fire site, chat:** `../sm-chat-plugin/classes/service.go` `SendMessageWith` (lines 287-345 at v0.1.3). Insert the call after the `ErrEmptyMessage` check (line ~311) and before `lagoon.Transaction`. Copy the existing nil-bus guard: +```go +if s.bus != nil { + if err := s.bus.Fire(ctx, &events.MessageCreated{MessageEvent: events.MessageEvent{ + MessageID: msg.ID, ConversationID: conversationID, ActorID: senderID, RootID: rootID, Tx: tx, + }}); err != nil { + return err + } +} +``` +Do the same in `EditMessage` after `CanEditMessage`. `Fire` returns `errors.Join`, so callers use `errors.As`. + +**Fire site, forum:** `../sm-forum-plugin/classes/forum_service.go` `CreateThread` lines 263-281. Fire between the `CanPost` check and `html := breakwater.Render(body)` (line 281). Do the same in `Reply` (line 316, before the Render at 330) and in `EditPost`. Use `s.Bus` (field at line 57-58), guarded for nil. + +**422 mapping:** `../sm-chat-plugin/api/errors.go` lines 39-44 (forum: `api/errors.go:28`): +```go +func writeValidation(w http.ResponseWriter, errs map[string][]string) { + wire.WriteJSON(w, http.StatusUnprocessableEntity, map[string]any{ + "error": errValidation, + "errors": errs, + }) +} +``` +In the service-error mapper (`writeChatError` / `writeForumError` line 44), add `var rej contracts.Rejection; if errors.As(err, &rej) { writeValidation(w, map[string][]string{rej.Field: {rej.Message}}); return }` before the 500 default. With no listener registered, no existing branch changes. + +**Tests:** keep `php_method_map_test.go` and `golden_test.go` green. Add the seam tests next to `tombstone_test.go` / `upload_limit_test.go`, using the `harness_test.go` fixture. + +--- + +### `../sm-moderation-plugin` (new shared plugin) + +**Plugin entry analog:** `../sm-sitemap-plugin/plugin.go` lines 1-60. Copy the package doc comment, the interface assertions and the embedded config: +```go +var ( + _ party.Plugin = (*Plugin)(nil) + _ pact.HasConfig = (*Plugin)(nil) + _ pact.HasMigrations = (*Plugin)(nil) + _ pact.HasModels = (*Plugin)(nil) +) +//go:embed config +var configFS embed.FS +type Plugin struct { app *backpack.App } +func (p *Plugin) ID() string { return "golem15.sitemap" } +func (p *Plugin) Requires() []string { return nil } +func (p *Plugin) Register(app *backpack.App) error { p.app = app; return nil } +``` +Publish `*classes.Filter` from an `lagoon.OnDatabase` hook in Boot, the way chat publishes its Service (`app.Lookup[*chatclasses.Service]()` in bm chat.go:58). The plugin must not import chat or forum. + +**Settings analog:** `../sm-sitemap-plugin/admin.go` lines 54-72: +```go +{ + Code: "sitemap_settings", + Label: "golem15.sitemap::lang.settings.label", + Description: "golem15.sitemap::lang.settings.description", + Category: "golem15.sitemap::lang.settings.category", + Icon: "settings", + Permissions: []string{controllers.PermissionAccessDefinitions}, + Form: "models/settings/fields.yaml", + NewModel: func() any { return &models.Settings{} }, +}, +// ... +_ pact.HasSettings = (*Plugin)(nil) +_ pact.HasLang = (*Plugin)(nil) +``` +**Model:** `../sm-sitemap-plugin/models/settings.go` uses a singleton row id 1 on `golem15__settings`, `*string` columns, and validation in `BeforeValidate` with a lang-key error const (`errFrontendURL`). The moderation model gets `banned_words` and `banned_links` (textarea, one per line). **fields.yaml:** copy the header block and the `label/comment/type/span` shape from `../sm-sitemap-plugin/models/settings/fields.yaml`. + +**Filter (no analog):** a stdlib-only `Check(ctx, text) *Violation`: case-insensitive word-boundary match for words; links matched by scheme, host or bare domain. Use table tests. + +--- + +### `../sm-demo-app/plugins/demo/chat.go` (host wiring) + +**Analog:** `../sm-bm-app/plugins/jz/bm/chat.go` lines 1-95. + +Imports (lines 3-15). Swap the bm classes for `sm-demo-app/plugins/demo/classes` and add the forum, chatbots, moderation and chat events imports: +```go +chatclasses "git.golem15.com/golem15/sm-chat-plugin/classes" +chatcontracts "git.golem15.com/golem15/sm-chat-plugin/contracts" +chanclasses "git.golem15.com/golem15/sm-chatchannels-plugin/classes" +"git.golem15.com/golem15/summercms/modules/backpack" +"git.golem15.com/golem15/summercms/modules/lagoon" +``` +Core (lines 35-50, 72-87). Add a second `reg.Add` for `chanmodels.ChannelAlias` (RESEARCH Pattern 1): +```go +if app == nil || !app.HasPlugin("golem15.chat") { return nil } +reg, ok := app.Lookup[*chatcontracts.Registry]() +if !ok || reg == nil { return errors.New("jz.bm: golem15.chat must be listed before jz.bm in summer.yaml") } +if err := reg.Add(p.ID(), chatcontracts.Registration{Alias: classes.ChatContextType, Loader: p.loadChatContext}); err != nil { return err } +... +return lagoon.OnDatabase(app, func(_ *sql.DB, _ *gorm.DB) error { + svc, ok := app.Lookup[*chanclasses.ChannelService]() + if !ok || svc == nil { return errors.New("... must be listed before ...") } + svc.SetAccessResolver(classes.NewChatAccess(p.database, p.chatGroup(), nil)) + return nil +}) +``` +Copy the config-key const block (lines 17-28) for `golem15.demo.chat.moderator_group` and the limits. The moderation listeners follow RESEARCH Pattern 2 (`app.Events.Listen[*chatevents.MessageSaving](p.ID(), ...)`). + +### `../sm-demo-app/plugins/demo/classes/chat_context.go` + +**Analog:** `../sm-bm-app/plugins/jz/bm/classes/chat_context.go`. Copy: +- The interface assertions (lines 12-17). +- The const block (lines 21-35): `ChatContextType = "demo"`, `ChatContextID = 1`, `ChatBroadcastChannel = "chat:context:1"`. The broadcast channel must never be empty. +- `MemberFunc` injection and `WithMembership` for tests. +- The five one-line methods (`ID/Type/ChatEnabled/GroupName/BroadcastChannel`). +- `CanActFor` → `chatcontracts.DefaultCanActFor`; `CanOverseeChats` false. +- `CanModerateMessage` limited to `conversationType == "group"` plus the moderator group (D-22). +- `RequireLiveMembership` true. + +Difference from bm: `HasMember` means "registered, not deleted, not banned". There is no group check (D-06), so drop `ChatMemberIDs`'s group join and query `users` directly. Add a `ChannelContext` with the same methods, loaded by channel id, and `BroadcastChannel() = "chat:channel:{id}"`. + +### `../sm-demo-app/plugins/demo/realtime_access.go` + +**Analog:** `../sm-bm-app/plugins/jz/bm/realtime_access.go` lines 1-70. Copy `MiddlewareRealtimeAccess` (name it `demo.realtime-access`), the `presentationsOwnerGuard` name const, the `sync.Once` owner-guard lookup, and the 401/503/member-pass flow (`bouncer.User(r.Context())`, `writeRealtimeGuard`). Then change `../sm-demo-app/plugins/demo/routes.go:78-86`. Mount when presentations **or** chat is active, and replace `"presentations.realtime-owner"` with the new guard: +```go +return lighthouse.Mount(r, p.realtime.Driver(), lighthouse.Surfaces{ + UserAuth: surf.Use("jwt.auth", "presentations.realtime-owner", bucketRealtime), + ServerToServer: surf.Use(), + Middleware: surf.Use(), +}) +``` +`plugin.go` gains `pact.HasMiddleware` (`Middlewares() map[string]pact.Middleware`, bm realtime_access.go:24-29). + +### `../sm-demo-app/plugins/demo/chat_api.go` + +**Analog:** `../sm-bm-app/plugins/jz/bm/controllers/api/chat.go` (bootstrap JSON shape: channels with unread and mentions, DMs, members, limits). Mount it the way `routes.go:60-62` does: `r.Group(APIPrefix, surf.Use("jwt.auth", "throttle:120,1"), func(g pact.Router) { g.Get("/chat", ...) })`. Write errors with `wire.WriteJSON(w, status, map[string]string{"error": ...})` (routes.go:98). + +### Seed and reset + +**Seed analog:** `../sm-demo-app/plugins/demo/console/seed.go`: +- Helpers at lines 177-189 (`exists`, `insertID` with `RETURNING id`). +- `seedQuiz` (309-340) is the idempotent `(bool, error)` step shape, registered through `created(...)` in `runSeed` (40-80). +- Use raw SQL only. Do not go through `chat.Service` or a `CreateChannel` gated by the resolver (RESEARCH Anti-Patterns). +- Locale variants follow `seed_locales.go`. + +**Reset analog:** `../sm-demo-app/plugins/demo/console/reset.go:21-40`, `resetTables []struct{ table, where string }`, children first. Insert the chat-family tables before `users` in the order from RESEARCH Pitfall 10. Keep `golem15_moderation_settings`. + +### Tests (sm-demo-app) + +Extend `go_mod_test.go:22 TestGoModRequiresTags` (its `wantPlugins` list), `boot_test.go:31 TestBootAllPlugins` (route prefixes), `realtime_test.go:124 TestRealtimeRoutes` (any member gets 200) and `reset_test.go`. New `chat_test.go` follows `demo_api_test.go`, and `seed_chat_test.go` follows `seed_test.go`, Docker-gated through `testmain_test.go`. + +### Deploy config + +- **Centrifugo:** `../sm-demo-app/deploy/centrifugo.json.example:41-46` uses the `{"name": ..., "subscribe_proxy_enabled": true}` format. Add `chat` (presence and join_leave options from `../sm-bm-app/deploy/centrifugo/bm-config.json` namespaces, line 39+), `user` and `video`. Keep the demo's option names, not bm's named proxies (RESEARCH A6). +- **Supervisor:** copy `deploy/supervisor/summercms-demo-centrifugo.conf` to `summercms-demo-livekit.conf` using the RESEARCH "Supervisor program" block. **LiveKit yaml:** use the RESEARCH block (ports 7890/7891/7892). +- **nginx:** add the `location ^~ /livekit/` block next to the existing `/ws` block in `deploy/nginx/demo.summercms.io.conf`, and extend the Go path allow-list with `/_chat/ /_chatchannels/ /_chatvideo/ /_chatbots/ /_forum/ /_userfriends/`. + +--- + +### `../vue-chat-layer` (and the structure of the other three layers) + +**Config analog:** `../vue-faq-layer/nuxt.config.ts`. Copy: +- The header comment ("An app consumes it with `extends: [...]`"). +- `const currentDir = fileURLToPath(new URL('.', import.meta.url))` with absolute css paths. +- `runtimeConfig..apiBase` (server, `NUXT__API_BASE`) and `runtimeConfig.public..apiBase` (same-origin). + +Leave out the faq-specific `cachePurge`/`siteCache` keys. Add `public.chat.hostApiBase` (default `/_demo/api/v1` must NOT be hard-coded with an app name; default `''` and let the app set it) and `routeRules: { '/chat/**': { ssr: false } }`. + +**package.json analog:** `../vue-faq-layer/package.json`: `"private": true, "type": "module", "main": "./nuxt.config.ts"`, `engines.node >=22.18`, `peerDependencies.nuxt 4.4.8`, devDeps nuxt 4.4.8 / typescript 5.9.3 / vue 3.5.35. For vue-chat-layer, replace the `node --test` script with vitest 3.2.7 and add `centrifuge@5.7.3`. + +**Auth-hook client analog:** `../vue-paymentgateway-layer/app/composables/usePaymentGatewayApi.ts` lines 1-55. Copy the doc comment with the `defineNuxtPlugin` provide example, the hook lookup through `useNuxtApp()`, `Authorization: Bearer`, and the server-vs-browser base split: +```ts +type AuthHook = () => string | null | undefined +const auth = (useNuxtApp() as unknown as { $paymentGatewayAuth?: AuthHook }).$paymentGatewayAuth +const authHeaders = (): Record => { + const token = typeof auth === 'function' ? auth() : null + return token ? { authorization: `Bearer ${token}` } : {} +} +``` +For chat, the hook becomes `$chatAuth: { token(), refresh(), unauthorized() }` (RESEARCH Pattern 4). The pure client goes in `app/utils/client.ts` (as in faq/paymentgateway `utils/client.ts`) so `tests/*.test.ts` can test it without Nuxt. + +**App wiring:** `../sm-demo-app/vue-demo-app/app/plugins/auth.ts`: +```ts +export default defineNuxtPlugin(() => ({ + provide: { paymentGatewayAuth: () => useCookie(DEMO_TOKEN_COOKIE).value ?? null }, +})) +``` +Add `chatAuth` (and forum/friends/video hooks) to the same `provide`. Append the layers to `nuxt.config.ts:51` `extends`, and add `package.json` deps in the `git+https://git.golem15.com/golem15/vue-x-layer.git#vX.Y.Z` form (lines 19-22). + +**Store port:** `../sm-bm-app/frontend/src/stores/chat.ts`. It imports `defineStore` from 'pinia' plus `@/api/chat`, `@/lib/chatEvents`, `@/stores/toast`. Convert it to a module-level composable that uses `useState` (no Pinia, 19.4.1). Replace `general`/`generalId` with `channels[]`. Strip the bm `embeds` lib (`deadline|class|reading`) and add a slot or renderer registry for it. + +**Realtime analog:** `../sm-demo-app/vue-demo-app/app/lib/realtime.ts`: `REALTIME_WS_PATH = '/ws'` (:7), `UNAVAILABLE = new Set([401,403,404])` (:19), `tokenFailureIsFinal` (:22), `wsURL` (:42), `openChannel` with `await import('centrifuge')` (:58-59). Move it into the layer as one shared client and add `onUserEvent(handler)` (Pattern 3). vue-chatvideo-layer reuses it and must not open a second client. + +**Pages, anonymous prompt analog:** `../sm-demo-app/vue-demo-app/app/pages/quizzes/index.vue`. The doc comment (line 4) covers the anonymous prompt; `registerLink` (line 46) is `{ path: '/register', query: { next } }`; line 68 renders `{{ t('quiz.registerToTake') }}`. Apply it to `/chat` (D-04) and `/forum` (D-21). Tour cards follow `` in the app layout. + +## Shared Patterns + +### Host wiring through the service registry plus OnDatabase +**Source:** `../sm-bm-app/plugins/jz/bm/chat.go:55-87` +**Apply to:** demo chat.go (chat limits, chatchannels resolver, forum resolver, chatbots registry, moderation filter lookup). Use the guard `if !app.HasPlugin(id) { return nil }`, then `app.Lookup[*T]()` inside `lagoon.OnDatabase`. A missing service returns the error "must be listed before … in summer.yaml". + +### Festival events with listener-error rollback +**Source:** `sm-user-plugin/classes/events.go:60-110`, fire sites `classes/access_revoked.go:50`, `backend_links.go:358` (`app.Events.Fire(ctx, &X{...})`) +**Apply to:** the chat and forum seams and the demo listeners. + +### Error envelope +**Source:** `../sm-chat-plugin/api/errors.go:35-45`, `../sm-forum-plugin/api/errors.go:24-44`: `{"error": msg}` and the 422 `{"error":"Validation failed","errors":{field:[msg]}}` through `wire.WriteJSON`. +**Apply to:** the seam rejection and the demo chat bootstrap API. + +### Layer conventions +**Source:** `../vue-faq-layer` (config, package, `tests/*.test.ts`, `app/utils/client.ts` + `app/composables/use*Api.ts`), `../vue-paymentgateway-layer` (auth hook). +**Apply to:** all four new layers. They name no app and have no Pinia. + +### Release hygiene +**Source:** `sm-bm-app/plugins/golem15/chatbots/go.mod` (commit d0b9fb1, no sibling replaces). Requires are tags only. `../sm-demo-app/scripts/check-pins.sh` and `go_mod_test.go:22`. +**Apply to:** every plugin go.mod and the demo pointer bumps. Make one `chore: bump to ` commit per plugin. + +## No Analog Found + +| File | Role | Data Flow | Reason | +|---|---|---|---| +| `../sm-moderation-plugin/classes/filter.go` | service | transform | No text filter exists. Use RESEARCH D-17 test map (word boundary, links). | +| `../vue-chat-layer` ChannelList / ThreadPane / PinsPanel / CommandPicker | component | request-response | bm UI has one channel, no threads or pins. Build on chatchannels pins and chat replies APIs (RESEARCH OQ 4). | +| `../vue-chatvideo-layer` call page | component | streaming | No video UI anywhere. Use livekit-client (human-verify checkpoint first), loaded with dynamic `import()` like centrifuge. | + +## Metadata + +**Analog search scope:** sm-demo-app (plugins/demo, deploy, scripts, vue-demo-app), sm-bm-app (plugins/jz/bm, plugins/golem15/{chat,chatchannels,chatbots}, frontend/src), sm-forum-plugin, sm-sitemap-plugin, sm-user-plugin (via demo submodule), vue-faq-layer, vue-paymentgateway-layer +**Files scanned:** ~40 +**Pattern extraction date:** 2026-10-28