docs(19): capture phase context

This commit is contained in:
2026-10-09 19:53:16 +02:00
parent 90cdc7145c
commit bf297d5244
2 changed files with 244 additions and 0 deletions

View File

@@ -0,0 +1,183 @@
# Phase 19: Chat and ChatChannels - Context
**Gathered:** 2026-10-09
**Status:** Ready for planning
<domain>
## 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).
</domain>
<decisions>
## 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.
</decisions>
<canonical_refs>
## 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
</canonical_refs>
<code_context>
## 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.
</code_context>
<specifics>
## 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.
</specifics>
<deferred>
## 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.
</deferred>
---
*Phase: 19-chat-and-chatchannels*
*Context gathered: 2026-10-09*

View File

@@ -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).