From bf297d5244260b200941136c58aad6eb8a33a227 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Fri, 9 Oct 2026 19:53:16 +0200 Subject: [PATCH] docs(19): capture phase context --- .../19-chat-and-chatchannels/19-CONTEXT.md | 183 ++++++++++++++++++ .../19-DISCUSSION-LOG.md | 61 ++++++ 2 files changed, 244 insertions(+) create mode 100644 .planning/phases/19-chat-and-chatchannels/19-CONTEXT.md create mode 100644 .planning/phases/19-chat-and-chatchannels/19-DISCUSSION-LOG.md diff --git a/.planning/phases/19-chat-and-chatchannels/19-CONTEXT.md b/.planning/phases/19-chat-and-chatchannels/19-CONTEXT.md new file mode 100644 index 0000000..d0852af --- /dev/null +++ b/.planning/phases/19-chat-and-chatchannels/19-CONTEXT.md @@ -0,0 +1,183 @@ +# Phase 19: Chat and ChatChannels - Context + +**Gathered:** 2026-10-09 +**Status:** Ready for planning + + +## Phase Boundary + +`Golem15.Chat` and `Golem15.ChatChannels` run on SummerCMS as the shared plugins `sm-chat-plugin` (`golem15.chat`) and `sm-chatchannels-plugin` (`golem15.chatchannels`), with realtime delivery through `lighthouse`. What they provide: + +- The schema, models, context adapters, jobs and services from PHP. +- A typed Go service API. +- A generic headless JSON API (`/_chat/api/v1`, `/_chatchannels/api/v1`). +- Private attachments and message search. + +The point is to have them ready for the later Wavepath port. + +Wavepath's own `/_wave/api/v1/community/...` routes, the satellites (chatbots, chatvideo, forum, herald, userfriends; Phase 19.1), the Vue frontend and the Wavepath app port itself are out of scope. + +**Repos touched:** `sm-chat-plugin` and `sm-chatchannels-plugin` (new, under `git.golem15.com/golem15/`), plus additive changes to `sm-user-plugin` (D-15, D-17) and `summercms.go` only if `lighthouse`, `attach` or `beachcomber` lack a feature. The ROADMAP Phase 19 "Repos" line and success criterion 3 need updating to match D-06 to D-09 and D-15 (do it in the first planning commit). + + + + +## Implementation Decisions + +### Source and legacy +- **D-01:** **The frozen port contract is Wavepath's tree:** chat at `943b712` and chatchannels at `b92fb42`, read from `/media/nvme/dev/jz/wavepath.org/plugins/golem15/{chat,chatchannels}`. + - The starter's chat (`4db7484`) is a strict ancestor, 97 commits behind, with nothing Wavepath lacks. Do not use it. + - The P15/P18 frozen-source rule applies. Binding: table and column names, migration-equivalent schema, realtime event names, channels and payloads, serializer output (`toApiArray` and friends), console command names, config keys and lang keys. + - **Reversibility:** costly. Names are copied into the DBs and clients of every app. +- **D-02:** **No QuestStream trace in the Go plugins.** Not ported: + - the `family_id` column and its index; + - `Conversation::LEGACY_FAMILY_CONTEXT`, `scopeForFamily`, `findOrCreateDm($family)` and `findOrCreateFamilyGroup`; + - `ChatService::getFamilyGroupChat` and `addUserToFamilyGroupChat`; + - the `migrate_family_to_context` data migration; + - the `parent_oversight` lang string; + - the QuestStream `ApiController` parent class. + + No identifier, string, comment or README names QuestStream, family, parent or child. The user does not plan a QuestStream port soon. +- **D-03:** **`canActAs` stays in the context contract, fully generic.** It means "this actor may act on behalf of another member within this context", with a neutral Go name chosen by Claude (for example `CanActFor(ctx, actorID, memberID)`). Adapters that don't need it return false. The PHP session-based "pick the member to act as" flow from the `ChatWindow` component is not ported. +- **D-04:** **Migrations are one squashed, idempotent baseline per plugin,** equal to the final PHP schema minus `family_id`: chat v1.3.0 (conversations, participants, messages with `root_id`/`edited_at`/soft deletes, thread_summaries) and chatchannels v1.6.0 (channels, channel_categories, personal_categories, channel_user_prefs, channel_members, channel_mentions, channel_pins, user_context_prefs). The baseline adopts an existing Wavepath database unchanged, as in Phase 5 and P18.1 D-04. The researcher diffs it against a real Wavepath DB. + - **Reversibility:** one-way. Migration IDs and table shapes become the shared contract. +- **D-05:** **`family_id` on adopted databases is ignored.** Fresh databases never get the column. On an adopted Wavepath DB it stays, nullable and untouched; Go never reads, writes or drops it. + +### HTTP surface +- **D-06:** **Both plugins ship a typed Go service API plus a generic headless JSON API.** + - **Service API:** a ChatService/ChannelService successor that mirrors the PHP services. App routes (the future Wavepath `/_wave` port) call it. + - **Generic API:** `/_chat/api/v1` (conversations, messages, DMs, replies and threads, read, typing, mute, attachments) and `/_chatchannels/api/v1` (channels, categories, personal categories, prefs, members, pins, mentions, message search). Each plugin owns its prefix (the `/_x/api/v1` convention, P15 D-14, P18 D-06), so an app that imports only chat gets no channel routes. + - **Reversibility:** one-way. It's a published API that apps will consume. +- **D-07:** **The generic API mirrors Wavepath's community routes.** It has the same operations, parameters and JSON payloads as `/_wave/api/v1/community/...` (`wavepath/routes.php` lines 488-526, 558-590, 673-721, and `threads/{rootId}/mute`), re-rooted at context-neutral paths. Both use one serializer set, so the Wavepath port can make its routes thin aliases or move the SPA onto `/_chat`. +- **D-08:** **Contexts are addressed by morph alias and id,** for example `/_chat/api/v1/contexts/{type}/{id}/conversation`. `{type}` is the alias registered at build time (D-11), and access is decided by the context adapter (`hasMember`, `canOverseeChats`, `isChatEnabled`). Conversation- and message-level routes use their own ids, and participation is checked on every call. +- **D-09:** **Auth is `jwt.auth`, with named rate-limit buckets.** The `chat-dm` bucket (10 per minute per user, PHP's `throttle:chat-dm`) guards DM start, and a general chat API bucket covers the rest. Mounting follows the `lighthouse.Mount` surfaces pattern, so the app picks the guard. +- **D-10:** **The legacy `/api/chat/dm` and `/api/chat/attachments/{messageId}/{fileId}` routes are dropped,** along with the `ChatWindow` and `ChatBadge` components and their `chat.js`. Both routes fatal on PHP today, and no client calls them (Wavepath's `dmApi.ts` avoids the attachment path explicitly). +- **Parity proof (replaces SC3's route wording):** golden tests show that the serializers (`Message.toApiArray` with attachments, thread summaries, channel, category, pref, pin and mention arrays) and every realtime event (name, channel and payload) match PHP output captured from Wavepath. Route-level parity of `/_wave` is checked at the Wavepath port. + +### Extension seams +- **D-11:** **Contexts plug in through a build-time morph registry,** the successor of the `chat.context.resolver` singleton (`registerAdapter`, `registerProvider`). + - At Boot, an app or plugin registers a morph alias with a loader that returns a Go chat-context interface: members, `hasMember`, `canOverseeChats`, `isChatEnabled`, group chat name, the D-03 act-for hook, and a broadcast channel of 2 or 3 segments. + - This mirrors P18.1 D-08. An unknown `context_type` read from the DB fails loudly and is never silently skipped. +- **D-12:** **Existing PHP class names in `context_type` (and `golem15_chatchannels_channels.context_type`) are rewritten to aliases on adoption.** + - A Go migration or adoption command rewrites the stored class strings to the registered aliases once. The class → alias map comes from the registrations (each registration may declare the legacy PHP class it replaces). + - After the rewrite, PHP can no longer resolve these rows, so it's a cutover point of no return, like P18 D-11's v2 rows. The app port takes a DB backup first. + - The researcher checks the other morph columns that store PHP class names, especially `system_files.attachment_type` for chat attachments (`Golem15\Chat\Models\Message`). The attachment owner morph name must keep resolving adopted files. + - **Reversibility:** one-way. Stored rows in every adopting app's DB change. +- **D-13:** **Cross-plugin hooks are typed festival events.** + - chat fires typed events for message create, update and delete inside the write transaction. chatchannels listens to them for mention accrual, reconcile and remove, and for search index and deindex. + - User lifecycle (the `beforeDelete` GDPR cascade and handle generation on create) uses `sm-user-plugin` events. Any that don't exist yet are added there, additively. + - Plugins never patch each other's models (the successor of `Message::extend` and `User::extend`). +- **D-14:** **ChannelAccessResolver** (`canAccessContext`, `canManageContext`) **is deny-all by default, with a setter.** The app installs its own resolver at Boot, and Boot logs a Warn when chatchannels runs with the default. +- **D-15:** **`users.handle` moves into `sm-user-plugin`.** + - The user plugin owns the column (unique, added idempotently if missing so Wavepath's DB adopts unchanged), handle auto-generation (the HandleGenerator port) and the backfill. + - chatchannels uses it for `@mentions` (MentionParser regex `@([\w.-]{2,255})`). + - This is an additive change to a core plugin contract. + - **Reversibility:** costly. It adds a column and behavior on the shared `users` table across every app that mounts the user plugin. +- **D-16:** **The WebSockets `$require` is replaced by `lighthouse`.** Broadcasts go through the lighthouse service: model broadcasts through a `lighthouse.Bind` binding, explicit events through `Emit`/`Publish`. chat registers the `chat` namespace authorizer (`chat:conversation:{id}`, participants only, capabilities `["prs"]`). +- **D-17:** **`sm-user-plugin` registers the lighthouse `user` namespace authorizer** (`user:{id}` allowed only to that user) if it doesn't already. chat (`chat:message`, `chat:unread`) and chatchannels (`mentions.changed`) publish there, and Phase 19.1's herald and forum will too. + +### Attachments and search +- **D-18:** **Attachments are private `lagoon/attach` files.** + - They are `system_files` rows with `is_public=false`, stored through `attach.Store` with ChatAttachmentPolicy's limits: 100 MB (`MAX_KB` 102400), at most 5 files (`MAX_ATTACHMENTS`), an extension allowlist, and SVG, HTML and XML blocked. + - Downloads go only through a `/_chat/api/v1` route that checks participation and streams the blob. They're never reachable through the public static handler. + - The attachment `kind`, `width` and `height` serialization stays as in PHP. +- **D-19:** **`chat:migrate-attachments` is not ported.** Run it on PHP before cutover. +- **D-20:** **Message search ports on `beachcomber`.** + - The read-only `MessageIndex` becomes a `beachcomber.Searchable` registration from chatchannels. + - It uses the same Typesense collection name (the scout prefix plus `messages`) and the same document fields. + - Indexing fails open, as in PHP. + - Search is exposed through `/_chatchannels/api/v1`. + +### Proof and tests +- **D-21:** **A test-only host in each plugin repo,** as in P18 D-03: testcontainers Postgres, the lighthouse memory driver, beachcomber with a fake engine, a neutral fixture context (for example `acme` teams, as chatchannels' PHP tests use `Acme\Teams\Team`), and `sm-user-plugin`. Every PHP test method (59 in chat, about 130 in chatchannels) maps to a Go case or to an explicit "not applicable" note (QuestStream-only cases). Golden payloads and events are captured from Wavepath. The mapping is completed in the last plan. + +### Claude's Discretion +- Go package, type and identifier names, including the context interface, the registry, the act-for hook, the service APIs and the event types. +- Exact endpoint paths of the generic API, within D-07 and D-08. +- Queue name and job shape for `MaintainThreadSummaryJob` on conga (PHP uses the `broadcasts` queue; it's dispatched after message create and delete when `root_id` is set, off-request). +- Config keys: `use_ui_avatars` (env `CHAT_USE_UI_AVATARS`) carries over under the plugin namespace. +- Lang strings port to phrasebook in EN and PL. The plugin display name becomes neutral (for example "Chat" rather than "Friends Chat"), and QuestStream strings are dropped per D-02. +- Whether the adoption rewrite (D-12) is a migration, a console command, or both. +- Plan count and split, subject to the plan-count checkpoint and "unit tests are the last plan". Expect chat first, then chatchannels, then the user plugin additions where they're needed. +- Run the security-review agent. The phase adds a public authenticated API, private file downloads, channel authorization and a GDPR delete cascade. + + + + +## Canonical References + +**Downstream agents MUST read these before planning or implementing.** + +### Port sources (frozen, D-01) +- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/chat` at `943b712`: models, `contracts/ChatContextInterface.php`, `classes/{ContextResolver,AbstractContextAdapter,ChatService,ChatChannelAuthorizer,ChatAttachmentPolicy}.php`, `traits/ChatBroadcasting.php`, `jobs/MaintainThreadSummaryJob.php`, `updates/`, `tests/` (59 methods) +- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/chatchannels` at `b92fb42`: `classes/ChannelService.php` (1381 lines), `MentionParser`, `HandleGenerator`, `SearchIndexer`, `contracts/ChannelAccessResolver`, `models/MessageIndex`, `updates/`, `tests/` (about 130 methods) +- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/routes.php` lines 171, 488-526, 558-590, 673-721: the community routes that the generic API mirrors (D-07) +- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/Plugin.php` lines 163-190 (context adapter registration) and 209 (access resolver rebinding): how a host wires the seams +- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends/Plugin.php` lines 41-52: the other context adapter and provider registration +- `/media/nvme/dev/jz/wavepath.org/vue-wavepath-app/src/`: the realtime events the client listens to (`created.chat.message`, `typing.chat.message`, `mentions.changed`, `chat:bot`) and the API usage in `api/dmApi.ts` + +### Project decisions +- `.planning/notes/core-plugin-port-inventory.md`: why websockets isn't ported (lighthouse covers it), chat and chatchannels sizes, port order +- `.planning/notes/core-plugins-own-repos.md`: the `sm-*-plugin` repo, module, submodule and commit workflow; core plugin contracts stay non-breaking +- `.planning/notes/plugin-layout-winter-directories.md` §"Evidence (jz golem15/chat)": the classes/models edges in chat (ChatAttachmentPolicy, the ChatBroadcasting hook, the context resolver) +- `.planning/phases/18.1-payment-gateway-subscriptions/18.1-CONTEXT.md` D-04, D-08, D-09: squashed baseline, build-time morph registry, and shared-users-column precedents +- `.planning/phases/18-payment-gateway/18-CONTEXT.md` D-03, D-06: the test-only host and the `/_x/api/v1` customer API precedents + +### Framework modules +- `modules/lighthouse/README.md`: publisher, `Bind`, `Emit`, Registry/Authorizer, `Mount` surfaces, memory driver +- `modules/lagoon/README.md` §Attachments: `attach.Store`, `attach.Limits`, `is_public`, `attach.Owner` morph name, two-phase delete +- `modules/beachcomber/README.md`: `Searchable`, Typesense engine, kill-switch +- `modules/festival/README.md`: typed synchronous events +- `modules/conga`: River jobs, for the thread summary job + + + + +## Existing Code Insights + +### Reusable Assets +- `lighthouse`: replaces the whole WebSockets dependency. `Bind` covers the `created.chat.message` model broadcast; `Emit` and `Publish` cover the typing, read, unread, `chat:message` and `mentions.changed` events; `Registry.Register("chat", …)` covers ChatChannelAuthorizer; `MemoryDriver` serves the golden event tests. +- `lagoon/attach`: protected `system_files` storage with limits and an extension and MIME allowlist, matching ChatAttachmentPolicy. +- `beachcomber` with its `typesense` driver: the Scout successor for MessageIndex. +- `conga`: River jobs for `MaintainThreadSummaryJob`. +- `festival`: the typed event bus for the D-13 hooks. +- `sm-user-plugin`: the `jwt.auth` guard, the user model and lifecycle; it gains `handle` (D-15) and the `user` authorizer (D-17). + +### Established Patterns +- A squashed idempotent baseline that adopts the PHP DB (Phase 5, P18.1 D-04). +- A build-time registry instead of PHP singletons and config pushes (P18 D-15, P18.1 D-08). +- A headless `/_x/api/v1` API per plugin (P15 D-14, P18 D-06, P18.1 D-13). +- Plugin READMEs never name a consuming app (CLAUDE.md); the fixtures use `acme`. + +### Integration Points +- The app (later Wavepath) registers context aliases (D-11), installs the ChannelAccessResolver (D-14), and mounts both APIs (D-09). +- chat → chatchannels only through festival events (D-13); chatchannels requires chat. +- Message broadcasts are enqueued in the write transaction through lighthouse callbacks, so nothing is published for a write that rolls back. + + + + +## Specific Ideas + +- "No trace of QuestStream / Family / Child at this stage". This applies to code, strings, comments and READMEs. +- The generic API should let the Wavepath port reuse it rather than re-implement it: identical payloads and serializers. + + + + +## Deferred Ideas + +- A `vue-chat-layer` with chat window and badge components: once a client wants the generic API. +- QuestStream chat features (family contexts, parent oversight): with a future QuestStream port, in its app plugin. +- `chat:migrate-attachments`: run on PHP before the Wavepath cutover, not ported (D-19). +- Wavepath's `/_wave/api/v1/community` routes: with the Wavepath app port. + +### Reviewed Todos (not folded) +- The todo matcher returned only keyword noise (journal author fields, menu/faq/sitemap ports, docs and scaffold todos). None concern chat, so none were folded. + + + +--- + +*Phase: 19-chat-and-chatchannels* +*Context gathered: 2026-10-09* diff --git a/.planning/phases/19-chat-and-chatchannels/19-DISCUSSION-LOG.md b/.planning/phases/19-chat-and-chatchannels/19-DISCUSSION-LOG.md new file mode 100644 index 0000000..59fc6a5 --- /dev/null +++ b/.planning/phases/19-chat-and-chatchannels/19-DISCUSSION-LOG.md @@ -0,0 +1,61 @@ +# Phase 19: Chat and ChatChannels - Discussion Log + +> **Audit trail only.** Do not use as input to planning, research, or execution agents. +> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered. + +**Date:** 2026-10-09 +**Phase:** 19-chat-and-chatchannels +**Areas discussed:** Source and QuestStream, HTTP surface, Extension seams, Attachments and search + +--- + +## Source and QuestStream + +| Question | Options | Selected | +|---|---|---| +| Frozen source | Wavepath HEADs (943b712 / b92fb42); latest develop at plan time | Wavepath HEADs | +| QuestStream leftovers | Schema only; port everything 1:1; drop column too | Drop column too ("won't be migrating QuestStream anytime soon") | +| canActAs | Keep in contract; drop it | Keep, "as generic as possible, no trace of QuestStream / Family / Child" | +| Migrations | Squashed baseline; port each version | Squashed baseline | +| family_id on adopted DBs | Drop if exists; ignore if present | Ignore if present | + +## HTTP surface + +| Question | Options | Selected | +|---|---|---| +| Plugin HTTP surface | Service API + serializers; plus generic /_chat/api/v1; move /_wave community routes in | Plus generic /_chat/api/v1 | +| Legacy /api/chat routes | Drop both; port as-is | Drop both | +| SC3 parity proof | Golden payloads + events; test-host routes | Golden payloads + events | +| Frontend | No layer now; vue-chat-layer | No layer now | +| API shape | Mirror /_wave community; fresh REST design | Mirror /_wave community | +| Channel endpoints | /_chatchannels/api/v1; all under /_chat | /_chatchannels/api/v1 | +| Context in URL | Morph alias + id; conversation id only | Morph alias + id | +| Auth | jwt.auth + named buckets; app mounts with its guard | jwt.auth + named buckets | + +## Extension seams + +| Question | Options | Selected | +|---|---|---| +| Context plug-in | Build-time morph registry; keep PHP class names | Build-time morph registry | +| Morph values in existing rows | Alias + legacy class map; rewrite on adoption | Rewrite on adoption | +| Message/User hooks | Typed festival events; GORM callbacks | Typed festival events | +| users.handle | chatchannels migration; move into sm-user-plugin | Move into sm-user-plugin | +| ChannelAccessResolver default | Deny-all + setter; derive from chat context | Deny-all + setter | +| user:{id} authorizer | sm-user-plugin; chat plugin | sm-user-plugin | + +## Attachments and search + +| Question | Options | Selected | +|---|---|---| +| Storage | attach private + service route; signed URLs | attach private + service route | +| chat:migrate-attachments | Port it; skip | Skip | +| MessageIndex search | Port on beachcomber; defer | Port on beachcomber | +| Tests | Map PHP tests in test host; fresh Go tests | Map PHP tests | + +## Claude's Discretion + +Go names, exact generic API paths, the thread summary job's queue, config key namespacing, lang neutral naming, whether the adoption rewrite is a migration or a command, plan split. + +## Deferred Ideas + +vue-chat-layer; QuestStream chat features; chat:migrate-attachments (run on PHP); /_wave community routes (Wavepath port).