docs(19): create phase plan
This commit is contained in:
@@ -1187,20 +1187,31 @@ Plans:
|
||||
**Goal:** `Golem15.Chat` and `Golem15.ChatChannels` run on SummerCMS as shared plugins, with realtime delivery through `lighthouse`, ready for the Wavepath port.
|
||||
**Mode:** mvp
|
||||
**Depends on:** Phase 11
|
||||
**Repos:** `sm-chat-plugin` and `sm-chatchannels-plugin` (new, under `git.golem15.com/golem15/`); `summercms.go` only if `lighthouse` is missing a feature
|
||||
**Repos:** `sm-chat-plugin` and `sm-chatchannels-plugin` (new, under `git.golem15.com/golem15/`); additive `sm-user-plugin` (`users.handle`, `user` lighthouse authorizer, lifecycle events; D-15, D-17); `summercms.go` only if `lighthouse`, `attach` or `beachcomber` lack a feature (none found)
|
||||
**Port sources:** `/media/nvme/dev/jz/wavepath.org/plugins/golem15/{chat,chatchannels}`. Port the Wavepath copy (with `root_id` threading and `thread_summaries`); decided 2026-10-11, see `.planning/notes/wavepath-framework-asks-division.md`.
|
||||
**Requirements**: TBD
|
||||
**Requirements**: TBD (plans cover SC-19-1..SC-19-4 and locked CONTEXT D-01..D-21)
|
||||
**Success Criteria** (what must be TRUE):
|
||||
|
||||
1. The chat schema, models, adapters (context-agnostic groups), jobs, commands and routes port 1:1. ChatChannels plugs into chat through the same adapter contract.
|
||||
2. Realtime broadcasts and channel authorization go through `lighthouse` (publisher, `Authorizer` per namespace). No WebSockets plugin is ported.
|
||||
3. Public API payloads match PHP for the routes Wavepath's clients call.
|
||||
3. 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.
|
||||
4. The new code has unit tests, delivered in the phase's last plan.
|
||||
|
||||
**Plans:** 0 plans
|
||||
**Plans:** 5 plans
|
||||
|
||||
Plans:
|
||||
- [ ] TBD (run $gsd-plan-phase 19 to break down)
|
||||
**Wave 1**
|
||||
- [ ] 19-01-PLAN.md — Chat core: new sm-chat-plugin repo, squashed baseline, models, context registry, ChatService, lighthouse Bind/Emit, thread-summary job
|
||||
|
||||
**Wave 2** *(blocked on Wave 1)*
|
||||
- [ ] 19-02-PLAN.md — Chat generic API: /_chat/api/v1 (conversations, messages, DMs, replies, read, typing, mute, attachments)
|
||||
- [ ] 19-03-PLAN.md — ChatChannels core: new sm-chatchannels-plugin repo, baseline, ChannelService, festival listeners, beachcomber search
|
||||
|
||||
**Wave 3** *(blocked on Wave 2)*
|
||||
- [ ] 19-04-PLAN.md — ChatChannels generic API + sm-user-plugin handle, user authorizer, lifecycle events
|
||||
|
||||
**Wave 4** *(blocked on Wave 3)*
|
||||
- [ ] 19-05-PLAN.md — Unit tests LAST: goldens, PHP method map (59 chat + 106 chatchannels), scripts/check-phase19.sh
|
||||
|
||||
### Phase 19.1: Chat satellites
|
||||
|
||||
|
||||
@@ -3,18 +3,17 @@ msd_state_version: "1.0"
|
||||
milestone: v1.0
|
||||
current_phase: 19
|
||||
current_phase_name: Chat and ChatChannels
|
||||
status: planning
|
||||
status: executing
|
||||
stopped_at: Phase 18.1 complete, ready to plan Phase 19
|
||||
last_updated: "2026-10-16T19:08:06.609Z"
|
||||
last_updated: "2026-10-16T23:27:40.740Z"
|
||||
last_activity: 2026-10-16
|
||||
last_activity_desc: Phase 18.1 complete, transitioned to Phase 19
|
||||
state_head: c02f2724aeb21135f9077b8037bd72871e74a5e8
|
||||
state_head: d12adcf19af2960084438b657dca3a1dc09464b8
|
||||
progress:
|
||||
total_phases: 40
|
||||
completed_phases: 32
|
||||
total_plans: 191
|
||||
total_plans: 196
|
||||
completed_plans: 191
|
||||
percent: 80
|
||||
gsd_state_version: "1.0"
|
||||
milestone_name: milestone
|
||||
---
|
||||
@@ -30,9 +29,9 @@ See: .planning/PROJECT.md (updated 2026-10-12)
|
||||
|
||||
## Current Position
|
||||
|
||||
Phase: 19 — Chat and ChatChannels
|
||||
Phase: 19 (Chat and ChatChannels) — READY TO EXECUTE
|
||||
Plan: Not started
|
||||
Status: Ready to plan
|
||||
Status: Ready to execute
|
||||
Last activity: 2026-10-16 — Phase 18.1 complete, transitioned to Phase 19
|
||||
|
||||
Progress: [████████░░] 80%
|
||||
|
||||
317
.planning/phases/19-chat-and-chatchannels/19-01-PLAN.md
Normal file
317
.planning/phases/19-chat-and-chatchannels/19-01-PLAN.md
Normal file
@@ -0,0 +1,317 @@
|
||||
---
|
||||
phase: 19-chat-and-chatchannels
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- .planning/ROADMAP.md
|
||||
- ../sm-chat-plugin/.gitignore
|
||||
- ../sm-chat-plugin/go.mod
|
||||
- ../sm-chat-plugin/go.sum
|
||||
- ../sm-chat-plugin/go.work
|
||||
- ../sm-chat-plugin/README.md
|
||||
- ../sm-chat-plugin/plugin.go
|
||||
- ../sm-chat-plugin/boot.go
|
||||
- ../sm-chat-plugin/config/config.yaml
|
||||
- ../sm-chat-plugin/lang/en/lang.yaml
|
||||
- ../sm-chat-plugin/lang/pl/lang.yaml
|
||||
- ../sm-chat-plugin/contracts/context.go
|
||||
- ../sm-chat-plugin/contracts/registry.go
|
||||
- ../sm-chat-plugin/models/conversation.go
|
||||
- ../sm-chat-plugin/models/participant.go
|
||||
- ../sm-chat-plugin/models/message.go
|
||||
- ../sm-chat-plugin/models/thread_summary.go
|
||||
- ../sm-chat-plugin/models/thread_mute.go
|
||||
- ../sm-chat-plugin/models/registry.go
|
||||
- ../sm-chat-plugin/events/events.go
|
||||
- ../sm-chat-plugin/classes/service.go
|
||||
- ../sm-chat-plugin/classes/authorizer.go
|
||||
- ../sm-chat-plugin/jobs/maintain_thread_summary.go
|
||||
- ../sm-chat-plugin/updates/registry.go
|
||||
- ../sm-chat-plugin/updates/202610160001_create_golem15_chat_baseline.go
|
||||
- ../sm-chat-plugin/console/rewrite_context_types.go
|
||||
- ../sm-chat-plugin/testmain_test.go
|
||||
- ../sm-chat-plugin/harness_test.go
|
||||
- ../sm-chat-plugin/fixture_plugin_test.go
|
||||
- ../sm-chat-plugin/tracer_test.go
|
||||
autonomous: false
|
||||
requirements: [SC-19-1, SC-19-2, D-01, D-02, D-03, D-04, D-05, D-11, D-12, D-13, D-16, D-19]
|
||||
estimate:
|
||||
tokens: 200000
|
||||
raw_tokens: 200000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "D-01: module git.golem15.com/golem15/sm-chat-plugin, package chat, plugin ID golem15.chat, Requires golem15.user; table names golem15_chat_conversations, golem15_chat_participants, golem15_chat_messages, golem15_chat_thread_summaries match PHP v1.3.0 at chat 943b712."
|
||||
- "D-04 / D-05: one gormigrate ID 202610160001_create_golem15_chat_baseline uses CREATE TABLE IF NOT EXISTS / CREATE INDEX IF NOT EXISTS, import-compatible Postgres types (TIMESTAMPTZ, JSONB), and never adds or drops the D-05 legacy owner column."
|
||||
- "D-11: contracts.Registry.Add(pluginID, Registration{Alias, Loader, LegacyPHPClass}) stores the alias; Resolver(alias) returns ErrUnknownContextType and is never skipped; chat itself registers no host aliases."
|
||||
- "D-03: chat.Context.CanActFor(ctx, actorID, memberID) is on the interface; the default adapter returns true only when actorID == memberID."
|
||||
- "D-13 / D-16: SendMessage, EditMessage and DeleteMessage persist in lagoon.Transaction and Fire events.MessageCreated / MessageUpdated / MessageDeleted inside that tx; Bind alias chat.message publishes created.chat.message with the PHP ids-only payload to context BroadcastChannel plus chat:conversation:{id}; MemoryDriver publications contain no message body key."
|
||||
- "SC-19-2: lighthouse.Registry.Register(\"chat\", authorizeChat) allows only participants on chat:conversation:{id} with capabilities [\"prs\"]; the authorizer splits the channel itself and does not call lighthouse.ChannelID for that id."
|
||||
artifacts:
|
||||
- path: "../sm-chat-plugin/plugin.go"
|
||||
provides: "golem15.chat plugin: Requires, config, lang, migrations, models, jobs, commands, boot steps"
|
||||
contains: "golem15.chat"
|
||||
- path: "../sm-chat-plugin/updates/202610160001_create_golem15_chat_baseline.go"
|
||||
provides: "idempotent chat v1.3.0 baseline minus the D-05 column"
|
||||
contains: "IF NOT EXISTS"
|
||||
- path: "../sm-chat-plugin/contracts/context.go"
|
||||
provides: "Context interface including CanActFor and BroadcastChannel"
|
||||
contains: "CanActFor"
|
||||
- path: "../sm-chat-plugin/classes/service.go"
|
||||
provides: "published *classes.Service ChatService successor"
|
||||
contains: "SendMessage"
|
||||
- path: "../sm-chat-plugin/events/events.go"
|
||||
provides: "MessageCreated / Updated / Deleted festival events"
|
||||
contains: "golem15.chat.message.created"
|
||||
key_links:
|
||||
- from: "../sm-chat-plugin/boot.go"
|
||||
to: "../sm-chat-plugin/classes/authorizer.go"
|
||||
via: "Boot realtime step lighthouse.From then Registry.Register chat and Bind Message"
|
||||
pattern: "Register\\(\"chat\""
|
||||
- from: "../sm-chat-plugin/classes/service.go"
|
||||
to: "../sm-chat-plugin/events/events.go"
|
||||
via: "Fire MessageCreated, MessageUpdated and MessageDeleted inside the write transaction"
|
||||
pattern: "MessageUpdated"
|
||||
- from: "../sm-chat-plugin/console/rewrite_context_types.go"
|
||||
to: "../sm-chat-plugin/contracts/registry.go"
|
||||
via: "D-12 rewrite uses each registration LegacyPHPClass"
|
||||
pattern: "LegacyPHPClass"
|
||||
prohibitions:
|
||||
- statement: "Production sources, comments, lang and README never contain the four D-02 tokens; plugin.name is Chat / Czat."
|
||||
status: unresolved
|
||||
verification: grep
|
||||
- statement: "summercms.go modules are not edited; no new third-party require beyond the analog pin set."
|
||||
status: unresolved
|
||||
verification: grep
|
||||
- statement: "chat:migrate-attachments is not ported (D-19). Friendship-gated global DMs and /_wave routes stay out."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As a** application developer, **I want to** have Chat and ChatChannels run on SummerCMS as shared plugins with realtime through lighthouse, **so that** they are ready for the Wavepath port.
|
||||
|
||||
This plan's slice (plan 1 of 5, wave 1): the `golem15.chat` core in a new repository, proven by one SendMessage path through models, the context registry, festival and lighthouse Bind.
|
||||
|
||||
## Spec-less probe fallback
|
||||
|
||||
No SPEC.md and no REQUIREMENTS IDs exist for this phase: the orchestrator recorded "spec-less probe fallback skipped: phase has no requirement IDs to probe (visible skip)". No probe predicates were generated. `must_haves` derive from ROADMAP success criteria SC-19-1..SC-19-4 and locked CONTEXT D-01..D-21.
|
||||
|
||||
## Schema-gate recorded choice
|
||||
|
||||
Schema-gate JS ORM push does not apply. This phase uses per-plugin gormigrate `CREATE TABLE IF NOT EXISTS` baselines (D-04). No prisma / drizzle / supabase / typeorm push task is added.
|
||||
|
||||
## Assumption-delta decision
|
||||
|
||||
<assumption_delta_decision>
|
||||
noun: context (registered morph alias + id)
|
||||
decision: promote
|
||||
rationale: D-11 and D-12 make the registered alias the primary identity; the PHP class string is LegacyPHPClass for a one-time rewrite only.
|
||||
</assumption_delta_decision>
|
||||
|
||||
## Decisions taken in this plan (Claude's discretion)
|
||||
|
||||
- Package `chat`, IDs `golem15.chat`, Winter directory layout (`contracts`, `models` leaf, `events`, `classes`, `jobs`, `updates`, `console`). Service is `classes.Service`, published at Boot, found with `app.Lookup[*classes.Service]()`.
|
||||
- Context API: `chat.Context` with `CanActFor`; `chat.Registry` ships `Add` / `Resolver` and a provider slot (`ContextForUser`) for PHP parity (RESEARCH Q5) that generic routes in plan 02 must not use (D-08).
|
||||
- Festival names: `golem15.chat.message.created`, `.updated`, `.deleted`; structs `events.MessageCreated`, `MessageUpdated`, `MessageDeleted` with `MessageEvent{MessageID, ConversationID, ActorID, RootID *uint, Tx *gorm.DB}`.
|
||||
- Job kind `golem15.chat.maintain_thread_summary`, queue `broadcasts`, `conga.MaxAttempts(1)`.
|
||||
- Config key `golem15.chat.use_ui_avatars` (env `SUMMER_GOLEM15__CHAT__USE_UI_AVATARS`; document `CHAT_USE_UI_AVATARS` as the PHP name).
|
||||
- D-12: both an idempotent `UPDATE … SET context_type = alias WHERE context_type = LegacyPHPClass` in the rewrite command path and `chat:rewrite-context-types` for hosts that register late.
|
||||
- ROADMAP Repos + SC-19-3 wording is patched in this plan if the planning commit has not already done so (CONTEXT parity proof).
|
||||
|
||||
## Artifacts this phase produces (this plan)
|
||||
|
||||
- New local repo `../sm-chat-plugin` (module `git.golem15.com/golem15/sm-chat-plugin`), analog `go.mod` / `go.work` (sibling replace `../summercms.go`, user replace `../fonoteka.go/plugins/golem15/user`).
|
||||
- Squashed baseline `202610160001_create_golem15_chat_baseline`.
|
||||
- Models, registry, `classes.Service` ChatService methods listed in RESEARCH (drop the two D-02 helpers), lighthouse Bind + chat authorizer, thread-summary job, rewrite command, EN/PL phrasebook, test-only `acme.teams` host, `TestChatTracer`.
|
||||
|
||||
<objective>
|
||||
Create sm-chat-plugin with the frozen chat v1.3.0 schema (D-01, D-04, D-05), the build-time context registry (D-11, D-03), ChatService writes that fire festival events (D-13) and publish created.chat.message through lighthouse Bind (D-16), plus the thread-summary job and D-12 rewrite command.
|
||||
|
||||
Purpose: later plans mount HTTP and ChatChannels on this service; the tracer proves the write path before any route exists.
|
||||
Output: a compiling sibling module whose TestChatTracer sends one message and sees an ids-only MemoryDriver publication.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/msd-core/workflows/execute-plan.md
|
||||
@~/.claude/msd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/19-chat-and-chatchannels/19-CONTEXT.md
|
||||
@.planning/phases/19-chat-and-chatchannels/19-RESEARCH.md
|
||||
@.planning/phases/19-chat-and-chatchannels/19-PATTERNS.md
|
||||
|
||||
Concurrency: other sessions may edit summercms.go (Phase 19.3 leftover) or STATE.md. Do not stage those. Do not edit modules/lighthouse, attach, beachcomber, festival or conga (RESEARCH A5: no framework gap). Stage by explicit path. No co-author tags. Do not push unless the Task 1 reply says so.
|
||||
|
||||
<interfaces>
|
||||
Analog (copy shape, change names):
|
||||
- ../sm-paymentgatewaysubscriptions-plugin/plugin.go (PluginID, embed config/lang, Requires, init party.Register)
|
||||
- ../sm-paymentgatewaysubscriptions-plugin/boot.go (registerBootStep / runBootSteps)
|
||||
- ../sm-paymentgatewaysubscriptions-plugin/contracts/registry.go (ErrUnknownSubscriberType never skip)
|
||||
- ../sm-paymentgatewaysubscriptions-plugin/events/events.go (one …EventName per line, Tx field)
|
||||
- ../sm-paymentgatewaysubscriptions-plugin/updates/202610150001_create_golem15_pgsub_baseline.go (IF NOT EXISTS)
|
||||
- ../sm-paymentgatewaysubscriptions-plugin/testmain_test.go (flag.Parse + testing.Short)
|
||||
- ../sm-paymentgatewaysubscriptions-plugin/harness_test.go and fixture_plugin_test.go (acme.members → acme.teams)
|
||||
- ../sm-pgstripe-plugin/subscriptions/job.go (Kind, conga.Job)
|
||||
- modules/lighthouse/README.md Bind / Registry.Register; modules/lighthouse/channel.go ChannelID is segment 1
|
||||
- Frozen PHP at /media/nvme/dev/jz/wavepath.org/plugins/golem15/chat @ 943b712 (models, ChatService, ChatContextInterface, ChatBroadcasting, ChatChannelAuthorizer, MaintainThreadSummaryJob, updates/)
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="checkpoint:decision" gate="blocking-human">
|
||||
<name>Task 1: Confirm D-01 names, D-04 baseline id and D-12 rewrite before the new repo exists</name>
|
||||
<read_first>.planning/phases/19-chat-and-chatchannels/19-CONTEXT.md (D-01, D-04, D-05, D-12), .planning/phases/19-chat-and-chatchannels/19-RESEARCH.md (discretion defaults, Pitfall 3), .planning/notes/core-plugins-own-repos.md</read_first>
|
||||
<action>Print a decision sheet, then stop. Do not create the directory, do not run tea, do not push. The sheet lists the locked identifiers this plan will write: module git.golem15.com/golem15/sm-chat-plugin, package chat, PluginID golem15.chat, tables golem15_chat_conversations / golem15_chat_participants / golem15_chat_messages / golem15_chat_thread_summaries plus chat-owned golem15_chat_thread_mutes (D-07), migration id 202610160001_create_golem15_chat_baseline, import-compatible Postgres IF NOT EXISTS (P18.1 D-16, RESEARCH Pitfall 3), D-05 column omitted on fresh databases and never dropped, D-12 both an idempotent UPDATE from LegacyPHPClass and command chat:rewrite-context-types, Message.MorphName stays the PHP class string Golem15\Chat\Models\Message. Also list tea repos create --owner golem15 --name sm-chat-plugin (check tea repos create --help for flags) and that hold keeps work local. Confirm ROADMAP Phase 19 Repos and success criterion 3 already carry the CONTEXT wording; if they still say clients-call route parity, patch .planning/ROADMAP.md in the first commit of Task 2.</action>
|
||||
<decision>Lock D-01 plugin names, D-04 baseline id 202610160001_create_golem15_chat_baseline, and D-12 rewrite-as-both, then implement locally and optionally create the remote?</decision>
|
||||
<context>These names land in every adopting app's database and in go.mod require paths. Changing the migration id or table names after an app migrates needs a new migration. The D-12 rewrite is a cutover: PHP can no longer resolve rewritten context_type rows. A pushed main is visible to every developer; no application mounts this plugin yet.</context>
|
||||
<options>
|
||||
<option id="proceed-and-push">
|
||||
<name>Lock the identifiers, implement, tea-create and push main after Task 3 is green</name>
|
||||
<pros>Backup and reviewable remote now.</pros>
|
||||
<cons>Later plans push follow-up commits.</cons>
|
||||
</option>
|
||||
<option id="proceed-local">
|
||||
<name>Lock the identifiers and implement locally; no remote</name>
|
||||
<pros>Nothing leaves the machine until a later gate.</pros>
|
||||
<cons>No remote backup until then. Plan 03 follows the same local-only choice for sm-chatchannels-plugin.</cons>
|
||||
</option>
|
||||
<option id="abort">
|
||||
<name>Stop; do not create the repository</name>
|
||||
<pros>The one-way door stays closed.</pros>
|
||||
<cons>Phase 19 cannot start.</cons>
|
||||
</option>
|
||||
</options>
|
||||
<resume-signal>Reply proceed-and-push, proceed-local, or abort.</resume-signal>
|
||||
<verify>
|
||||
<automated>test -f .planning/phases/19-chat-and-chatchannels/19-CONTEXT.md && grep -q "D-04" .planning/phases/19-chat-and-chatchannels/19-CONTEXT.md && echo decision-sheet-ready</automated>
|
||||
<fails_when>non-zero exit or "decision-sheet-ready" absent</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- SUMMARY records the reply verbatim.
|
||||
- After abort, ../sm-chat-plugin does not exist.
|
||||
</acceptance_criteria>
|
||||
<done>The human confirmed the locked names and the remote choice before any repo file was written.</done>
|
||||
</task>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 2: End-to-end SendMessage — one path through repo, baseline, registry, service, festival and lighthouse Bind</name>
|
||||
<reversibility rating="one-way">D-01 table and plugin names and D-04 migration id become the shared contract every adopting app migrates; undoing them needs a new migration and a go.mod path change.</reversibility>
|
||||
<precondition>Task 1 reply is proceed-and-push or proceed-local; ../sm-chat-plugin does not already exist as a foreign module.</precondition>
|
||||
<files>../sm-chat-plugin/go.mod, ../sm-chat-plugin/go.work, ../sm-chat-plugin/go.sum, ../sm-chat-plugin/.gitignore, ../sm-chat-plugin/README.md, ../sm-chat-plugin/plugin.go, ../sm-chat-plugin/boot.go, ../sm-chat-plugin/config/config.yaml, ../sm-chat-plugin/lang/en/lang.yaml, ../sm-chat-plugin/lang/pl/lang.yaml, ../sm-chat-plugin/contracts/context.go, ../sm-chat-plugin/contracts/registry.go, ../sm-chat-plugin/models/conversation.go, ../sm-chat-plugin/models/participant.go, ../sm-chat-plugin/models/message.go, ../sm-chat-plugin/models/thread_summary.go, ../sm-chat-plugin/models/thread_mute.go, ../sm-chat-plugin/models/registry.go, ../sm-chat-plugin/events/events.go, ../sm-chat-plugin/classes/service.go, ../sm-chat-plugin/classes/authorizer.go, ../sm-chat-plugin/updates/registry.go, ../sm-chat-plugin/updates/202610160001_create_golem15_chat_baseline.go, ../sm-chat-plugin/testmain_test.go, ../sm-chat-plugin/harness_test.go, ../sm-chat-plugin/fixture_plugin_test.go, ../sm-chat-plugin/tracer_test.go, .planning/ROADMAP.md</files>
|
||||
<read_first>../sm-paymentgatewaysubscriptions-plugin/plugin.go, ../sm-paymentgatewaysubscriptions-plugin/boot.go, ../sm-paymentgatewaysubscriptions-plugin/go.mod, ../sm-paymentgatewaysubscriptions-plugin/go.work, ../sm-paymentgatewaysubscriptions-plugin/contracts/registry.go, ../sm-paymentgatewaysubscriptions-plugin/events/events.go, ../sm-paymentgatewaysubscriptions-plugin/updates/202610150001_create_golem15_pgsub_baseline.go, ../sm-paymentgatewaysubscriptions-plugin/testmain_test.go, ../sm-paymentgatewaysubscriptions-plugin/harness_test.go, ../sm-paymentgatewaysubscriptions-plugin/fixture_plugin_test.go, modules/lighthouse/README.md, modules/lighthouse/broadcast.go, modules/lighthouse/channel.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chat/models/Message.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chat/classes/ChatService.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chat/contracts/ChatContextInterface.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chat/traits/ChatBroadcasting.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chat/classes/ChatChannelAuthorizer.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chat/updates/</read_first>
|
||||
<action>Per D-01 and D-04, create ../sm-chat-plugin as a sibling of summercms.go. Copy the analog go.mod pin set (Go 1.27.0, gorm v1.31.2, gormigrate v2.1.7, pgx v5.10.0, testcontainers v0.44.0, gocloud v0.46.0, summercms + sm-user-plugin require). replace summercms => ../summercms.go. go.work also replaces sm-user-plugin => ../fonoteka.go/plugins/golem15/user. Do not copy the user plugin's deep relative replace. No new third-party library.
|
||||
|
||||
plugin.go: package chat, PluginID golem15.chat, Requires golem15.user, embed config and lang, assert party.Plugin plus pact.HasConfig, HasMigrations, HasModels, HasLang, HasJobs, HasCommands (HasRoutes waits for plan 02; add it only if a compile-time assertion is needed — prefer adding Routes() that returns nil so plan 02 owns the prefix). README H1 Chat, one-sentence summary, import line, **Name:** mnemonic, standard sections; names only acme / blog; plugin display name Chat / Czat (D-02). config.yaml: use_ui_avatars default true.
|
||||
|
||||
Baseline 202610160001_create_golem15_chat_baseline (D-04): CREATE TABLE IF NOT EXISTS for the four PHP v1.3.0 tables and their indexes from RESEARCH Frozen PHP inventory. Types: TIMESTAMPTZ, JSONB for metadata and participant_ids, VARCHAR checks for conversation type dm|group and message type text|image|system. root_id and thread_summaries.root_id have no FK. Soft-delete deleted_at on messages. D-05: do not add the legacy owner column or its index; do not DROP it. Also CREATE TABLE IF NOT EXISTS golem15_chat_thread_mutes (id, user_id, root_id, is_muted boolean not null default false, timestamps, unique (user_id, root_id), indexes on user_id and root_id) as the chat-owned D-07 thread-mute store; no FK required (root_id matches messages.id). updates/registry.go copies the analog Register / All / run.
|
||||
|
||||
Models (leaf): Conversation.TableName golem15_chat_conversations; Message.MorphName the PHP class string Golem15\Chat\Models\Message (D-12 does not rewrite attachment_type, Pitfall 4); Message.AttachRelations Name attachments, Many true, Public false; ThreadMute.TableName golem15_chat_thread_mutes; models/registry.go Register / All. No lighthouse, festival or conga imports in models/.
|
||||
|
||||
contracts.Context (D-11, D-03): ID, Type (alias), Members, HasMember, CanOverseeChats, ChatEnabled, GroupName, CanActFor(ctx, actorID, memberID), BroadcastChannel (2 or 3 segments). Default CanActFor: actorID == memberID only. Registry: ErrUnknownContextType, ErrDuplicateAlias, Add(pluginID, Registration{Alias, Loader, LegacyPHPClass}), Resolver never skips, optional provider register unused by this tracer.
|
||||
|
||||
events: MessageCreatedEventName = golem15.chat.message.created (and updated / deleted constants, one per line). MessageEvent fields MessageID, ConversationID, ActorID, RootID *uint, Tx *gorm.DB.
|
||||
|
||||
classes.Service (D-13): Options{DB, Bus, Registry, Realtime, Logger}, New refuses nil DB. This tracer implements SendMessage (PHP sendMessage): participation or HasMember / ChatEnabled checked; persist Message; Fire MessageCreated inside lagoon.Transaction; Bind enqueue is lighthouse's job. Sentinel errors prefixed golem15.chat:.
|
||||
|
||||
Boot (D-16): registries publish *contracts.Registry (empty; hosts register later); realtime lighthouse.From, Bind[models.Message] Alias chat.message, ShouldBroadcast only ActionCreated, Channels = context BroadcastChannel plus chat:conversation:{id} (empty list if context unresolved — Pitfall 6), Payload = PHP getBroadcastPayload keys id, conversation_id, sender_id, root_id, type, action, created_at — never content, names, avatars or attachments (Pitfall 5); Registry.Register("chat", authorizeChat). authorizeChat splits on ":" after stripping a presence: prefix; require three segments chat, conversation, digit; do not use lighthouse.ChannelID; Allowed map user_id and conversation_id; Capabilities []string{"prs"} only. Production plugin sources do not blank-import lighthouse/centrifugo.
|
||||
|
||||
Test host (D-21 smoke only; full map is plan 05): testmain_test.go flag.Parse then testing.Short; harness boots golem15.user + golem15.chat + acme.teams with SUMMER_REALTIME__DRIVER=memory; fixturePluginID acme.teams registers a Context loader (table acme_teams_teams); TestFixtureAbsentFromProduction greps production .go/.yaml/.md for the fixture id and the four D-02 tokens. tracer_test.go TestChatTracer (Postgres, skip under -short): two members, SendMessage text, row exists, MemoryDriver.Publications() has created.chat.message on chat:conversation:{id} with ids-only payload and no content key. TestChatAuthorizer (short): participant allowed with prs; stranger denied; ChannelID-style segment-1 mistake is not used.
|
||||
|
||||
If ROADMAP Phase 19 Repos or criterion 3 still lack D-06..D-09 / D-15 and the CONTEXT parity-proof sentence, patch those two lines only.
|
||||
|
||||
Task 3 adds the rest of ChatService, Emit helpers, the job and the rewrite command. Do not add HTTP routes here.</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-chat-plugin vet ./... && go -C ../sm-chat-plugin test -count=1 -short ./...</automated>
|
||||
<fails_when>non-zero exit, or "ok" lines absent, or a package reports FAIL</fails_when>
|
||||
<automated>docker info >/dev/null 2>&1 && go -C ../sm-chat-plugin test -count=1 -run '^TestChatTracer$' -v .</automated>
|
||||
<fails_when>non-zero exit: no Docker daemon (record NOT RUN in the SUMMARY), or TestChatTracer is missing, fails, or MemoryDriver saw a content key</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep -v '^#' ../sm-chat-plugin/plugin.go | grep -c 'golem15.chat' prints at least 1.
|
||||
- grep -v '^#' ../sm-chat-plugin/updates/202610160001_create_golem15_chat_baseline.go | grep -c 'golem15_chat_conversations' prints at least 1.
|
||||
- grep -v '^#' ../sm-chat-plugin/updates/202610160001_create_golem15_chat_baseline.go | grep -c 'golem15_chat_thread_mutes' prints at least 1.
|
||||
- grep -v '^#' ../sm-chat-plugin/updates/*.go | grep -c 'family_id' prints 0.
|
||||
- grep -n 'CanActFor' ../sm-chat-plugin/contracts/context.go prints at least one line.
|
||||
- grep -n 'created.chat.message\|Alias: "chat.message"' ../sm-chat-plugin/boot.go ../sm-chat-plugin/classes/*.go prints at least one line.
|
||||
</acceptance_criteria>
|
||||
<done>A member can send one text message through ChatService; Postgres stores it; MemoryDriver sees created.chat.message with ids only.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Expand ChatService, Emit helpers, thread-summary job and D-12 rewrite</name>
|
||||
<reversibility rating="one-way">D-12 rewrite changes stored context_type rows in every adopting app; PHP cannot resolve those rows afterwards.</reversibility>
|
||||
<files>../sm-chat-plugin/classes/service.go, ../sm-chat-plugin/classes/authorizer.go, ../sm-chat-plugin/models/thread_mute.go, ../sm-chat-plugin/jobs/maintain_thread_summary.go, ../sm-chat-plugin/console/rewrite_context_types.go, ../sm-chat-plugin/plugin.go, ../sm-chat-plugin/boot.go, ../sm-chat-plugin/README.md</files>
|
||||
<read_first>../sm-chat-plugin/classes/service.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chat/classes/ChatService.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chat/jobs/MaintainThreadSummaryJob.php, ../sm-pgstripe-plugin/subscriptions/job.go, modules/conga/job.go, ../sm-journal-plugin/console/export_posts.go</read_first>
|
||||
<action>Per D-13 and D-16, finish classes.Service as the PHP ChatService successor. Port: GetContextForUser (registry provider only; unused by generic routes, RESEARCH Q5 RESOLVED), IsChatAvailable, SendMessage (already in the tracer), GetOrCreateDm (same context, Pitfall 2 — not a friendship DM), GetGroupChat, MarkAsRead, GetConversationsForUser, GetMessages (rootsOnly), GetThreadReplies, CanActFor, CanView, SendTypingIndicator, GetTotalUnreadCount, PublishUnreadCount, AddUserToGroupChat, GetAvailableDmRecipients, CanDeleteMessage, DeleteMessage, CanEditMessage, EditMessage, ToggleMute. Do not port the two D-02 helpers. GetOrCreateDm refusal is a typed error the HTTP layer maps to 422 CANNOT_CREATE_DM in plan 02.
|
||||
|
||||
Per D-13, EditMessage and DeleteMessage persist inside lagoon.Transaction and Fire events.MessageUpdated / events.MessageDeleted on that same tx (SendMessage already Fires MessageCreated in the tracer). Plan 03 listens to all three.
|
||||
|
||||
Per D-07, add MuteThread(ctx, actorID, rootID, muted bool): resolve the root message and its conversation, check participation / HasMember, upsert golem15_chat_thread_mutes keyed to (actorID, rootID) with is_muted, inside lagoon.Transaction. Actor id is the argument, never a payload field. Missing root or unauthorized actor returns the same typed not-found error the HTTP layer maps to 404. Conversation ToggleMute is a different resource and is not called here.
|
||||
|
||||
Emit helpers on the service (not Bind): typing.chat.message on chat:conversation:{id} with conversation_id, user_id, action typing-start; chat:message on user:{id} for other participants with conversation_id and message toApiArray; chat:read on user:{id} others with conversation_id, user_id, read_at; chat:unread on user:{id} with count; deleted.chat.message on chat:conversation:{id} with id, conversation_id, action deleted (RESEARCH A2 — generic delete in plan 02 calls this). Bind stays created-only.
|
||||
|
||||
Job (discretion): MaintainThreadSummaryArgs {RootID uint json root_id}, Kind golem15.chat.maintain_thread_summary. Plugin.Jobs returns conga.Job(work, conga.OnQueue("broadcasts"), conga.MaxAttempts(1)). Dispatch inside the write tx when root_id is set after create or delete, matching PHP Message.php. Worker lockForUpdate recomputes reply_count, last_reply_at, participant_ids.
|
||||
|
||||
D-12: command chat:rewrite-context-types walks Registry registrations that set LegacyPHPClass and UPDATE golem15_chat_conversations SET context_type = alias WHERE context_type = that class, idempotent. README documents the command and that hosts must register before running it. Do not rewrite system_files.attachment_type.
|
||||
|
||||
Serializer toApiArray keys from PHP Message.php plus root_id and thread_summary when present (RESEARCH Q2). Attachment URL prefix is /_chat/api/v1/… (D-10); plan 02 serves that path.
|
||||
|
||||
If Task 1 replied proceed-and-push: after verify is green, tea repos create --owner golem15 --name sm-chat-plugin, git remote add origin git@git.golem15.com:golem15/sm-chat-plugin.git, git push -u origin main (never force). If proceed-local: no remote. Record the reply outcome in the SUMMARY.</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-chat-plugin vet ./... && go -C ../sm-chat-plugin test -count=1 -short ./...</automated>
|
||||
<fails_when>non-zero exit or a package reports FAIL</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep -n 'func (s \*Service) GetOrCreateDm' ../sm-chat-plugin/classes/service.go prints one line.
|
||||
- grep -n 'func (s \*Service) MuteThread' ../sm-chat-plugin/classes/service.go prints one line.
|
||||
- grep -n 'MessageUpdated' ../sm-chat-plugin/classes/service.go prints at least one line.
|
||||
- grep -n 'MessageDeleted' ../sm-chat-plugin/classes/service.go prints at least one line.
|
||||
- grep -n 'golem15.chat.maintain_thread_summary' ../sm-chat-plugin/jobs/maintain_thread_summary.go prints at least one line.
|
||||
- grep -n 'rewrite-context-types' ../sm-chat-plugin/console/rewrite_context_types.go prints at least one line.
|
||||
- grep -v '^#' ../sm-chat-plugin/classes/service.go | grep -c 'getFamilyGroupChat' prints 0.
|
||||
</acceptance_criteria>
|
||||
<done>ChatService exposes the PHP method set minus D-02, thread summaries recompute off-request, and hosts can rewrite stored class strings to aliases.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| ChatService write → Postgres | authenticated actor ids become conversation and message rows |
|
||||
| ChatService → lighthouse MemoryDriver / Centrifugo | realtime payloads cross to subscribers |
|
||||
| Authorizer → subscribe | client-chosen channel string is trusted only after participant check |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-19-04 | Information disclosure | lighthouse Bind Payload | high | mitigate | Payload is PHP getBroadcastPayload (ids only); TestChatTracer fails if publications contain content |
|
||||
| T-19-05 | Elevation of privilege | classes/authorizer.go | high | mitigate | Split chat:conversation:{id} locally; participants only; capabilities prs; re-check every subscribe |
|
||||
| T-19-SC | Tampering | go.mod requires | high | mitigate | No new registry packages; analog pins only; package-legitimacy table is empty |
|
||||
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
go -C ../sm-chat-plugin vet ./... and go -C ../sm-chat-plugin test -count=1 -short ./... stay green. TestChatTracer on Docker proves the write-and-broadcast path. ROADMAP Phase 19 Repos and SC-19-3 match CONTEXT.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
sm-chat-plugin exists with the v1.3.0 baseline, a working SendMessage tracer, Bind created.chat.message, the remaining ChatService methods, the thread-summary job and the D-12 rewrite command. summercms.go modules are untouched.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/19-chat-and-chatchannels/19-01-SUMMARY.md` when done
|
||||
</output>
|
||||
248
.planning/phases/19-chat-and-chatchannels/19-02-PLAN.md
Normal file
248
.planning/phases/19-chat-and-chatchannels/19-02-PLAN.md
Normal file
@@ -0,0 +1,248 @@
|
||||
---
|
||||
phase: 19-chat-and-chatchannels
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["19-01"]
|
||||
files_modified:
|
||||
- ../sm-chat-plugin/plugin.go
|
||||
- ../sm-chat-plugin/boot.go
|
||||
- ../sm-chat-plugin/routes.go
|
||||
- ../sm-chat-plugin/api/json.go
|
||||
- ../sm-chat-plugin/api/errors.go
|
||||
- ../sm-chat-plugin/api/auth.go
|
||||
- ../sm-chat-plugin/api/conversations.go
|
||||
- ../sm-chat-plugin/api/messages.go
|
||||
- ../sm-chat-plugin/api/dm.go
|
||||
- ../sm-chat-plugin/api/read.go
|
||||
- ../sm-chat-plugin/api/typing.go
|
||||
- ../sm-chat-plugin/api/mute.go
|
||||
- ../sm-chat-plugin/api/threads.go
|
||||
- ../sm-chat-plugin/api/replies.go
|
||||
- ../sm-chat-plugin/api/attachments.go
|
||||
- ../sm-chat-plugin/api/download.go
|
||||
- ../sm-chat-plugin/classes/attachment_policy.go
|
||||
- ../sm-chat-plugin/README.md
|
||||
- ../sm-chat-plugin/http_tracer_test.go
|
||||
autonomous: false
|
||||
requirements: [SC-19-1, SC-19-3, D-06, D-07, D-08, D-09, D-10, D-18]
|
||||
estimate:
|
||||
tokens: 180000
|
||||
raw_tokens: 180000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "D-06 / D-08: const APIPrefix is /_chat/api/v1; contexts are addressed as /contexts/{type}/{id}/conversation; conversation- and message-level routes use their own ids; participation or HasMember is checked on every call; unknown type is 404 via ErrUnknownContextType."
|
||||
- "D-09: groups use surf.Use(\"throttle:chat-api\", \"jwt.auth\"); DM start uses throttle:chat-dm Max 10 Decay time.Minute keyed by frontend user id else ClientIP; backend principals are not actors."
|
||||
- "D-07: operations, parameters and JSON keys match Wavepath community chat (not FriendGroup / forum / presence), including PUT /_chat/api/v1/threads/{rootId}/mute with body muted, response keys root_id and is_muted, actor from jwt never the body, and 404 collapse. Persistence is chat-owned golem15_chat_thread_mutes, not a Wavepath host table. Conversation ToggleMute is a different resource."
|
||||
- "D-10: /api/chat/dm and /api/chat/attachments/{messageId}/{fileId} are not registered."
|
||||
- "D-18: uploads go through attach.Store with ChatAttachmentPolicy limits (MaxBytes 102400*1024, max 5 files, KNOWN_EXTS, SVG/HTML/XML banned by extension and sniffed MIME, is_public=false); downloads stream only from /_chat/api/v1/attachments/{messageId}/{fileId} after participation check; missing and unauthorized both 404."
|
||||
- "SC-19-3: Message.toApiArray keys include attachments id,url,filename,size,mime_type,kind,width,height and root_id; IDOR collapses to 404."
|
||||
artifacts:
|
||||
- path: "../sm-chat-plugin/routes.go"
|
||||
provides: "APIPrefix, chat-api and chat-dm buckets, jwt.auth groups"
|
||||
contains: "/_chat/api/v1"
|
||||
- path: "../sm-chat-plugin/api/download.go"
|
||||
provides: "participation-gated private blob stream"
|
||||
contains: "Content-Disposition"
|
||||
- path: "../sm-chat-plugin/classes/attachment_policy.go"
|
||||
provides: "Limits and banned MIME wrapper"
|
||||
contains: "102400"
|
||||
key_links:
|
||||
- from: "../sm-chat-plugin/api/messages.go"
|
||||
to: "../sm-chat-plugin/classes/service.go"
|
||||
via: "handlers call classes.Service; they do not write rows themselves"
|
||||
pattern: "Lookup\\[\\*classes.Service\\]"
|
||||
- from: "../sm-chat-plugin/api/download.go"
|
||||
to: "modules/lagoon/attach"
|
||||
via: "bucket.NewReader after participant check; never StaticHandlerPublic"
|
||||
pattern: "NewReader"
|
||||
prohibitions:
|
||||
- statement: "No QuestStream controller parent (D-02 / Pitfall 8). No /_wave routes. No golem15_wavepath_thread_subscriptions table."
|
||||
status: unresolved
|
||||
verification: grep
|
||||
- statement: "Chat blobs are never mounted on attach.StaticHandler or StaticHandlerPublic (D-18)."
|
||||
status: unresolved
|
||||
verification: grep
|
||||
- statement: "Generic /dm is context-scoped ChatService.GetOrCreateDm, not a friendship DM (Pitfall 2)."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As a** application developer, **I want to** have Chat and ChatChannels run on SummerCMS as shared plugins with realtime through lighthouse, **so that** they are ready for the Wavepath port.
|
||||
|
||||
This plan's slice (plan 2 of 5, wave 2): the published `/_chat/api/v1` surface on the ChatService from 19-01, proven by one authenticated send, then expanded to DMs, replies, read, typing, conversation mute, thread mute and private attachments.
|
||||
|
||||
## Spec-less probe fallback
|
||||
|
||||
Skipped visibly: the phase has no requirement IDs to probe (see 19-01). `must_haves` derive from SC-19-1..SC-19-4 and D-01..D-21.
|
||||
|
||||
## Decisions taken in this plan (Claude's discretion)
|
||||
|
||||
- Paths (RESEARCH): GET/POST `/_chat/api/v1/contexts/{type}/{id}/conversation`, `/conversations/{id}/messages`, `/messages/{id}`, `/conversations/{id}/read`, `/conversations/{id}/typing`, `/conversations/{id}/mute`, `/contexts/{type}/{id}/dm`, `/attachments` (pending), `/attachments/{messageId}/{fileId}`, `/messages/{id}/replies`, `PUT /threads/{rootId}/mute` (D-07; Q1 RESOLVED).
|
||||
- Error envelope copies pgsub `api/errors.go`: `wire.WriteJSON` `{"error": message}`; 422 `{"error":"Validation failed","errors":{field:[msg]}}`; DM refusal 422 `CANNOT_CREATE_DM`. IDOR → 404.
|
||||
- Auth: `surf.Use("jwt.auth")` (D-09). Prefer wrapping with paymentgateway `RequireJWT` when cookies must not be read. `frontendUser` refuses backend principals.
|
||||
- Buckets: `chat-api` 120/min per user else IP; `chat-dm` 10/min; 429 body stays surf `{"message":"Too Many Attempts."}`.
|
||||
- Pending uploads field `chat-pending-{actorID}`, unattached, is_public=false (PHP ChatService).
|
||||
|
||||
## Artifacts this phase produces (this plan)
|
||||
|
||||
- `routes.go` APIPrefix `/_chat/api/v1`, named buckets, jwt.auth groups.
|
||||
- `api/*` handlers for the D-06 chat operations, including D-07 `PUT /threads/{rootId}/mute`.
|
||||
- `classes/attachment_policy.go` + participation-gated download.
|
||||
- `http_tracer_test.go` TestChatHTTPTracer (one send) and short tests for throttle, 404 collapse, attachment policy.
|
||||
|
||||
<objective>
|
||||
Publish the D-06 / D-07 generic chat API on /_chat/api/v1 with jwt.auth and named buckets (D-09), context addressing (D-08), private attachments (D-18) and no legacy /api/chat/* routes (D-10).
|
||||
|
||||
Purpose: apps and the later Wavepath port call this prefix or thin-alias it; payloads share one serializer set.
|
||||
Output: an authenticated member can POST a message under /_chat/api/v1 and download an allowed private file; unauthorized ids 404.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/msd-core/workflows/execute-plan.md
|
||||
@~/.claude/msd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/19-chat-and-chatchannels/19-CONTEXT.md
|
||||
@.planning/phases/19-chat-and-chatchannels/19-RESEARCH.md
|
||||
@.planning/phases/19-chat-and-chatchannels/19-PATTERNS.md
|
||||
@.planning/phases/19-chat-and-chatchannels/19-01-PLAN.md
|
||||
|
||||
<interfaces>
|
||||
- ../sm-chat-plugin/classes/service.go (plan 01)
|
||||
- ../sm-paymentgatewaysubscriptions-plugin/routes.go (APIPrefix, jwt.auth group, Buckets)
|
||||
- ../sm-journal-plugin/routes.go and ../sm-journal-plugin/plugin.go Buckets
|
||||
- ../sm-paymentgateway-plugin/api/auth.go RequireJWT / frontendUser
|
||||
- ../sm-paymentgatewaysubscriptions-plugin/api/errors.go and api/json.go
|
||||
- modules/lagoon/attach/store.go Limits / Store; attach/static.go NewReader; attach/file.go DeleteForOwner
|
||||
- /media/nvme/dev/jz/wavepath.org/plugins/golem15/chat/classes/ChatAttachmentPolicy.php
|
||||
- Wavepath community chat operations (D-07): use RESEARCH operation lists, not stale routes.php line numbers (Pitfall 1)
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="checkpoint:decision" gate="blocking-human">
|
||||
<name>Task 1: Confirm the published /_chat/api/v1 contract (D-06)</name>
|
||||
<read_first>.planning/phases/19-chat-and-chatchannels/19-CONTEXT.md (D-06, D-07, D-08, D-10), .planning/phases/19-chat-and-chatchannels/19-RESEARCH.md (discretion paths, Q1, Pitfall 1, Pitfall 2, Pitfall 10)</read_first>
|
||||
<action>Print the path list from this plan's Decisions, then stop. Do not add routes.go yet. The sheet states: prefix /_chat/api/v1 is a published contract (D-06); operations are the D-06 chat list re-rooted from Wavepath community chat (D-07) with D-08 {type}/{id}, including PUT /threads/{rootId}/mute (Q1 RESOLVED: include because D-07 is locked; chat-owned store, not a Wavepath host table); friendship DMs stay on the Wavepath port; legacy /api/chat/* stay unregistered (D-10). Changing these paths after an app consumes them is a breaking API change.</action>
|
||||
<decision>Lock APIPrefix /_chat/api/v1 and the RESEARCH discretion paths, including D-07 thread mute, as the published chat API?</decision>
|
||||
<context>D-06 is one-way: apps will hard-code these paths. D-07's stale line numbers must not pull FriendGroup or forum routes into chat. Thread mute is chat-owned (golem15_chat_thread_mutes from plan 01); conversation mute is a different resource.</context>
|
||||
<options>
|
||||
<option id="proceed">
|
||||
<name>Lock the RESEARCH path list including PUT /threads/{rootId}/mute</name>
|
||||
<pros>Matches D-06 resources and D-07 thread mute without a Wavepath host table.</pros>
|
||||
<cons>Generic /_chat becomes the mute write the Wavepath port can alias.</cons>
|
||||
</option>
|
||||
<option id="abort">
|
||||
<name>Stop; do not publish the prefix</name>
|
||||
<pros>The contract stays unpublished.</pros>
|
||||
<cons>Plan 02 cannot start.</cons>
|
||||
</option>
|
||||
</options>
|
||||
<resume-signal>Reply proceed or abort.</resume-signal>
|
||||
<verify>
|
||||
<automated>test -f ../sm-chat-plugin/classes/service.go && grep -q 'func (s \*Service) SendMessage' ../sm-chat-plugin/classes/service.go && echo chat-service-ready</automated>
|
||||
<fails_when>non-zero exit or "chat-service-ready" absent (plan 01 did not land SendMessage)</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- SUMMARY records the reply verbatim.
|
||||
- After abort, ../sm-chat-plugin/routes.go does not exist.
|
||||
</acceptance_criteria>
|
||||
<done>The human locked the published chat prefix and path list before handlers were written.</done>
|
||||
</task>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 2: End-to-end POST one message on /_chat/api/v1</name>
|
||||
<reversibility rating="one-way">D-06 publishes /_chat/api/v1; apps will consume these paths and JSON keys.</reversibility>
|
||||
<precondition>Task 1 reply is proceed; ../sm-chat-plugin/classes/service.go exports SendMessage.</precondition>
|
||||
<files>../sm-chat-plugin/plugin.go, ../sm-chat-plugin/routes.go, ../sm-chat-plugin/api/json.go, ../sm-chat-plugin/api/errors.go, ../sm-chat-plugin/api/auth.go, ../sm-chat-plugin/api/conversations.go, ../sm-chat-plugin/api/messages.go, ../sm-chat-plugin/http_tracer_test.go, ../sm-chat-plugin/README.md</files>
|
||||
<read_first>../sm-chat-plugin/classes/service.go, ../sm-paymentgatewaysubscriptions-plugin/routes.go, ../sm-journal-plugin/routes.go, ../sm-paymentgateway-plugin/api/auth.go, ../sm-paymentgatewaysubscriptions-plugin/api/errors.go, ../sm-paymentgatewaysubscriptions-plugin/api/json.go, ../sm-chat-plugin/harness_test.go</read_first>
|
||||
<action>Per D-06, D-08 and D-09, mount one write path. routes.go: const APIPrefix = "/_chat/api/v1", APIBucket = "chat-api", DMBucket = "chat-dm". Plugin asserts pact.HasRoutes and surf.BucketProvider. Group APIPrefix with surf.Use("throttle:chat-api", "jwt.auth"). This tracer registers GET and POST `/contexts/{type}/{id}/conversation` and POST `/conversations/{id}/messages` (and GET messages if the send needs a conversation id from the first call). Handlers resolve {type} through contracts.Registry (unknown → 404), check HasMember / ChatEnabled / participation on every call, call classes.Service, write JSON with wire.WriteJSON. frontendUser refuses backend principals.
|
||||
|
||||
http_tracer_test.go TestChatHTTPTracer (Postgres, skip -short): boot the plan 01 host, mint a frontend JWT for a member of acme.teams, POST a text message, expect 200/201 and toApiArray keys id, conversation_id, sender_id, content, type, created_at, root_id; a stranger JWT on the same conversation id is 404; a missing conversation id is 404. TestChatHTTPAuth (short): a request without jwt.auth is 401.
|
||||
|
||||
README documents APIPrefix and that the app mounts the guard (D-09, lighthouse.Mount stays the app's job). Task 3 adds DM, replies, read, typing, conversation mute, thread mute and attachments. Do not register /api/chat/* (D-10).</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-chat-plugin vet ./... && go -C ../sm-chat-plugin test -count=1 -short ./...</automated>
|
||||
<fails_when>non-zero exit or a package reports FAIL</fails_when>
|
||||
<automated>docker info >/dev/null 2>&1 && go -C ../sm-chat-plugin test -count=1 -run '^TestChatHTTPTracer$' -v .</automated>
|
||||
<fails_when>non-zero exit: no Docker daemon (record NOT RUN in the SUMMARY), or the named test is missing, fails, or a stranger did not receive 404</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep -n 'const APIPrefix = "/_chat/api/v1"' ../sm-chat-plugin/routes.go prints one line.
|
||||
- grep -n 'jwt.auth' ../sm-chat-plugin/routes.go prints at least one line.
|
||||
- grep -vE '^#' ../sm-chat-plugin/routes.go ../sm-chat-plugin/api/*.go | grep -c '/api/chat/' prints 0.
|
||||
</acceptance_criteria>
|
||||
<done>An authenticated context member can POST one message under /_chat/api/v1 and a stranger sees 404.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: DMs, replies, read, typing, mute, thread mute, private attachments and chat-dm throttle</name>
|
||||
<reversibility rating="one-way">D-18 download URLs become the serializer attachment.url values apps will store and call.</reversibility>
|
||||
<files>../sm-chat-plugin/routes.go, ../sm-chat-plugin/api/dm.go, ../sm-chat-plugin/api/replies.go, ../sm-chat-plugin/api/read.go, ../sm-chat-plugin/api/typing.go, ../sm-chat-plugin/api/mute.go, ../sm-chat-plugin/api/threads.go, ../sm-chat-plugin/api/attachments.go, ../sm-chat-plugin/api/download.go, ../sm-chat-plugin/classes/attachment_policy.go, ../sm-chat-plugin/plugin.go, ../sm-chat-plugin/README.md, ../sm-chat-plugin/http_tracer_test.go</files>
|
||||
<read_first>../sm-chat-plugin/routes.go, ../sm-chat-plugin/classes/service.go, modules/lagoon/attach/store.go, modules/lagoon/attach/static.go, modules/lagoon/attach/file.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chat/classes/ChatAttachmentPolicy.php, ../sm-journal-plugin/classes/media_store.go, ../sm-paymentgatewaysubscriptions-plugin/routes.go</read_first>
|
||||
<action>Per D-06, D-07, D-09, D-10 and D-18, add the remaining generic operations. Nested group or per-route throttle:chat-dm on POST `/contexts/{type}/{id}/dm` (Max 10, Decay time.Minute). GetOrCreateDm is context-scoped (Pitfall 2). Replies POST/GET `/messages/{id}/replies` (root_id set; dispatch the plan 01 job). Read, typing and conversation mute call the service Emit helpers from 19-01. Edit and delete handlers call EditMessage / DeleteMessage (those already Fire MessageUpdated / MessageDeleted inside the write tx per D-13 / 19-01). Lighthouse Emit: deleted.chat.message on delete; updated.chat.message on edit if a Wavepath capture in plan 05 shows it (RESEARCH Q4 RESOLVED).
|
||||
|
||||
Per D-07, register PUT `/threads/{rootId}/mute` (jwt.auth, chat-api bucket). Handler reads muted from JSON as bool, never a user id from the body, calls Service.MuteThread with the jwt actor, writes `{root_id, is_muted}` via wire.WriteJSON, and collapses missing root / non-participant to 404. Conversation `/conversations/{id}/mute` stays ToggleMute. TestThreadMute (short): participant PUT muted true returns those keys; stranger on the same rootId is 404; conversation is_muted is unchanged.
|
||||
|
||||
Attachment policy (Pitfall 9): attach.Limits MaxBytes 102400*1024, Extensions = PHP KNOWN_EXTS (include oga/m4a/csv; do not use attach.DefaultFileExtensions), plus a wrapper that rejects BANNED_EXTS and BANNED_MIMES (svg, html, xml) after sniff. MaxAttachments 5. Store Public false. Pending field chat-pending-{actorID} (claiming another user's pending upload is refused). kind / width / height from attach metadata for images. Download: GET `/attachments/{messageId}/{fileId}` streams via bucket.NewReader after participation; missing and unauthorized 404 (PHP downloadAttachment). Content-Disposition attachment for unsafe types. Never mount chat blobs on attach.StaticHandler or StaticHandlerPublic. Two-phase delete: attach.DeleteForOwner then DeleteKeys after commit.
|
||||
|
||||
Tests (short where possible): TestDMThrottle (10th DM in a minute is 429; 422 CANNOT_CREATE_DM on service refusal), TestAttachmentPolicy (banned svg/html/xml by ext and MIME; sixth file refused; oversize refused), TestAttachmentDownload (participant 200 stream; stranger and missing 404). README lists the paths and the private-download rule.
|
||||
|
||||
Do not create a Wavepath host mute table. Do not port ChatWindow, ChatBadge or chat.js (D-10).</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-chat-plugin vet ./... && go -C ../sm-chat-plugin test -count=1 -short ./...</automated>
|
||||
<fails_when>non-zero exit or a package reports FAIL</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep -n 'chat-dm' ../sm-chat-plugin/routes.go ../sm-chat-plugin/plugin.go prints at least one line.
|
||||
- grep -n 'threads/{rootId}/mute' ../sm-chat-plugin/routes.go ../sm-chat-plugin/api/threads.go prints at least one line.
|
||||
- grep -n 'func TestThreadMute' ../sm-chat-plugin/http_tracer_test.go prints one line.
|
||||
- grep -n 'CANNOT_CREATE_DM' ../sm-chat-plugin/api/*.go prints at least one line.
|
||||
- grep -n 'MaxBytes' ../sm-chat-plugin/classes/attachment_policy.go prints at least one line.
|
||||
- grep -vE '^#' ../sm-chat-plugin/routes.go ../sm-chat-plugin/api/*.go | grep -c 'StaticHandler' prints 0.
|
||||
- grep -vE '^#' ../sm-chat-plugin/*.go ../sm-chat-plugin/updates/*.go | grep -c 'thread_subscriptions' prints 0.
|
||||
</acceptance_criteria>
|
||||
<done>The D-06 / D-07 chat API is complete: DMs are throttled, thread mute is chat-owned, attachments are private and participation-gated, and legacy /api/chat paths stay unregistered.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| client → /_chat/api/v1 | untrusted JSON, ids and multipart files cross jwt.auth |
|
||||
| download handler → blob store | private system_files bytes leave the bucket only after a participation check |
|
||||
| DM start → other users | unsolicited conversation creation |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-19-01 | Information disclosure | api/download.go, message GET | high | mitigate | Missing and unauthorized collapse to 404; TestAttachmentDownload |
|
||||
| T-19-02 | Elevation of privilege | api/dm.go | medium | mitigate | chat-dm 10/min; 422 CANNOT_CREATE_DM; context-scoped GetOrCreateDm |
|
||||
| T-19-03 | Tampering | classes/attachment_policy.go | high | mitigate | Allowlist + banned MIME; max 5; 100 MB; TestAttachmentPolicy |
|
||||
| T-19-09 | Elevation of privilege | api/attachments.go pending field | high | mitigate | field chat-pending-{actorID}; refuse another actor's pending row |
|
||||
| T-19-SC | Tampering | go.mod requires | high | mitigate | No new packages |
|
||||
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
go -C ../sm-chat-plugin vet ./... and go -C ../sm-chat-plugin test -count=1 -short ./... stay green. TestChatHTTPTracer (Docker) proves one authenticated send. Short tests cover throttle, policy and 404 collapse.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
/_chat/api/v1 serves the D-06 chat operations with jwt.auth, named buckets, context addressing, private attachments and no legacy /api/chat routes.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/19-chat-and-chatchannels/19-02-SUMMARY.md` when done
|
||||
</output>
|
||||
263
.planning/phases/19-chat-and-chatchannels/19-03-PLAN.md
Normal file
263
.planning/phases/19-chat-and-chatchannels/19-03-PLAN.md
Normal file
@@ -0,0 +1,263 @@
|
||||
---
|
||||
phase: 19-chat-and-chatchannels
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["19-01"]
|
||||
files_modified:
|
||||
- ../sm-chatchannels-plugin/.gitignore
|
||||
- ../sm-chatchannels-plugin/go.mod
|
||||
- ../sm-chatchannels-plugin/go.sum
|
||||
- ../sm-chatchannels-plugin/go.work
|
||||
- ../sm-chatchannels-plugin/README.md
|
||||
- ../sm-chatchannels-plugin/plugin.go
|
||||
- ../sm-chatchannels-plugin/boot.go
|
||||
- ../sm-chatchannels-plugin/config/config.yaml
|
||||
- ../sm-chatchannels-plugin/lang/en/lang.yaml
|
||||
- ../sm-chatchannels-plugin/lang/pl/lang.yaml
|
||||
- ../sm-chatchannels-plugin/contracts/access_resolver.go
|
||||
- ../sm-chatchannels-plugin/classes/deny_all.go
|
||||
- ../sm-chatchannels-plugin/classes/channel_service.go
|
||||
- ../sm-chatchannels-plugin/classes/mention_parser.go
|
||||
- ../sm-chatchannels-plugin/classes/search_indexer.go
|
||||
- ../sm-chatchannels-plugin/search.go
|
||||
- ../sm-chatchannels-plugin/models/channel.go
|
||||
- ../sm-chatchannels-plugin/models/category.go
|
||||
- ../sm-chatchannels-plugin/models/personal_category.go
|
||||
- ../sm-chatchannels-plugin/models/prefs.go
|
||||
- ../sm-chatchannels-plugin/models/member.go
|
||||
- ../sm-chatchannels-plugin/models/mention.go
|
||||
- ../sm-chatchannels-plugin/models/pin.go
|
||||
- ../sm-chatchannels-plugin/models/user_context_pref.go
|
||||
- ../sm-chatchannels-plugin/models/message_index.go
|
||||
- ../sm-chatchannels-plugin/models/registry.go
|
||||
- ../sm-chatchannels-plugin/updates/registry.go
|
||||
- ../sm-chatchannels-plugin/updates/202610160001_create_golem15_chatchannels_baseline.go
|
||||
- ../sm-chatchannels-plugin/console/rewrite_context_types.go
|
||||
- ../sm-chatchannels-plugin/testmain_test.go
|
||||
- ../sm-chatchannels-plugin/harness_test.go
|
||||
- ../sm-chatchannels-plugin/fixture_plugin_test.go
|
||||
- ../sm-chatchannels-plugin/tracer_test.go
|
||||
autonomous: true
|
||||
requirements: [SC-19-1, SC-19-2, D-04, D-12, D-13, D-14, D-20]
|
||||
estimate:
|
||||
tokens: 200000
|
||||
raw_tokens: 200000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "D-01 / D-04: module git.golem15.com/golem15/sm-chatchannels-plugin, package chatchannels, PluginID golem15.chatchannels, Requires golem15.chat and golem15.user; baseline 202610160001_create_golem15_chatchannels_baseline creates the eight PHP v1.6.0 tables with IF NOT EXISTS and never adds users.handle (D-15)."
|
||||
- "D-14: AccessResolver CanAccessContext / CanManageContext default false; SetAccessResolver installs the host; Boot slog.Warn when the default remains."
|
||||
- "D-13: Boot listens to chat.MessageCreated, MessageUpdated, MessageDeleted; listeners accrue / reconcile / remove mentions and call the search indexer; chatchannels never imports chat models to patch them."
|
||||
- "D-20: MessageIndex.SearchableAs returns messages (no scout prefix); document fields id, content, conversation_id, sender_id, type, created_at Unix; ShouldBeSearchable excludes TYPE_SYSTEM and empty content; indexer fail-open (Warn, send still succeeds)."
|
||||
- "D-12: rewrite of golem15_chatchannels_channels.context_type uses the same LegacyPHPClass map as chat; command or shared helper is idempotent."
|
||||
artifacts:
|
||||
- path: "../sm-chatchannels-plugin/plugin.go"
|
||||
provides: "golem15.chatchannels plugin"
|
||||
contains: "golem15.chatchannels"
|
||||
- path: "../sm-chatchannels-plugin/updates/202610160001_create_golem15_chatchannels_baseline.go"
|
||||
provides: "idempotent v1.6.0 channel tables"
|
||||
contains: "golem15_chatchannels_channels"
|
||||
- path: "../sm-chatchannels-plugin/classes/channel_service.go"
|
||||
provides: "ChannelService successor"
|
||||
contains: "CreateChannel"
|
||||
- path: "../sm-chatchannels-plugin/classes/search_indexer.go"
|
||||
provides: "fail-open beachcomber Sync/Remove"
|
||||
contains: "SearchableAs"
|
||||
key_links:
|
||||
- from: "../sm-chatchannels-plugin/boot.go"
|
||||
to: "../sm-chat-plugin/events/events.go"
|
||||
via: "Listen MessageCreated/Updated/Deleted"
|
||||
pattern: "MessageCreated"
|
||||
- from: "../sm-chatchannels-plugin/classes/channel_service.go"
|
||||
to: "../sm-chatchannels-plugin/contracts/access_resolver.go"
|
||||
via: "every access check goes through the resolver; default deny-all"
|
||||
pattern: "CanAccessContext"
|
||||
- from: "../sm-chatchannels-plugin/classes/search_indexer.go"
|
||||
to: "modules/beachcomber"
|
||||
via: "SearchableAs messages; fail-open Sync"
|
||||
pattern: "\"messages\""
|
||||
prohibitions:
|
||||
- statement: "users.handle is not a chatchannels migration (D-15)."
|
||||
status: unresolved
|
||||
verification: grep
|
||||
- statement: "Searchable is not implemented on chat.models.Message (D-13)."
|
||||
status: unresolved
|
||||
verification: grep
|
||||
- statement: "Production sources never contain the four D-02 tokens; README names only acme / blog."
|
||||
status: unresolved
|
||||
verification: grep
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As a** application developer, **I want to** have Chat and ChatChannels run on SummerCMS as shared plugins with realtime through lighthouse, **so that** they are ready for the Wavepath port.
|
||||
|
||||
This plan's slice (plan 3 of 5, wave 2, parallel with 19-02): the `golem15.chatchannels` core, proven by one CreateChannel that backs a conversation, then expanded to the full ChannelService, mention listeners and fail-open search.
|
||||
|
||||
## Spec-less probe fallback
|
||||
|
||||
Skipped visibly: see 19-01. No new probe predicates.
|
||||
|
||||
## Decisions taken in this plan (Claude's discretion)
|
||||
|
||||
- Package `chatchannels`, Winter directories, service `classes.ChannelService` published at Boot.
|
||||
- AccessResolver interface matches PHP; default `classes.DenyAll`.
|
||||
- MentionParser ports `'/(?<![\w.@])@([\w.-]{2,255})/u'` against `users.handle` (column lands in 19-04; parser and participant-scoped resolve land here so 19-04 only adds the generator).
|
||||
- SearchableAs `"messages"`; tests use `SUMMER_SEARCH__DRIVER=null`.
|
||||
- D-12 channel rewrite: extend chat's command or add `chatchannels:rewrite-context-types` that UPDATEs `golem15_chatchannels_channels.context_type` from LegacyPHPClass (19-01 already locked rewrite-as-both).
|
||||
- Remote: reuse the 19-01 Task 1 reply (proceed-and-push vs proceed-local). Do not re-ask.
|
||||
|
||||
## Artifacts this phase produces (this plan)
|
||||
|
||||
- New `../sm-chatchannels-plugin` with analog go.mod/go.work (replaces summercms, sm-chat-plugin, sm-user-plugin).
|
||||
- Baseline `202610160001_create_golem15_chatchannels_baseline`.
|
||||
- ChannelService public methods from RESEARCH, deny-all resolver, festival listeners, MentionParser, fail-open MessageIndex, acme.teams host, TestCreateChannel tracer.
|
||||
|
||||
<objective>
|
||||
Create sm-chatchannels-plugin with the v1.6.0 baseline (D-04), deny-all ChannelAccessResolver (D-14), ChannelService, festival listeners on chat message events (D-13) and beachcomber MessageIndex (D-20).
|
||||
|
||||
Purpose: plan 04 mounts /_chatchannels/api/v1 on this service; mentions and search must already react to chat writes.
|
||||
Output: CreateChannel persists a channel and backing conversation; a MessageCreated listener runs without failing the send when search is down.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/msd-core/workflows/execute-plan.md
|
||||
@~/.claude/msd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/19-chat-and-chatchannels/19-CONTEXT.md
|
||||
@.planning/phases/19-chat-and-chatchannels/19-RESEARCH.md
|
||||
@.planning/phases/19-chat-and-chatchannels/19-PATTERNS.md
|
||||
@.planning/phases/19-chat-and-chatchannels/19-01-PLAN.md
|
||||
|
||||
Do not wait for 19-02. Do not edit ../sm-chat-plugin (D-13: listen, do not patch).
|
||||
|
||||
<interfaces>
|
||||
- ../sm-chat-plugin/events/events.go, contracts/context.go, classes/service.go (plan 01)
|
||||
- ../sm-paymentgatewaysubscriptions-plugin/{plugin,boot,updates,fixture_plugin_test}.go
|
||||
- ../sm-journal-plugin/search.go and models/post_search.go
|
||||
- modules/beachcomber/searchable.go (SearchableAs without prefix) and sync.go (fail-open Warn)
|
||||
- /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatchannels @ b92fb42 (ChannelService.php, MentionParser, SearchIndexer, MessageIndex, DenyAllChannelAccessResolver, updates/)
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: End-to-end CreateChannel — repo, baseline, deny-all resolver, backing conversation</name>
|
||||
<reversibility rating="one-way">D-04 chatchannels migration id and table names become the shared contract; D-12 will rewrite channels.context_type on adoption.</reversibility>
|
||||
<precondition>../sm-chat-plugin/events/events.go exports MessageCreated; 19-01 Task 1 was not abort.</precondition>
|
||||
<files>../sm-chatchannels-plugin/go.mod, ../sm-chatchannels-plugin/go.work, ../sm-chatchannels-plugin/go.sum, ../sm-chatchannels-plugin/.gitignore, ../sm-chatchannels-plugin/README.md, ../sm-chatchannels-plugin/plugin.go, ../sm-chatchannels-plugin/boot.go, ../sm-chatchannels-plugin/config/config.yaml, ../sm-chatchannels-plugin/lang/en/lang.yaml, ../sm-chatchannels-plugin/lang/pl/lang.yaml, ../sm-chatchannels-plugin/contracts/access_resolver.go, ../sm-chatchannels-plugin/classes/deny_all.go, ../sm-chatchannels-plugin/classes/channel_service.go, ../sm-chatchannels-plugin/models/channel.go, ../sm-chatchannels-plugin/models/registry.go, ../sm-chatchannels-plugin/updates/registry.go, ../sm-chatchannels-plugin/updates/202610160001_create_golem15_chatchannels_baseline.go, ../sm-chatchannels-plugin/testmain_test.go, ../sm-chatchannels-plugin/harness_test.go, ../sm-chatchannels-plugin/fixture_plugin_test.go, ../sm-chatchannels-plugin/tracer_test.go</files>
|
||||
<read_first>../sm-chat-plugin/plugin.go, ../sm-chat-plugin/events/events.go, ../sm-paymentgatewaysubscriptions-plugin/go.mod, ../sm-paymentgatewaysubscriptions-plugin/go.work, ../sm-paymentgatewaysubscriptions-plugin/updates/202610150001_create_golem15_pgsub_baseline.go, ../sm-paymentgatewaysubscriptions-plugin/testmain_test.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatchannels/classes/ChannelService.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatchannels/classes/DenyAllChannelAccessResolver.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatchannels/updates/</read_first>
|
||||
<action>Per D-04 and D-14, create ../sm-chatchannels-plugin beside summercms.go. Analog pins only (Go 1.27.0, gorm v1.31.2, gormigrate v2.1.7, pgx v5.10.0, testcontainers v0.44.0). require summercms, sm-user-plugin, sm-chat-plugin. go.work replaces summercms => ../summercms.go, sm-chat-plugin => ../sm-chat-plugin, sm-user-plugin => ../fonoteka.go/plugins/golem15/user.
|
||||
|
||||
plugin.go: PluginID golem15.chatchannels, Requires golem15.chat and golem15.user, embed config/lang, pact.HasConfig/Migrations/Models/Lang (HasRoutes in plan 04). README neutral names, display name Chat Channels / Kanały czatu.
|
||||
|
||||
Baseline 202610160001_create_golem15_chatchannels_baseline: the eight tables from RESEARCH Frozen PHP inventory (channel_categories, channels, personal_categories, channel_user_prefs, channel_members, channel_mentions, channel_pins, user_context_prefs) with PHP names, unique keys and FK notes (no FK to messages; pins have no FK to channel/message; last_active_channel_id no FK). Visibility CHECK public|private; notify_pref VARCHAR(16) default mentions. Do not add users.handle. Do not add the D-05 column.
|
||||
|
||||
contracts.AccessResolver: CanAccessContext, CanManageContext. classes.DenyAll returns false, false. ChannelService.Options include Resolver; New default DenyAll. This tracer implements CreateChannel (PHP createChannel): persist Channel with context_type alias + context_id, create backing Conversation with the same context (Pitfall 6: context_type is the channel alias the host registers — test fixture registers alias channel or uses acme.teams as the channel owner), unique conversation_id. AdoptConversation if PHP does that on create. Access checks go through the resolver; the tracer test installs an allow-all fixture resolver so CreateChannel can run (default deny-all would block). Boot: if resolver is still DenyAll, slog.Warn (D-14).
|
||||
|
||||
Test host: copy chat's testmain (flag.Parse + testing.Short); harness activates golem15.user, golem15.chat, golem15.chatchannels, acme.teams; SUMMER_REALTIME__DRIVER=memory, SUMMER_SEARCH__DRIVER=null. Fixture registers a chat.Context for acme.teams and a channel-as-context adapter so later broadcasts resolve (Pitfall 6). TestFixtureAbsentFromProduction greps the four D-02 tokens and acme.teams in production sources. tracer_test.go TestCreateChannel (Postgres): allow-all resolver, CreateChannel, row in golem15_chatchannels_channels, backing conversation_id set, unique (context_type, context_id, slug). TestAccessResolverDefault (short): DenyAll returns false.
|
||||
|
||||
Task 2 adds the rest of ChannelService. Task 3 adds listeners and search. Do not add HTTP. Follow the 19-01 remote reply for tea/push at the end of Task 3.</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-chatchannels-plugin vet ./... && go -C ../sm-chatchannels-plugin test -count=1 -short ./...</automated>
|
||||
<fails_when>non-zero exit or a package reports FAIL</fails_when>
|
||||
<automated>docker info >/dev/null 2>&1 && go -C ../sm-chatchannels-plugin test -count=1 -run '^TestCreateChannel$' -v .</automated>
|
||||
<fails_when>non-zero exit: no Docker daemon (record NOT RUN in the SUMMARY), or TestCreateChannel is missing, fails, or conversation_id is null</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep -v '^#' ../sm-chatchannels-plugin/plugin.go | grep -c 'golem15.chatchannels' prints at least 1.
|
||||
- grep -v '^#' ../sm-chatchannels-plugin/updates/202610160001_create_golem15_chatchannels_baseline.go | grep -c 'golem15_chatchannels_channels' prints at least 1.
|
||||
- grep -v '^#' ../sm-chatchannels-plugin/updates/*.go | grep -c 'users.handle\|ADD COLUMN.*handle' prints 0.
|
||||
- grep -n 'CanAccessContext' ../sm-chatchannels-plugin/contracts/access_resolver.go prints at least one line.
|
||||
</acceptance_criteria>
|
||||
<done>CreateChannel writes a channel row and a backing conversation under an installed resolver; Boot warns when the resolver is still deny-all.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Expand ChannelService to the PHP public method set</name>
|
||||
<reversibility rating="costly">ChannelService is the typed API Wavepath /_wave aliases will call; method names and error sentinels become a cross-plugin contract.</reversibility>
|
||||
<files>../sm-chatchannels-plugin/classes/channel_service.go, ../sm-chatchannels-plugin/models/category.go, ../sm-chatchannels-plugin/models/personal_category.go, ../sm-chatchannels-plugin/models/prefs.go, ../sm-chatchannels-plugin/models/member.go, ../sm-chatchannels-plugin/models/mention.go, ../sm-chatchannels-plugin/models/pin.go, ../sm-chatchannels-plugin/models/user_context_pref.go, ../sm-chatchannels-plugin/README.md</files>
|
||||
<read_first>../sm-chatchannels-plugin/classes/channel_service.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatchannels/classes/ChannelService.php</read_first>
|
||||
<action>Per D-14, port every public ChannelService method listed in RESEARCH: CreateChannel (tracer), AdoptConversation, ChannelsFor, CanAccessChannel, CreateCategory, AddMember, EditChannel, SetVisibility, ArchiveChannel, RestoreChannel, RemoveMember, SeverUserFromContext, RenameCategory, DeleteCategory, SetPreference, SetMuted, SetNotifyPref, SetCategoryCollapsed, PersonalCategoriesFor, CreatePersonalCategory, RenamePersonalCategory, DeletePersonalCategory, JoinChannel, LeaveChannel, SetLastActiveChannel, LastActiveChannel, UnreadCountForContext, PreferencesFor, MembersFor, AccrueForMessage, PublishMentionsChanged, ReconcileForMessage, RemoveForMessage, RecordMentions, ReconcileMentions, RemoveMentions, PinMessage, UnpinMessage, PinsFor, IsPinned, PinCountFor, RemovePinsForMessage, MentionCountFor, MentionCountForContext.
|
||||
|
||||
Writes use lagoon.Transaction. Visibility public|private. notify_pref all|mentions|none; column default mentions; absent pref row reads as all (PHP). PublishMentionsChanged Emits mentions.changed on user:{id} with action mentions (D-16 / RESEARCH event table). Mentions resolve handles against that conversation's participants and exclude the sender (threat table); parser lands in Task 3 but AccrueForMessage can accept pre-parsed user ids until then. SeverUserFromContext deletes prefs, members and personal categories for GDPR (plan 04 fires the user event).
|
||||
|
||||
README API reference lists the service methods. Do not patch chat.Message.</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-chatchannels-plugin vet ./... && go -C ../sm-chatchannels-plugin test -count=1 -short ./...</automated>
|
||||
<fails_when>non-zero exit or a package reports FAIL</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep -c 'func (s \*ChannelService) ' ../sm-chatchannels-plugin/classes/channel_service.go is at least 30.
|
||||
- grep -n 'mentions.changed' ../sm-chatchannels-plugin/classes/channel_service.go prints at least one line.
|
||||
- grep -n 'SeverUserFromContext' ../sm-chatchannels-plugin/classes/channel_service.go prints at least one line.
|
||||
</acceptance_criteria>
|
||||
<done>ChannelService mirrors the PHP public surface; mention publish and GDPR sever exist for plan 04 to hook.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Festival listeners, MentionParser, fail-open search and D-12 channel rewrite</name>
|
||||
<reversibility rating="one-way">D-12 rewrite of channels.context_type is the same cutover as chat conversations; D-20 collection name messages is a live Typesense contract.</reversibility>
|
||||
<files>../sm-chatchannels-plugin/boot.go, ../sm-chatchannels-plugin/classes/mention_parser.go, ../sm-chatchannels-plugin/classes/search_indexer.go, ../sm-chatchannels-plugin/search.go, ../sm-chatchannels-plugin/models/message_index.go, ../sm-chatchannels-plugin/console/rewrite_context_types.go, ../sm-chatchannels-plugin/plugin.go, ../sm-chatchannels-plugin/README.md</files>
|
||||
<read_first>../sm-chatchannels-plugin/boot.go, ../sm-journal-plugin/search.go, ../sm-journal-plugin/models/post_search.go, modules/beachcomber/searchable.go, modules/beachcomber/sync.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatchannels/classes/MentionParser.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatchannels/models/MessageIndex.php, ../sm-chat-plugin/console/rewrite_context_types.go</read_first>
|
||||
<action>Per D-13, D-12 and D-20, Boot Listen[*chat/events.MessageCreated], MessageUpdated, MessageDeleted. Created: AccrueForMessage + indexer Sync. Updated: ReconcileForMessage + Sync. Deleted: RemoveForMessage + RemovePinsForMessage + indexer Remove. Listener errors that are search failures must not fail the chat write (fail-open, log Warn). Mention parse errors that are programming errors may fail the write; keep the PHP fail-open intent for search only.
|
||||
|
||||
MentionParser: port the PHP regex with a bounded {2,255} class, no nested quantifiers (ReDoS). Resolve against users.handle once 19-04 adds the column; until then resolve against a handle field if present else skip silently so the listener compiles. Identity column is handle, not username.
|
||||
|
||||
MessageIndex: SearchableAs returns "messages" (not prefix+messages). Fields: id string, content string, conversation_id int, sender_id int, type string, created_at int Unix. ShouldBeSearchable: not TYPE_SYSTEM, content non-empty. default_sorting_field created_at. search.go wires beachcomber.From; do not blank-import typesense in production plugin sources (host / test only). Tests use null driver. TestSearchIndexer (short): SearchableAs is messages; a forced engine error leaves SendMessage successful and logs Warn.
|
||||
|
||||
D-12: chatchannels:rewrite-context-types (or a shared helper called from chat's command) UPDATEs golem15_chatchannels_channels.context_type from each LegacyPHPClass. Idempotent. Do not rewrite attachment_type.
|
||||
|
||||
If 19-01 replied proceed-and-push: tea repos create --owner golem15 --name sm-chatchannels-plugin and push main after verify (never force). If proceed-local: no remote.</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-chatchannels-plugin vet ./... && go -C ../sm-chatchannels-plugin test -count=1 -short ./...</automated>
|
||||
<fails_when>non-zero exit or a package reports FAIL</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep -n 'SearchableAs' ../sm-chatchannels-plugin/models/message_index.go ../sm-chatchannels-plugin/classes/search_indexer.go prints at least one line that includes messages.
|
||||
- grep -n 'Listen\[' ../sm-chatchannels-plugin/boot.go prints at least one line.
|
||||
- grep -n '@' ../sm-chatchannels-plugin/classes/mention_parser.go prints at least one line.
|
||||
- grep -vE '^#' ../sm-chat-plugin/models/*.go | grep -c 'SearchableAs' prints 0.
|
||||
</acceptance_criteria>
|
||||
<done>Chat writes notify chatchannels through festival; mentions parse bounded handles; search is fail-open on collection messages.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| ChannelService → resolver | host-installed access policy; default deny-all |
|
||||
| festival Message* → mention / search | chat write tx fans out to chatchannels |
|
||||
| MentionParser → user handles | untrusted message body scanned for @tokens |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-19-06 | Elevation of privilege | classes/mention_parser.go | high | mitigate | Resolve handles against that conversation's participants; exclude sender |
|
||||
| T-19-08 | Denial of service | classes/search_indexer.go | medium | mitigate | Fail-open Warn; send succeeds when the engine errors (TestSearchIndexer) |
|
||||
| T-19-11 | Denial of service | mention regex | medium | mitigate | Bounded {2,255}, no nested quantifiers |
|
||||
| T-19-12 | Elevation of privilege | DenyAll + Boot Warn | medium | mitigate | Default both methods false; Boot Warn until the host sets a resolver |
|
||||
| T-19-SC | Tampering | go.mod requires | high | mitigate | Analog pins only; no new packages |
|
||||
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
go -C ../sm-chatchannels-plugin vet ./... and go -C ../sm-chatchannels-plugin test -count=1 -short ./... stay green. TestCreateChannel (Docker) proves one channel + backing conversation. TestSearchIndexer proves fail-open.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
sm-chatchannels-plugin exists with the v1.6.0 baseline, deny-all resolver, full ChannelService, festival listeners and fail-open MessageIndex. users.handle is not in this repo's migrations. summercms.go modules are untouched.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/19-chat-and-chatchannels/19-03-SUMMARY.md` when done
|
||||
</output>
|
||||
266
.planning/phases/19-chat-and-chatchannels/19-04-PLAN.md
Normal file
266
.planning/phases/19-chat-and-chatchannels/19-04-PLAN.md
Normal file
@@ -0,0 +1,266 @@
|
||||
---
|
||||
phase: 19-chat-and-chatchannels
|
||||
plan: 04
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: ["19-03"]
|
||||
files_modified:
|
||||
- ../fonoteka.go/plugins/golem15/user/models/user.go
|
||||
- ../fonoteka.go/plugins/golem15/user/updates/202610160002_add_handle_to_users.go
|
||||
- ../fonoteka.go/plugins/golem15/user/updates/registry.go
|
||||
- ../fonoteka.go/plugins/golem15/user/classes/handle_generator.go
|
||||
- ../fonoteka.go/plugins/golem15/user/classes/events.go
|
||||
- ../fonoteka.go/plugins/golem15/user/plugin.go
|
||||
- ../fonoteka.go/plugins/golem15/user/boot.go
|
||||
- ../fonoteka.go/plugins/golem15/user/controllers/registration.go
|
||||
- ../fonoteka.go/plugins/golem15/user/controllers/users_admin_controller.go
|
||||
- ../fonoteka.go/plugins/golem15/user/console/backfill_handles.go
|
||||
- ../fonoteka.go/plugins/golem15/user/README.md
|
||||
- ../sm-chatchannels-plugin/routes.go
|
||||
- ../sm-chatchannels-plugin/plugin.go
|
||||
- ../sm-chatchannels-plugin/boot.go
|
||||
- ../sm-chatchannels-plugin/api/json.go
|
||||
- ../sm-chatchannels-plugin/api/errors.go
|
||||
- ../sm-chatchannels-plugin/api/auth.go
|
||||
- ../sm-chatchannels-plugin/api/channels.go
|
||||
- ../sm-chatchannels-plugin/api/categories.go
|
||||
- ../sm-chatchannels-plugin/api/prefs.go
|
||||
- ../sm-chatchannels-plugin/api/members.go
|
||||
- ../sm-chatchannels-plugin/api/pins.go
|
||||
- ../sm-chatchannels-plugin/api/mentions.go
|
||||
- ../sm-chatchannels-plugin/api/search.go
|
||||
- ../sm-chatchannels-plugin/README.md
|
||||
- ../sm-chatchannels-plugin/http_tracer_test.go
|
||||
- ../sm-chatchannels-plugin/classes/mention_parser.go
|
||||
autonomous: false
|
||||
requirements: [SC-19-1, D-06, D-07, D-08, D-09, D-13, D-15, D-17]
|
||||
estimate:
|
||||
tokens: 200000
|
||||
raw_tokens: 200000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "D-15: users.handle VARCHAR(255) NULL plus unique index users_handle_unique, added IF NOT EXISTS; HandleGenerator charset is a subset of [A-Za-z0-9._-]; MentionParser @([\\w.-]{2,255}) resolves against handle among that conversation's participants."
|
||||
- "D-13: user.Created and user.Deleting festival events fire from every user create path and from FormAfterDelete before unscoped delete, inside the same transaction; chatchannels listens Deleting and calls SeverUserFromContext."
|
||||
- "D-17: sm-user-plugin Boot registers lighthouse namespace user if free; user:{id} allowed only to that user. chat:message, chat:unread and mentions.changed publish there."
|
||||
- "D-06 / D-08 / D-09: APIPrefix /_chatchannels/api/v1, jwt.auth, bucket chatchannels-api 120/min, contexts at /contexts/{type}/{id}/…; operations are ChannelService resources (channels, categories, personal categories, prefs, members, pins, mentions, search)."
|
||||
artifacts:
|
||||
- path: "../fonoteka.go/plugins/golem15/user/updates/202610160002_add_handle_to_users.go"
|
||||
provides: "idempotent handle column on users"
|
||||
contains: "ADD COLUMN IF NOT EXISTS handle"
|
||||
- path: "../fonoteka.go/plugins/golem15/user/classes/handle_generator.go"
|
||||
provides: "unique handle generation"
|
||||
contains: "HandleGenerator"
|
||||
- path: "../sm-chatchannels-plugin/routes.go"
|
||||
provides: "APIPrefix /_chatchannels/api/v1"
|
||||
contains: "/_chatchannels/api/v1"
|
||||
key_links:
|
||||
- from: "../fonoteka.go/plugins/golem15/user/plugin.go"
|
||||
to: "modules/lighthouse"
|
||||
via: "Boot Registry.Register user if the namespace is free"
|
||||
pattern: "Register\\(\"user\""
|
||||
- from: "../sm-chatchannels-plugin/boot.go"
|
||||
to: "../fonoteka.go/plugins/golem15/user/classes/events.go"
|
||||
via: "Listen user.Deleting then SeverUserFromContext"
|
||||
pattern: "Deleting"
|
||||
- from: "../sm-chatchannels-plugin/api/channels.go"
|
||||
to: "../sm-chatchannels-plugin/classes/channel_service.go"
|
||||
via: "handlers call ChannelService only"
|
||||
pattern: "Lookup\\[\\*classes.ChannelService\\]"
|
||||
prohibitions:
|
||||
- statement: "Handle is additive; User.MorphName stays Golem15\\User\\Models\\User; handle is omitted from user API payloads unless a golden requires it."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
- statement: "Plugins never patch each other's models (D-13). No User::extend / Message::extend."
|
||||
status: unresolved
|
||||
verification: grep
|
||||
- statement: "Commit and push sm-user-plugin first; app submodule pointer bumps are out of this plan unless the human asks."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As a** application developer, **I want to** have Chat and ChatChannels run on SummerCMS as shared plugins with realtime through lighthouse, **so that** they are ready for the Wavepath port.
|
||||
|
||||
This plan's slice (plan 4 of 5, wave 3): `users.handle` plus the user lighthouse authorizer and lifecycle events in sm-user-plugin, proven by one @handle mention, then the `/_chatchannels/api/v1` surface.
|
||||
|
||||
## Spec-less probe fallback
|
||||
|
||||
Skipped visibly: see 19-01.
|
||||
|
||||
## Decisions taken in this plan (Claude's discretion)
|
||||
|
||||
- User events named `user.Created` / `user.Deleting` (RESEARCH A6) with exported `CreatedEventName` / `DeletingEventName`.
|
||||
- Handle migration id `202610160002_add_handle_to_users` (analog stripe_customer_id). Rollback no-op.
|
||||
- Optional `user:backfill-handles` for existing rows.
|
||||
- Channel API paths: `/_chatchannels/api/v1/contexts/{type}/{id}/channels` plus CommunityChannelController operations (categories, personal-categories, last-active, members, prefs, notify-pref, join/leave, pins, search) re-rooted off `{type}/{id}` not `{groupId}`.
|
||||
- User plugin checkout is `../fonoteka.go/plugins/golem15/user` (origin `git.golem15.com/golem15/sm-user-plugin`). Edit that tree; do not invent a sibling clone.
|
||||
|
||||
## Artifacts this phase produces (this plan)
|
||||
|
||||
- Additive sm-user-plugin: Handle field, migration, HandleGenerator, user authorizer, Created/Deleting events, optional backfill command.
|
||||
- `/_chatchannels/api/v1` handlers + TestChannelHTTPTracer.
|
||||
- MentionParser wired to users.handle.
|
||||
|
||||
<objective>
|
||||
Add users.handle and the user namespace authorizer to sm-user-plugin (D-15, D-17), fire user lifecycle festival events (D-13), and publish the D-06 chatchannels generic API.
|
||||
|
||||
Purpose: @mentions and GDPR delete need the user-plugin contract; the Wavepath port aliases /_chatchannels the same way it aliases /_chat.
|
||||
Output: creating a user yields a unique handle; @that-handle in a channel message accrues a mention; deleting the user severs channel rows; GET/POST channels work under jwt.auth.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/msd-core/workflows/execute-plan.md
|
||||
@~/.claude/msd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/19-chat-and-chatchannels/19-CONTEXT.md
|
||||
@.planning/phases/19-chat-and-chatchannels/19-RESEARCH.md
|
||||
@.planning/phases/19-chat-and-chatchannels/19-PATTERNS.md
|
||||
@.planning/phases/19-chat-and-chatchannels/19-03-PLAN.md
|
||||
|
||||
sm-user-plugin is a core plugin: additive only, non-breaking (.planning/notes/core-plugins-own-repos.md). Another session may edit the same submodule — run git status --porcelain on that path before Task 2 and stop if it is dirty with work that is not yours.
|
||||
|
||||
<interfaces>
|
||||
- ../fonoteka.go/plugins/golem15/user/models/user.go (MorphName, Fillable)
|
||||
- ../fonoteka.go/plugins/golem15/user/classes/events.go (RegisterEvent)
|
||||
- ../fonoteka.go/plugins/golem15/user/controllers/registration.go FireRegisterEvent
|
||||
- ../fonoteka.go/plugins/golem15/user/controllers/users_admin_controller.go FormAfterDelete
|
||||
- ../sm-paymentgatewaysubscriptions-plugin/updates/202610150002_add_stripe_customer_id_to_users.go
|
||||
- ../sm-chatchannels-plugin/classes/channel_service.go (plan 03)
|
||||
- ../sm-chat-plugin/routes.go (plan 02 analog for prefix and buckets)
|
||||
- modules/lighthouse/README.md Registry.Register; ChannelID is correct for user:{id} (segment 1)
|
||||
- PHP HandleGenerator in /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatchannels
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="checkpoint:decision" gate="blocking-human">
|
||||
<name>Task 1: Confirm users.handle on the shared users table (D-15) and the /_chatchannels prefix (D-06)</name>
|
||||
<read_first>.planning/phases/19-chat-and-chatchannels/19-CONTEXT.md (D-15, D-17, D-06), ../fonoteka.go/plugins/golem15/user/models/user.go, ../sm-paymentgatewaysubscriptions-plugin/updates/202610150002_add_stripe_customer_id_to_users.go</read_first>
|
||||
<action>Print the sheet, then stop. Do not edit the user plugin yet. The sheet states: D-15 adds a unique nullable handle column to the shared users table of every app that mounts sm-user-plugin (costly / treated one-way here); generation and backfill live in the user plugin; chatchannels only reads handle for mentions; D-17 registers lighthouse user if free; D-06 publishes /_chatchannels/api/v1 with the RESEARCH path list. Confirm the checkout path is ../fonoteka.go/plugins/golem15/user and that git status there is clean enough to edit.</action>
|
||||
<decision>Add users.handle (unique, IF NOT EXISTS) to sm-user-plugin and publish /_chatchannels/api/v1 as specified?</decision>
|
||||
<context>Every app that migrates sm-user-plugin gains the column. Removing it later needs a coordinated migration. The chatchannels prefix is a published API (same door as 19-02's /_chat prefix, now for the second plugin).</context>
|
||||
<options>
|
||||
<option id="proceed">
|
||||
<name>Add handle in sm-user-plugin and publish /_chatchannels/api/v1</name>
|
||||
<pros>Mentions and the generic channel API match CONTEXT.</pros>
|
||||
<cons>Every user-plugin consumer migrates a new column.</cons>
|
||||
</option>
|
||||
<option id="abort">
|
||||
<name>Stop; do not touch users or the channel prefix</name>
|
||||
<pros>The shared table stays unchanged.</pros>
|
||||
<cons>Plan 04 cannot start; mentions have no identity column.</cons>
|
||||
</option>
|
||||
</options>
|
||||
<resume-signal>Reply proceed or abort.</resume-signal>
|
||||
<verify>
|
||||
<automated>test -f ../sm-chatchannels-plugin/classes/channel_service.go && test -f ../fonoteka.go/plugins/golem15/user/models/user.go && echo user-and-channels-ready</automated>
|
||||
<fails_when>non-zero exit or "user-and-channels-ready" absent</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- SUMMARY records the reply verbatim.
|
||||
- After abort, users.handle is still absent from the user model.
|
||||
</acceptance_criteria>
|
||||
<done>The human confirmed the shared-column and published channel prefix before those files were written.</done>
|
||||
</task>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 2: End-to-end @handle mention — column, generator, parser, accrue</name>
|
||||
<reversibility rating="one-way">D-15 adds a unique column and generator on the shared users table every app migrates.</reversibility>
|
||||
<precondition>Task 1 reply is proceed; git status --porcelain -- ../fonoteka.go/plugins/golem15/user is empty or only this session's files.</precondition>
|
||||
<files>../fonoteka.go/plugins/golem15/user/models/user.go, ../fonoteka.go/plugins/golem15/user/updates/202610160002_add_handle_to_users.go, ../fonoteka.go/plugins/golem15/user/updates/registry.go, ../fonoteka.go/plugins/golem15/user/classes/handle_generator.go, ../fonoteka.go/plugins/golem15/user/classes/events.go, ../fonoteka.go/plugins/golem15/user/controllers/registration.go, ../fonoteka.go/plugins/golem15/user/controllers/users_admin_controller.go, ../sm-chatchannels-plugin/classes/mention_parser.go, ../sm-chatchannels-plugin/http_tracer_test.go</files>
|
||||
<read_first>../fonoteka.go/plugins/golem15/user/models/user.go, ../fonoteka.go/plugins/golem15/user/classes/events.go, ../fonoteka.go/plugins/golem15/user/controllers/registration.go, ../fonoteka.go/plugins/golem15/user/controllers/users_admin_controller.go, ../sm-paymentgatewaysubscriptions-plugin/updates/202610150002_add_stripe_customer_id_to_users.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatchannels/classes/HandleGenerator.php, ../sm-chatchannels-plugin/classes/mention_parser.go, ../sm-chatchannels-plugin/classes/channel_service.go</read_first>
|
||||
<action>Per D-15 and D-13, add Handle *string `gorm:"column:handle"` on User. Keep MorphName Golem15\User\Models\User. Do not add handle to Fillable unless registration must accept an explicit handle (PHP generates it). Migration 202610160002_add_handle_to_users: ALTER TABLE users ADD COLUMN IF NOT EXISTS handle VARCHAR(255) NULL; CREATE UNIQUE INDEX IF NOT EXISTS users_handle_unique ON users (handle); Rollback no-op.
|
||||
|
||||
HandleGenerator: port PHP; charset subset of MentionParser [A-Za-z0-9._-], length 2–255, unique, retry on conflict. Fire a typed user.Created event from every create path: API register next to FireRegisterEvent in registration.go, and admin create from usersAdminController.FormAfterCreate in users_admin_controller.go (inside that create tx). Generate the handle on Created (or inside the same create tx before commit) so a new user always has one.
|
||||
|
||||
MentionParser (already in 19-03) now looks up users.handle among that conversation's participants and excludes the sender. http_tracer_test.go TestHandleMention (Postgres): two members with handles alice and bob; alice sends a channel message containing @bob; a golem15_chatchannels_channel_mentions row exists for bob; @carol (not a participant) creates none; @alice (self) creates none. TestHandle (short, in the user plugin): generator charset and uniqueness.
|
||||
|
||||
Task 3 adds the HTTP API, the user authorizer, Deleting/GDPR and the backfill command. Do not bump app submodule pointers. Do not put handle on the public user JSON unless a golden requires it.</action>
|
||||
<verify>
|
||||
<automated>go -C ../fonoteka.go/plugins/golem15/user vet ./... && go -C ../fonoteka.go/plugins/golem15/user test -count=1 -short ./...</automated>
|
||||
<fails_when>non-zero exit or a package reports FAIL</fails_when>
|
||||
<automated>go -C ../sm-chatchannels-plugin vet ./... && go -C ../sm-chatchannels-plugin test -count=1 -short ./...</automated>
|
||||
<fails_when>non-zero exit or a package reports FAIL</fails_when>
|
||||
<automated>docker info >/dev/null 2>&1 && go -C ../sm-chatchannels-plugin test -count=1 -run '^TestHandleMention$' -v .</automated>
|
||||
<fails_when>non-zero exit: no Docker daemon (record NOT RUN in the SUMMARY), or TestHandleMention is missing, fails, or a non-participant handle accrued</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep -n 'column:handle' ../fonoteka.go/plugins/golem15/user/models/user.go prints one line.
|
||||
- grep -n 'ADD COLUMN IF NOT EXISTS handle' ../fonoteka.go/plugins/golem15/user/updates/202610160002_add_handle_to_users.go prints one line.
|
||||
- grep -n 'CreatedEventName\|user.Created' ../fonoteka.go/plugins/golem15/user/classes/events.go prints at least one line.
|
||||
- grep -n 'CreatedEventName\|user.Created' ../fonoteka.go/plugins/golem15/user/controllers/users_admin_controller.go prints at least one line.
|
||||
</acceptance_criteria>
|
||||
<done>A newly created user has a unique handle; @that-handle in a channel message accrues a mention only for a participant other than the sender.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: /_chatchannels/api/v1, user authorizer, Deleting cascade and handle backfill</name>
|
||||
<reversibility rating="one-way">D-06 publishes /_chatchannels/api/v1; D-17 makes user:{id} the private inbox for chat:message, chat:unread and mentions.changed.</reversibility>
|
||||
<files>../sm-chatchannels-plugin/routes.go, ../sm-chatchannels-plugin/plugin.go, ../sm-chatchannels-plugin/boot.go, ../sm-chatchannels-plugin/api/json.go, ../sm-chatchannels-plugin/api/errors.go, ../sm-chatchannels-plugin/api/auth.go, ../sm-chatchannels-plugin/api/channels.go, ../sm-chatchannels-plugin/api/categories.go, ../sm-chatchannels-plugin/api/prefs.go, ../sm-chatchannels-plugin/api/members.go, ../sm-chatchannels-plugin/api/pins.go, ../sm-chatchannels-plugin/api/mentions.go, ../sm-chatchannels-plugin/api/search.go, ../sm-chatchannels-plugin/README.md, ../sm-chatchannels-plugin/http_tracer_test.go, ../fonoteka.go/plugins/golem15/user/plugin.go, ../fonoteka.go/plugins/golem15/user/boot.go, ../fonoteka.go/plugins/golem15/user/classes/events.go, ../fonoteka.go/plugins/golem15/user/controllers/users_admin_controller.go, ../fonoteka.go/plugins/golem15/user/console/backfill_handles.go, ../fonoteka.go/plugins/golem15/user/README.md</files>
|
||||
<read_first>../sm-chat-plugin/routes.go, ../sm-chatchannels-plugin/classes/channel_service.go, ../fonoteka.go/plugins/golem15/user/plugin.go, ../fonoteka.go/plugins/golem15/user/controllers/users_admin_controller.go, modules/lighthouse/README.md, ../fonoteka.go/plugins/golem15/user/console/require_password_change.go</read_first>
|
||||
<action>Per D-06, D-08, D-09, D-13 and D-17, mount /_chatchannels/api/v1 with surf.Use("throttle:chatchannels-api", "jwt.auth"), frontendUser only. Paths: /contexts/{type}/{id}/channels CRUD-ish via ChannelService, categories, personal-categories, last-active, members, prefs, notify-pref, join, leave, pins, mentions, search (beachcomber query, fail-open empty list). Access: resolver + participation; IDOR 404. Same wire error envelope as chat. Search is D-20 exposed here.
|
||||
|
||||
User authorizer (D-17): in sm-user-plugin Boot, lighthouse.From then Registry.Register("user", …) if the namespace is free (skip if already registered). Allow only user:{id} for that user; lighthouse.ChannelID is correct for this two-segment name. chat already Emits chat:message and chat:unread to user:{id}; mentions.changed already Emits from ChannelService.
|
||||
|
||||
user.Deleting: add the typed event; fire from FormAfterDelete (and any other delete path the user plugin owns) before ForceDeleteCleanup / unscoped delete, same tx. chatchannels Boot Listen Deleting → SeverUserFromContext (prefs, members, personal categories). Do not hook User with GORM from chatchannels.
|
||||
|
||||
Optional user:backfill-handles fills null handles. User plugin README documents handle, the authorizer and the two events. ChatChannels README documents APIPrefix.
|
||||
|
||||
TestChannelHTTPTracer (Postgres): jwt member with allow-all resolver creates a channel via POST /_chatchannels/api/v1/contexts/{type}/{id}/channels and lists it; stranger 404. TestUserAuthorizer (short): user:9 allowed only for 9. TestDeletingCascade (Postgres): delete user removes that user's prefs/members/personal categories.
|
||||
|
||||
Commit sm-user-plugin on its own (core-plugins-own-repos). Push only if the human already allowed pushes for this phase (19-01 proceed-and-push); otherwise leave the user-plugin commit local and record that. Do not bump fonoteka or other app pointers.</action>
|
||||
<verify>
|
||||
<automated>go -C ../fonoteka.go/plugins/golem15/user vet ./... && go -C ../fonoteka.go/plugins/golem15/user test -count=1 -short ./...</automated>
|
||||
<fails_when>non-zero exit or a package reports FAIL</fails_when>
|
||||
<automated>go -C ../sm-chatchannels-plugin vet ./... && go -C ../sm-chatchannels-plugin test -count=1 -short ./...</automated>
|
||||
<fails_when>non-zero exit or a package reports FAIL</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep -n 'const APIPrefix = "/_chatchannels/api/v1"' ../sm-chatchannels-plugin/routes.go prints one line.
|
||||
- grep -n 'Register("user"' ../fonoteka.go/plugins/golem15/user/*.go ../fonoteka.go/plugins/golem15/user/classes/*.go prints at least one line.
|
||||
- grep -n 'DeletingEventName\|user.Deleting' ../fonoteka.go/plugins/golem15/user/classes/events.go prints at least one line.
|
||||
- grep -n 'SeverUserFromContext' ../sm-chatchannels-plugin/boot.go prints at least one line.
|
||||
</acceptance_criteria>
|
||||
<done>The channel API is mounted; user:{id} is authorized in sm-user-plugin; deleting a user severs channel rows through a festival event.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| client → /_chatchannels/api/v1 | untrusted JSON and ids cross jwt.auth |
|
||||
| mention body → users.handle | untrusted @tokens become mention rows |
|
||||
| user delete → channel tables | GDPR cascade must not leave prefs/members |
|
||||
| subscribe user:{id} | client-chosen user channel |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-19-07 | Information disclosure | user.Deleting → SeverUserFromContext | high | mitigate | Fire Deleting in the delete tx; TestDeletingCascade |
|
||||
| T-19-10 | Elevation of privilege | user namespace authorizer | high | mitigate | Allow only user:{id} for that user; skip-if-registered; TestUserAuthorizer |
|
||||
| T-19-06 | Elevation of privilege | mention resolve (completed here) | high | mitigate | Participant-only handle match; TestHandleMention |
|
||||
| T-19-SC | Tampering | go.mod requires | high | mitigate | No new packages; analog handle migration only |
|
||||
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
go -C on sm-user-plugin and sm-chatchannels-plugin: vet and test -count=1 -short ./... green. Docker: TestHandleMention and TestChannelHTTPTracer. User-plugin commit is separate from the chatchannels commit.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
users.handle exists and generates; @mentions accrue for participants; /_chatchannels/api/v1 is authenticated; user:{id} is authorized; GDPR delete severs channel rows. No app pointer bump unless asked.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/19-chat-and-chatchannels/19-04-SUMMARY.md` when done
|
||||
</output>
|
||||
247
.planning/phases/19-chat-and-chatchannels/19-05-PLAN.md
Normal file
247
.planning/phases/19-chat-and-chatchannels/19-05-PLAN.md
Normal file
@@ -0,0 +1,247 @@
|
||||
---
|
||||
phase: 19-chat-and-chatchannels
|
||||
plan: 05
|
||||
type: execute
|
||||
wave: 4
|
||||
depends_on: ["19-02", "19-04"]
|
||||
files_modified:
|
||||
- ../sm-chat-plugin/testdata/goldens/message_to_api_array.json
|
||||
- ../sm-chat-plugin/testdata/goldens/created_chat_message.json
|
||||
- ../sm-chat-plugin/testdata/goldens/typing_chat_message.json
|
||||
- ../sm-chat-plugin/testdata/goldens/chat_message.json
|
||||
- ../sm-chat-plugin/testdata/goldens/chat_read.json
|
||||
- ../sm-chat-plugin/testdata/goldens/chat_unread.json
|
||||
- ../sm-chat-plugin/testdata/goldens/deleted_chat_message.json
|
||||
- ../sm-chat-plugin/golden_test.go
|
||||
- ../sm-chat-plugin/php_method_map_test.go
|
||||
- ../sm-chat-plugin/queststream_trace_test.go
|
||||
- ../sm-chatchannels-plugin/testdata/goldens/channel.json
|
||||
- ../sm-chatchannels-plugin/testdata/goldens/category.json
|
||||
- ../sm-chatchannels-plugin/testdata/goldens/pref.json
|
||||
- ../sm-chatchannels-plugin/testdata/goldens/pin.json
|
||||
- ../sm-chatchannels-plugin/testdata/goldens/mention.json
|
||||
- ../sm-chatchannels-plugin/testdata/goldens/mentions_changed.json
|
||||
- ../sm-chatchannels-plugin/golden_test.go
|
||||
- ../sm-chatchannels-plugin/php_method_map_test.go
|
||||
- ../sm-chatchannels-plugin/queststream_trace_test.go
|
||||
- ../sm-chatchannels-plugin/testdata/PHP_METHOD_MAP.md
|
||||
- ../sm-chat-plugin/testdata/PHP_METHOD_MAP.md
|
||||
- ../fonoteka.go/plugins/golem15/user/handle_test.go
|
||||
- scripts/check-phase19.sh
|
||||
- .planning/phases/19-chat-and-chatchannels/19-VALIDATION.md
|
||||
autonomous: true
|
||||
requirements: [SC-19-3, SC-19-4, D-02, D-21]
|
||||
estimate:
|
||||
tokens: 180000
|
||||
raw_tokens: 180000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "SC-19-3: TestToAPIArray and TestGoldenEvents (chat) plus TestChannelGoldenEvents (chatchannels) compare serializer keys (Message.toApiArray with attachments kind/width/height, thread_summary, channel/category/pref/pin/mention arrays) and every realtime event (name, channel, payload) to testdata/goldens/ captured from frozen PHP or a Wavepath recording; MemoryDriver publications contain no content key."
|
||||
- "D-21 / SC-19-4: testdata/PHP_METHOD_MAP.md lists 59 chat and 106 chatchannels PHP test methods, each mapped to a Go case or an explicit not-applicable note for D-02-only cases; TestPHPMethodMap fails if a PHP method is missing from the map."
|
||||
- "D-02: TestNoQuestStreamTrace greps production sources in both new plugins for the four locked tokens and fails on a hit; fixture id acme.teams stays out of production sources."
|
||||
- "scripts/check-phase19.sh is fail-closed: named tests must pass; a skipped, missing or red required test fails the stage; no admin or layers stage."
|
||||
artifacts:
|
||||
- path: "../sm-chat-plugin/testdata/PHP_METHOD_MAP.md"
|
||||
provides: "59-row chat PHP→Go map"
|
||||
contains: "59"
|
||||
- path: "../sm-chatchannels-plugin/testdata/PHP_METHOD_MAP.md"
|
||||
provides: "106-row chatchannels PHP→Go map"
|
||||
contains: "106"
|
||||
- path: "scripts/check-phase19.sh"
|
||||
provides: "phase gate (self-test, plugins, docs, postgres)"
|
||||
contains: "TestGoldenEvents"
|
||||
key_links:
|
||||
- from: "../sm-chat-plugin/golden_test.go"
|
||||
to: "../sm-chat-plugin/testdata/goldens"
|
||||
via: "byte-compare event name, channels and payload"
|
||||
pattern: "TestGoldenEvents"
|
||||
- from: "../sm-chatchannels-plugin/golden_test.go"
|
||||
to: "../sm-chatchannels-plugin/testdata/goldens"
|
||||
via: "byte-compare channel/category/pref/pin/mention arrays and mentions.changed"
|
||||
pattern: "TestChannelGoldenEvents"
|
||||
- from: "scripts/check-phase19.sh"
|
||||
to: "../sm-chat-plugin"
|
||||
via: "go -C named tests; fail-closed detectors copied from check-phase18.1.sh"
|
||||
pattern: "TestNoQuestStreamTrace"
|
||||
prohibitions:
|
||||
- statement: "Do not change summercms.go modules (lighthouse / attach / beachcomber / festival / conga). The script is the only framework-repo code artifact."
|
||||
status: unresolved
|
||||
verification: grep
|
||||
- statement: "Do not invent live MySQL column types; goldens come from frozen PHP serializers and ChatBroadcasting, or from a recorded Wavepath capture."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
- statement: "Vue chat UI, /_wave routes, satellites and chat:migrate-attachments stay out (Deferred / D-19)."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As a** application developer, **I want to** have Chat and ChatChannels run on SummerCMS as shared plugins with realtime through lighthouse, **so that** they are ready for the Wavepath port.
|
||||
|
||||
This plan's slice (plan 5 of 5, wave 4, LAST): goldens, the PHP method map (D-21) and `scripts/check-phase19.sh`. Unit tests are the last plan (CLAUDE.md).
|
||||
|
||||
## Spec-less probe fallback
|
||||
|
||||
Skipped visibly: see 19-01. This plan attaches SC-19-3, SC-19-4, D-02 and D-21 without generating probe predicates.
|
||||
|
||||
## Decisions taken in this plan (Claude's discretion)
|
||||
|
||||
- Golden files are JSON with a `_source` comment key naming frozen PHP path + SHA or a Wavepath capture timestamp. Normalize attachment URL host/prefix (D-10 / RESEARCH): compare object keys, rewrite the URL prefix before diffing.
|
||||
- PHP method counts: chat 59, chatchannels 106 (CONTEXT "about 130" is stale; RESEARCH counted 106).
|
||||
- Gate stages: self-test, plugins (chat, chatchannels, user), docs (README identifier check if docs/ is touched — none expected), postgres. No admin / layers.
|
||||
- Env overrides: PHASE19_ROOT, CHAT, CHATCHANNELS, USER_PLUGIN defaulting to sibling checkouts (`../sm-chat-plugin`, `../sm-chatchannels-plugin`, `../fonoteka.go/plugins/golem15/user`).
|
||||
|
||||
## Artifacts this phase produces (this plan)
|
||||
|
||||
- `testdata/goldens/` in both new plugins.
|
||||
- `testdata/PHP_METHOD_MAP.md` in both new plugins.
|
||||
- Named tests from 19-VALIDATION.md.
|
||||
- `scripts/check-phase19.sh`.
|
||||
- 19-VALIDATION.md task IDs updated to 19-01..19-05 (planner replaces 19-W0-* seeds).
|
||||
|
||||
<objective>
|
||||
Prove serializer and realtime parity (SC-19-3, D-21 goldens), map every PHP test method (59 + 106), lock D-02 absence, and add the fail-closed phase gate.
|
||||
|
||||
Purpose: the phase cannot seal on green package tests that never ran the named suites.
|
||||
Output: check-phase19.sh --all (or plugins + docs when Docker is down) is green; every PHP method is mapped.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/msd-core/workflows/execute-plan.md
|
||||
@~/.claude/msd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/19-chat-and-chatchannels/19-CONTEXT.md
|
||||
@.planning/phases/19-chat-and-chatchannels/19-RESEARCH.md
|
||||
@.planning/phases/19-chat-and-chatchannels/19-VALIDATION.md
|
||||
@.planning/phases/19-chat-and-chatchannels/19-02-PLAN.md
|
||||
@.planning/phases/19-chat-and-chatchannels/19-04-PLAN.md
|
||||
scripts/check-phase18.1.sh
|
||||
scripts/check-phase19.3.sh
|
||||
|
||||
<interfaces>
|
||||
- Frozen chat tests at /media/nvme/dev/jz/wavepath.org/plugins/golem15/chat/tests (59 function test)
|
||||
- Frozen chatchannels tests at /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatchannels/tests (106 function test)
|
||||
- chat/models/Message.php toApiArray; traits/ChatBroadcasting.php getBroadcastPayload
|
||||
- chatchannels ChannelService array shapes; mentions.changed payload
|
||||
- ../sm-paymentgatewaysubscriptions-plugin/fixture_plugin_test.go TestFixtureAbsentFromProduction
|
||||
- modules/lighthouse/drivers.go MemoryDriver.Publications
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: End-to-end one golden — toApiArray plus created.chat.message</name>
|
||||
<files>../sm-chat-plugin/testdata/goldens/message_to_api_array.json, ../sm-chat-plugin/testdata/goldens/created_chat_message.json, ../sm-chat-plugin/golden_test.go, ../sm-chat-plugin/queststream_trace_test.go</files>
|
||||
<read_first>/media/nvme/dev/jz/wavepath.org/plugins/golem15/chat/models/Message.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chat/traits/ChatBroadcasting.php, ../sm-chat-plugin/models/message.go, ../sm-chat-plugin/harness_test.go, modules/lighthouse/drivers.go, ../sm-paymentgatewaysubscriptions-plugin/fixture_plugin_test.go</read_first>
|
||||
<action>Per SC-19-3 and D-21, land one production golden pair that the rest of the suite will copy. Author testdata/goldens/message_to_api_array.json from PHP toApiArray keys (id, conversation_id, sender_id, sender_name, sender_avatar, content, type, attachments[{id,url,filename,size,mime_type,kind,width,height}], created_at, created_at_human, edited_at, deleted_at, is_mine) plus root_id (RESEARCH Q2). Author created_chat_message.json from ChatBroadcasting: event created.chat.message, channels = context BroadcastChannel + chat:conversation:{id}, payload id, conversation_id, sender_id, root_id, type, action, created_at. Prefer a live Wavepath capture when one is on disk; otherwise freeze the PHP source at 943b712 and record `_source` in the JSON. Normalize URL prefix to /_chat/api/v1 before compare (D-10).
|
||||
|
||||
golden_test.go TestToAPIArray (short): build a message with one image attachment (kind, width, height set) and compare keys and scalar fields to the golden. TestGoldenEvents (short or memory driver): MemoryDriver.Publications() for a SendMessage matches created_chat_message.json name, channels and payload; fail if any publication map contains key content (T-19-04).
|
||||
|
||||
queststream_trace_test.go TestNoQuestStreamTrace (short): walk production .go / .yaml / .md (exclude testdata, *_test.go, PHP_METHOD_MAP.md) and fail if any of the four D-02 tokens appear; also fail if acme.teams appears outside test files. Use grep -v '^#' style filters so comments in this plan are irrelevant.
|
||||
|
||||
Task 2 expands goldens and the PHP maps. Task 3 writes the gate.</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-chat-plugin vet ./... && go -C ../sm-chat-plugin test -count=1 -short -run '^(TestToAPIArray|TestGoldenEvents|TestNoQuestStreamTrace)$' ./...</automated>
|
||||
<fails_when>non-zero exit, "no tests to run", or a named test is missing from the PASS list</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- test -s ../sm-chat-plugin/testdata/goldens/message_to_api_array.json
|
||||
- test -s ../sm-chat-plugin/testdata/goldens/created_chat_message.json
|
||||
- grep -n 'func TestToAPIArray' ../sm-chat-plugin/golden_test.go prints one line.
|
||||
- grep -n 'func TestNoQuestStreamTrace' ../sm-chat-plugin/queststream_trace_test.go prints one line.
|
||||
</acceptance_criteria>
|
||||
<done>One serializer golden and one created.chat.message golden pass; production sources have none of the four D-02 tokens.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Remaining goldens and the 59 + 106 PHP method maps</name>
|
||||
<files>../sm-chat-plugin/testdata/goldens/typing_chat_message.json, ../sm-chat-plugin/testdata/goldens/chat_message.json, ../sm-chat-plugin/testdata/goldens/chat_read.json, ../sm-chat-plugin/testdata/goldens/chat_unread.json, ../sm-chat-plugin/testdata/goldens/deleted_chat_message.json, ../sm-chat-plugin/golden_test.go, ../sm-chat-plugin/php_method_map_test.go, ../sm-chat-plugin/testdata/PHP_METHOD_MAP.md, ../sm-chatchannels-plugin/testdata/goldens/channel.json, ../sm-chatchannels-plugin/testdata/goldens/category.json, ../sm-chatchannels-plugin/testdata/goldens/pref.json, ../sm-chatchannels-plugin/testdata/goldens/pin.json, ../sm-chatchannels-plugin/testdata/goldens/mention.json, ../sm-chatchannels-plugin/testdata/goldens/mentions_changed.json, ../sm-chatchannels-plugin/golden_test.go, ../sm-chatchannels-plugin/php_method_map_test.go, ../sm-chatchannels-plugin/queststream_trace_test.go, ../sm-chatchannels-plugin/testdata/PHP_METHOD_MAP.md, ../fonoteka.go/plugins/golem15/user/handle_test.go</files>
|
||||
<read_first>/media/nvme/dev/jz/wavepath.org/plugins/golem15/chat/tests, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatchannels/tests, .planning/phases/19-chat-and-chatchannels/19-RESEARCH.md (Realtime events table, Validation Architecture), ../sm-chat-plugin/golden_test.go</read_first>
|
||||
<action>Per D-21 and SC-19-3, finish the golden set from RESEARCH Realtime events: typing.chat.message, chat:message (toApiArray on user:{id}), chat:read, chat:unread, deleted.chat.message, mentions.changed. Channel / category / pref / pin / mention array goldens from ChannelService serializers. Same URL-prefix normalisation. Extend chat TestGoldenEvents to load every chat event file. In sm-chatchannels-plugin, add TestChannelGoldenEvents (short) that loads channel.json, category.json, pref.json, pin.json, mention.json and mentions_changed.json and compares object keys and scalar fields the same way TestGoldenEvents does for chat.
|
||||
|
||||
Write testdata/PHP_METHOD_MAP.md in each plugin: a table PHP test method | Go test name | status (ported | not-applicable). not-applicable rows must name the D-02 reason (the dropped helpers / legacy context) and nothing else. Chat map has exactly 59 PHP methods; chatchannels map has exactly 106. php_method_map_test.go TestPHPMethodMap parses the markdown and the PHP tests directory (or a checked-in list of PHP method names extracted this session) and fails if the counts differ or a PHP name is missing. Port remaining unmapped behaviours as real Go tests in the matching package (short when they need no Docker; Postgres when they need the host). Do not leave a ported row pointing at a test that does not exist.
|
||||
|
||||
Mirror TestNoQuestStreamTrace in sm-chatchannels-plugin. User plugin: TestHandle covers unique + charset (VALIDATION 19-W0-07) if 19-04 did not already name it TestHandle.
|
||||
|
||||
Fill any still-missing named tests from 19-VALIDATION.md: TestBaseline, TestChatAuthorizer, TestBroadcastCreated, TestAttachmentPolicy, TestAttachmentDownload, TestDMThrottle, TestSearchIndexer, TestCreateChannel. Prefer renaming existing 19-01..04 tests to those names over writing duplicates.</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-chat-plugin vet ./... && go -C ../sm-chat-plugin test -count=1 -short ./...</automated>
|
||||
<fails_when>non-zero exit or a package reports FAIL</fails_when>
|
||||
<automated>go -C ../sm-chatchannels-plugin vet ./... && go -C ../sm-chatchannels-plugin test -count=1 -short ./...</automated>
|
||||
<fails_when>non-zero exit or a package reports FAIL</fails_when>
|
||||
<automated>go -C ../sm-chatchannels-plugin test -count=1 -short -run '^TestChannelGoldenEvents$' ./...</automated>
|
||||
<fails_when>non-zero exit, "no tests to run", or TestChannelGoldenEvents is missing from the PASS list</fails_when>
|
||||
<automated>go -C ../fonoteka.go/plugins/golem15/user test -count=1 -short -run '^TestHandle$' ./...</automated>
|
||||
<fails_when>non-zero exit, "no tests to run", or TestHandle is missing from the PASS list</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep -c '|' ../sm-chat-plugin/testdata/PHP_METHOD_MAP.md is at least 59.
|
||||
- grep -c '|' ../sm-chatchannels-plugin/testdata/PHP_METHOD_MAP.md is at least 106.
|
||||
- grep -n 'func TestPHPMethodMap' ../sm-chat-plugin/php_method_map_test.go ../sm-chatchannels-plugin/php_method_map_test.go prints one line per file.
|
||||
- grep -n 'func TestGoldenEvents' ../sm-chat-plugin/golden_test.go prints one line.
|
||||
- grep -n 'func TestChannelGoldenEvents' ../sm-chatchannels-plugin/golden_test.go prints one line.
|
||||
</acceptance_criteria>
|
||||
<done>Every PHP test method is mapped; every RESEARCH event has a golden; TestPHPMethodMap and TestGoldenEvents pass.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: scripts/check-phase19.sh fail-closed gate</name>
|
||||
<files>scripts/check-phase19.sh, .planning/phases/19-chat-and-chatchannels/19-VALIDATION.md</files>
|
||||
<read_first>scripts/check-phase18.1.sh, scripts/check-phase19.3.sh, .planning/phases/19-chat-and-chatchannels/19-VALIDATION.md</read_first>
|
||||
<action>Per SC-19-4 and D-21, copy the fail-closed detectors from scripts/check-phase18.1.sh (detect_go, go_json, go_named, expect_detector, run_self_test, docker_up). Drop admin and layers stages. Stages: self-test, plugins, docs, postgres. plugins: go -C vet ./... and go -C test -count=1 -short ./... in CHAT, CHATCHANNELS and USER_PLUGIN, then go_named required short tests. docs: if this phase touched no summercms.go docs/, run bash -n on the script only (or TestDocsTree if a docs page was added — none should have been). postgres: named Docker tests in the three plugin modules; without Docker, --all prints NOT RUN postgres unless PHASE19_REQUIRE_POSTGRES=1.
|
||||
|
||||
Required short names (adjust to the names that actually exist after Task 2, but keep these tokens present): TestToAPIArray, TestGoldenEvents, TestChannelGoldenEvents, TestNoQuestStreamTrace, TestPHPMethodMap, TestChatAuthorizer, TestAttachmentPolicy, TestDMThrottle, TestSearchIndexer, TestHandle, TestUserAuthorizer, TestFixtureAbsentFromProduction. Required postgres names: TestChatTracer, TestChatHTTPTracer, TestCreateChannel, TestHandleMention, TestChannelHTTPTracer, TestDeletingCascade, TestBaseline / TestBroadcastCreated if present.
|
||||
|
||||
--self-test plants failing JSON and asserts the detectors exit non-zero. Header comment lists env overrides PHASE19_ROOT, CHAT, CHATCHANNELS, USER_PLUGIN. Never install packages.
|
||||
|
||||
Update 19-VALIDATION.md task IDs from 19-W0-* to 19-01 / 19-02 / 19-03 / 19-04 / 19-05 and mark File Exists for the tests this plan added. Do not edit summercms.go modules.</action>
|
||||
<verify>
|
||||
<automated>bash -n scripts/check-phase19.sh && bash scripts/check-phase19.sh --self-test && bash scripts/check-phase19.sh plugins</automated>
|
||||
<fails_when>non-zero exit, "self-test" does not print a passed line, or plugins reports FAIL / refuse</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep -n 'TestGoldenEvents' scripts/check-phase19.sh prints at least one line.
|
||||
- grep -n 'TestChannelGoldenEvents' scripts/check-phase19.sh prints at least one line.
|
||||
- grep -n 'TestNoQuestStreamTrace' scripts/check-phase19.sh prints at least one line.
|
||||
- grep -n 'TestPHPMethodMap' scripts/check-phase19.sh prints at least one line.
|
||||
- grep -n '^run_admin\|^run_layers' scripts/check-phase19.sh prints nothing.
|
||||
</acceptance_criteria>
|
||||
<done>The phase gate is fail-closed on named tests across chat, chatchannels and sm-user-plugin; self-test and plugins stages pass.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| golden files → CI | recorded PHP payloads become the acceptance oracle |
|
||||
| check-phase19.sh → sibling repos | the gate trusts CHAT / CHATCHANNELS / USER_PLUGIN paths |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-19-04 | Information disclosure | TestGoldenEvents | high | mitigate | Fail if a MemoryDriver publication contains content (already required in Task 1) |
|
||||
| T-19-SC | Tampering | scripts/check-phase19.sh | high | mitigate | No package installs; analog detectors; --self-test plants failures |
|
||||
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
go -C ../sm-chat-plugin test -count=1 -short ./... and the chatchannels / user equivalents stay green. bash scripts/check-phase19.sh --self-test and plugins pass. postgres runs on a Docker host or is recorded NOT RUN.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
SC-19-3 goldens pass, D-21 maps 59+106, D-02 grep is clean, and scripts/check-phase19.sh is the phase gate. Unit tests were the last plan.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/19-chat-and-chatchannels/19-05-SUMMARY.md` when done
|
||||
</output>
|
||||
478
.planning/phases/19-chat-and-chatchannels/19-PATTERNS.md
Normal file
478
.planning/phases/19-chat-and-chatchannels/19-PATTERNS.md
Normal file
@@ -0,0 +1,478 @@
|
||||
# Phase 19: Chat and ChatChannels - Pattern Map
|
||||
|
||||
**Mapped:** 2026-10-17
|
||||
**Files analyzed:** 44 (grouped by the RESEARCH five-plan split)
|
||||
**Analogs found:** 44 / 44 (0 with no analog; 3 are role-match / partial)
|
||||
|
||||
All analog paths are git-tracked (`git ls-files` this session in each repo). No gitignored mirror is named.
|
||||
|
||||
Path roots (siblings under `/media/nvme/dev/golem15/summercms.io/summercms/` unless noted):
|
||||
|
||||
| Prefix | Path | Role this phase |
|
||||
|--------|------|-----------------|
|
||||
| `FW/` | `summercms.go/` | Framework seams only (`scripts/check-phase19.sh`, ROADMAP SC3 wording). **No lighthouse / attach / beachcomber / festival / conga change** unless a golden proves a gap (RESEARCH A5). |
|
||||
| `PGSUB/` | `sm-paymentgatewaysubscriptions-plugin/` | Closest compiled-plugin analog (Winter dirs, squashed `IF NOT EXISTS` baseline, morph registry, `/_x/api/v1`, acme fixture, test-only host). |
|
||||
| `JR/` | `sm-journal-plugin/` | Closest analog for `/_x/api/v1` + `jwt.auth` groups, beachcomber `Searchable`, attach MorphName, console commands, Buckets. |
|
||||
| `USR/` | `fonoteka.go/plugins/golem15/user/` | Tracked checkout of `sm-user-plugin` (RESEARCH: edit this submodule; origin `git.golem15.com/golem15/sm-user-plugin`). Additive D-15 / D-17. Also mounted at `sm-summercmsio-app/plugins/golem15/user` (PGSUB `go.work`) and `sm-grzybyfunkcjonalne-app/plugins/golem15/user` (journal `go.work`). |
|
||||
| `PG/` | `sm-paymentgateway-plugin/` | Bearer-only `RequireJWT` (no cookies) for generic APIs. |
|
||||
| `PGS/` | `sm-pgstripe-plugin/` | `pact.HasJobs` + `conga.Job` registration. |
|
||||
| `CHAT/` | `sm-chat-plugin/` | **Does not exist yet.** Create with `tea repos create --owner golem15`. |
|
||||
| `CH/` | `sm-chatchannels-plugin/` | **Does not exist yet.** Same. |
|
||||
| `PHP-CHAT/` | `/media/nvme/dev/jz/wavepath.org/plugins/golem15/chat` at `943b712` | Frozen contract (read-only). |
|
||||
| `PHP-CH/` | `/media/nvme/dev/jz/wavepath.org/plugins/golem15/chatchannels` at `b92fb42` | Frozen contract (read-only). |
|
||||
|
||||
**Replace paths (locked, Phase 15 pitfall 9):** sibling plugin `go.mod` uses `replace git.golem15.com/golem15/summercms => ../summercms.go` (`PGSUB/go.mod:149`, `JR/go.mod:148`). Do **not** copy `USR/go.mod`'s `../../../../summercms.go` into the sibling checkout. User replace lives in `go.work` (`PGSUB/go.work:9` → `../sm-summercmsio-app/plugins/golem15/user`; RESEARCH prefers the fonoteka submodule while editing). Chat `go.work` also replaces `sm-user-plugin`. ChatChannels `go.work` replaces chat + user.
|
||||
|
||||
**Plugin IDs / packages (discretion, RESEARCH default):** `golem15.chat` / package `chat`; `golem15.chatchannels` / package `chatchannels`. Tables keep PHP prefixes `golem15_chat_*` and `golem15_chatchannels_*`. `models/` is a leaf (`.planning/notes/plugin-layout-winter-directories.md`).
|
||||
|
||||
**D-02:** no production identifier, string, comment or README names QuestStream, family, parent or child. EN `Friends Chat` / PL `Czat Rodzinny` become `Chat` / `Czat`. Copy `PGSUB/fixture_plugin_test.go` `TestFixtureAbsentFromProduction` and add those four words to the grep.
|
||||
|
||||
Suggested plan split from RESEARCH (present at the plan-count checkpoint; unit tests last): **19-01** chat repo + baseline + models + registry + ChatService + Bind/Emit + job; **19-02** chat generic API + private attachments; **19-03** chatchannels repo + baseline + ChannelService + festival listeners + search; **19-04** chatchannels generic API + sm-user-plugin handle + user authorizer + lifecycle events; **19-05** unit tests + goldens + PHP method map + `scripts/check-phase19.sh`. First planning commit also patches ROADMAP Repos + SC3.
|
||||
|
||||
## File Classification
|
||||
|
||||
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|
||||
|-------------------|------|-----------|----------------|---------------|
|
||||
| **Plan 1 — chat core** | | | | |
|
||||
| `CHAT/go.mod`, `CHAT/go.work` | config | — | `PGSUB/go.mod` + `PGSUB/go.work` (sibling replace `../summercms.go`) | exact |
|
||||
| `CHAT/README.md` | docs | — | `PGSUB/README.md` (neutral names; never name Wavepath / QuestStream) | exact |
|
||||
| `CHAT/plugin.go` | provider | request-response | `PGSUB/plugin.go` + `JR/plugin.go` (`HasRoutes`/`Buckets`/`HasJobs`) | exact |
|
||||
| `CHAT/boot.go` | provider | event-driven (boot) | `PGSUB/boot.go` `registerBootStep` / `bootRegistries` / `lagoon.OnDatabase` | exact |
|
||||
| `CHAT/contracts/context.go` | model/interface | — | `PGSUB/contracts/subscriber.go` + RESEARCH Context sketch | role-match |
|
||||
| `CHAT/contracts/registry.go` | utility | — | `PGSUB/contracts/registry.go` (`ErrUnknownSubscriberType` never skip) | exact |
|
||||
| `CHAT/classes/service.go` | service | CRUD | `PGSUB/classes/service.go` (`New`, `lagoon.Transaction`, typed Fire) | exact |
|
||||
| `CHAT/classes/authorizer.go` | middleware | request-response | `FW/modules/lighthouse/registry.go` + README Boot example; **do not use `ChannelID`** (segment 1) | exact (with footgun) |
|
||||
| `CHAT/classes/attachment_policy.go` | utility | file-I/O | `FW/modules/lagoon/attach/store.go` `Limits` + `JR/classes/media_store.go` constants | role-match |
|
||||
| `CHAT/models/{conversation,participant,message,thread_summary}.go` | model | CRUD | `PGSUB/models/subscription.go` + `JR/models/post.go` (`TableName`, `MorphName`, `Fillable`, `Rules`) | exact |
|
||||
| `CHAT/models/registry.go` | utility | — | `PGSUB/models/registry.go` | exact |
|
||||
| `CHAT/events/events.go` | model (event types) | pub-sub | `PGSUB/events/events.go` (in-tx Fire, `Tx` field, grep-able `…EventName`) | exact |
|
||||
| `CHAT/jobs/maintain_thread_summary.go` | service (job) | event-driven | `PGS/subscriptions/job.go` (`Kind()`, `conga.Job`, `HasJobs`) | exact |
|
||||
| `CHAT/updates/registry.go` | utility | — | `PGSUB/updates/registry.go` | exact |
|
||||
| `CHAT/updates/202610160001_create_golem15_chat_baseline.go` | migration | CRUD | `PGSUB/updates/202610150001_create_golem15_pgsub_baseline.go` | exact |
|
||||
| `CHAT/config/config.yaml` | config | — | `PGSUB/config/config.yaml` (`use_ui_avatars` / `CHAT_USE_UI_AVATARS`) | exact |
|
||||
| `CHAT/lang/{en,pl}/lang.yaml` | config | transform | `PGSUB/lang/{en,pl}/lang.yaml` + neutralize PHP names | exact |
|
||||
| `CHAT/console/rewrite_context_types.go` | utility | batch | `JR/console/export_posts.go` + `USR/console/require_password_change.go` | role-match |
|
||||
| **Plan 2 — chat HTTP + attachments** | | | | |
|
||||
| `CHAT/routes.go` | route | request-response | `PGSUB/routes.go` `APIPrefix` + `JR/routes.go` groups + `PG/api/auth.go` `RequireJWT` | exact |
|
||||
| `CHAT/api/*.go` (conversations, messages, dm, read, typing, mute, replies, attachments) | controller | request-response | `PGSUB/api/json.go` + `PGSUB/api/errors.go` + `USR/controllers/api_controller.go` | exact |
|
||||
| `CHAT/api/download.go` (private blob stream) | controller | file-I/O | `FW/modules/lagoon/attach/static.go` `NewReader` + participation 404 collapse | role-match |
|
||||
| **Plan 3 — chatchannels core** | | | | |
|
||||
| `CH/go.mod`, `CH/go.work`, `CH/README.md`, `CH/plugin.go` | config/provider | — | same as chat; `Requires()` = `golem15.chat`, `golem15.user` | exact |
|
||||
| `CH/contracts/access_resolver.go` + `CH/classes/deny_all.go` | model/interface | request-response | `PGSUB/contracts/subscriber.go` + deny-all default + Boot Warn (D-14) | role-match |
|
||||
| `CH/classes/channel_service.go` | service | CRUD | `PGSUB/classes/service.go` | exact |
|
||||
| `CH/classes/mention_parser.go` | utility | transform | `USR/models/slug.go` (charset helper only); regex is PHP port | partial |
|
||||
| `CH/classes/search_indexer.go` + `CH/search.go` | service | event-driven | `JR/search.go` + `JR/models/post_search.go` + `FW/modules/beachcomber/searchable.go` | exact |
|
||||
| `CH/models/{channel,category,personal_category,prefs,member,mention,pin,user_context_pref}.go` | model | CRUD | `PGSUB/models/subscription.go` (`visibility`/`notify_pref` CHECKs) | exact |
|
||||
| `CH/updates/202610160001_create_golem15_chatchannels_baseline.go` | migration | CRUD | `PGSUB` baseline (`IF NOT EXISTS`, **no** `users.handle`) | exact |
|
||||
| `CH` festival listeners (mention accrue / search) | hook | pub-sub | `PGSUB/fixture_plugin_test.go` `Listen[*events.…]` | exact |
|
||||
| **Plan 4 — chatchannels HTTP + user plugin** | | | | |
|
||||
| `CH/routes.go` + `CH/api/*.go` | route/controller | request-response | `CHAT/routes.go` once it exists; until then `PGSUB/routes.go` + `JR/routes.go` | exact |
|
||||
| `USR/models/user.go` (`Handle *string`) | model | CRUD | same file (`MorphName` stays PHP class) | exact (extend) |
|
||||
| `USR/updates/202610160002_add_handle_to_users.go` | migration | CRUD | `PGSUB/updates/202610150002_add_stripe_customer_id_to_users.go` | exact |
|
||||
| `USR/classes/handle_generator.go` | service | transform | PHP `HandleGenerator`; charset subset of MentionParser. `USR/models/slug.go` is **not** the generator | partial |
|
||||
| `USR` lighthouse `user` authorizer (Boot) | middleware | request-response | `FW/modules/lighthouse/README.md` Boot `Registry.Register` | exact |
|
||||
| `USR/classes/events.go` (`Created` / `Deleting`) | model (event types) | pub-sub | same file `RegisterEvent` + `PGSUB/events/events.go` | exact (extend) |
|
||||
| `USR/controllers/registration.go` + `users_admin_controller.go` | controller | CRUD | same files: fire new events from create + `FormAfterDelete` | exact (extend) |
|
||||
| `USR/console/backfill_handles.go` (optional) | utility | batch | `USR/console/require_password_change.go` | role-match |
|
||||
| **Plan 5 — tests + gate** | | | | |
|
||||
| `CHAT/testmain_test.go`, `CHAT/harness_test.go` | test | — | `PGSUB/testmain_test.go` (`flag.Parse` + `testing.Short`) + `PGSUB/harness_test.go` | exact |
|
||||
| `CHAT/fixture_plugin_test.go` (`acme.teams`) | test | — | `PGSUB/fixture_plugin_test.go` (`acme.members`, `TestFixtureAbsentFromProduction`) | exact |
|
||||
| `CH` same host + `search.driver=null` + lighthouse `memory` | test | — | same + `FW/modules/lighthouse/drivers.go` `MemoryDriver` | exact |
|
||||
| `CHAT|CH/testdata/goldens/` | test | transform | new capture from Wavepath; compare with `MemoryDriver.Publications()` | role-match |
|
||||
| `FW/scripts/check-phase19.sh` | test gate | batch | `FW/scripts/check-phase18.1.sh` / `check-phase19.3.sh` | exact |
|
||||
| `FW/.planning/ROADMAP.md` Repos + SC3 | docs | — | same file (first planning commit) | exact (extend) |
|
||||
|
||||
## Pattern Assignments
|
||||
|
||||
### `CHAT/go.mod` + `CHAT/go.work` (config)
|
||||
|
||||
**Analog:** `PGSUB/go.mod` lines 1–15 and 149–151; `PGSUB/go.work` whole file.
|
||||
|
||||
```
|
||||
module git.golem15.com/golem15/sm-chat-plugin
|
||||
go 1.27.0
|
||||
require (
|
||||
git.golem15.com/golem15/sm-user-plugin v0.1.1
|
||||
git.golem15.com/golem15/summercms v0.2.2
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.7
|
||||
github.com/jackc/pgx/v5 v5.10.0
|
||||
github.com/testcontainers/testcontainers-go v0.44.0
|
||||
github.com/testcontainers/testcontainers-go/modules/postgres v0.44.0
|
||||
gocloud.dev v0.46.0
|
||||
gorm.io/gorm v1.31.2
|
||||
)
|
||||
replace git.golem15.com/golem15/summercms => ../summercms.go
|
||||
```
|
||||
|
||||
`go.work` (copy PGSUB, swap the user replace to the checkout this phase edits):
|
||||
|
||||
```
|
||||
go 1.27.0
|
||||
use .
|
||||
replace git.golem15.com/golem15/summercms => ../summercms.go
|
||||
replace git.golem15.com/golem15/sm-user-plugin => ../fonoteka.go/plugins/golem15/user
|
||||
```
|
||||
|
||||
ChatChannels adds `replace …/sm-chat-plugin => ../sm-chat-plugin` and `Requires` chat. No new third-party library.
|
||||
|
||||
### `CHAT/plugin.go` (provider)
|
||||
|
||||
**Analog:** `PGSUB/plugin.go` (91 lines).
|
||||
|
||||
Interface assertions (PGSUB lines 25–31); chat also asserts `pact.HasRoutes`, `surf.BucketProvider`, `pact.HasJobs`, `pact.HasCommands` (D-12 rewrite command):
|
||||
|
||||
```go
|
||||
var (
|
||||
_ party.Plugin = (*Plugin)(nil)
|
||||
_ pact.HasConfig = (*Plugin)(nil)
|
||||
_ pact.HasMigrations = (*Plugin)(nil)
|
||||
_ pact.HasModels = (*Plugin)(nil)
|
||||
_ pact.HasLang = (*Plugin)(nil)
|
||||
_ pact.HasRoutes = (*Plugin)(nil)
|
||||
_ pact.HasJobs = (*Plugin)(nil)
|
||||
_ pact.HasCommands = (*Plugin)(nil)
|
||||
_ surf.BucketProvider = (*Plugin)(nil)
|
||||
)
|
||||
```
|
||||
|
||||
- `const PluginID = "golem15.chat"` (PGSUB line 34).
|
||||
- `//go:embed config` / `//go:embed lang` (PGSUB 41–45).
|
||||
- `Requires() []string { return []string{"golem15.user"} }` (PGSUB 58). ChatChannels: `{"golem15.chat", "golem15.user"}`.
|
||||
- `Register` stores `p.app` (60–63); `Boot` → `p.runBootSteps(app)` (67–70).
|
||||
- `Migrations() → updates.All()`, `Models() → models.All()` (76–78).
|
||||
- `init() { party.Register(&Plugin{}) }` (89–91).
|
||||
- Package doc: one paragraph, no consuming-app name, no QuestStream / family.
|
||||
|
||||
**Jobs analog:** `PGS/subscriptions/plugin.go` lines 34–41 (`_ pact.HasJobs`) and `PGS/subscriptions/job.go` lines 110–114:
|
||||
|
||||
```go
|
||||
func (p *Plugin) Jobs() []pact.Job {
|
||||
return []pact.Job{conga.Job(p.workMaintainThreadSummary, conga.OnQueue("broadcasts"), conga.MaxAttempts(1))}
|
||||
}
|
||||
```
|
||||
|
||||
`pact.HasJobs` / `pact.JobArgs.Kind()` live at `FW/modules/pact/capabilities.go` lines 88–103.
|
||||
|
||||
### `CHAT/boot.go` (provider, boot)
|
||||
|
||||
**Analog:** `PGSUB/boot.go` lines 19–47 (`registerBootStep` / `runBootSteps`, errors `"%s: boot %s: %w"`) and 110–136 `bootService` via `lagoon.OnDatabase`.
|
||||
|
||||
Chat boot steps (recommend):
|
||||
|
||||
1. `registries` — publish `*chat.Registry` (empty at boot; hosts register aliases from their own `Boot`, which runs after required plugins). Unknown `context_type` is an error at **read**, not boot (D-11).
|
||||
2. `realtime` — `lighthouse.From(app)` then `Bind` Message + `Registry.Register("chat", …)`. Blank-import `_ "…/lighthouse/centrifugo"` only in the **test host** / app, not in production plugin sources (RESEARCH Don't Hand-Roll).
|
||||
3. `service` — `lagoon.OnDatabase` → `classes.New` → `app.Publish(svc)` (PGSUB 121–135).
|
||||
|
||||
ChatChannels boot: `SetAccessResolver` default deny-all; `slog.Warn` when still default (D-14); `app.Events.Listen[*chat.MessageCreated|Updated|Deleted]`; `beachcomber.From` + fail-open indexer (not GORM Searchable on chat.Message — D-13 forbids model patching).
|
||||
|
||||
### `CHAT/contracts/context.go` + `CHAT/contracts/registry.go`
|
||||
|
||||
**Analog:** `PGSUB/contracts/subscriber.go` lines 9–38 (interface + resolver) and `PGSUB/contracts/registry.go` lines 9–95 (sentinels, `sync.RWMutex`, `Add` fails duplicate naming both plugins, `Resolver` wraps unknown).
|
||||
|
||||
```go
|
||||
var ErrUnknownContextType = errors.New("golem15.chat: unknown context type")
|
||||
var ErrDuplicateAlias = errors.New("golem15.chat: duplicate context alias")
|
||||
```
|
||||
|
||||
`Add(pluginID, registration)` stores alias → loader + optional `LegacyPHPClass` (D-12). `Resolver(alias)` never skips. Host registrations: recommend aliases `friendgroup`, `channel`, `friendcontext` (Wavepath / userfriends, Phase 19.1). Chat itself does not register host types.
|
||||
|
||||
Context methods (discretion names; PHP `ChatContextInterface`): `ID()`, `Type()` (alias, never FQCN after D-12), `Members`, `HasMember`, `CanOverseeChats`, `ChatEnabled`, `GroupName`, `CanActFor(ctx, actorID, memberID)`, `BroadcastChannel()` (2 or 3 segments). Default `CanActFor`: `actorID == memberID` only (`PHP-CHAT/classes/AbstractContextAdapter.php`).
|
||||
|
||||
Ship `registerProvider` / `ContextForUser` on the registry for PHP parity (Open Question 5) but **do not** use providers on generic routes (D-08 addresses `{type}/{id}`).
|
||||
|
||||
### `CHAT/classes/service.go` (service, CRUD)
|
||||
|
||||
**Analog:** `PGSUB/classes/service.go` lines 1–119.
|
||||
|
||||
- Package doc: published as `*classes.Service`, found with `app.Lookup[*classes.Service]()`.
|
||||
- Sentinel errors prefixed `golem15.chat:` (`ErrAlreadySubscribed` style, PGSUB 24–31). Generic DM refusal is 422 `CANNOT_CREATE_DM` (RESEARCH threat table).
|
||||
- `Options{DB, Bus, Registry, Realtime, Logger}`; `New` refuses nil DB (PGSUB 72–95).
|
||||
- Writes go through `lagoon.Transaction`. Fire festival events **inside** the tx (`PGSUB/classes/apply.go` lines 326–333):
|
||||
|
||||
```go
|
||||
func fire[T any](ctx context.Context, s *Service, event T, name string, id uint) error {
|
||||
if err := s.bus.Fire(ctx, event); err != nil {
|
||||
return fmt.Errorf("golem15.chat: %s listener for %d: %w", name, id, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
A listener error fails the write (PGSUB events package doc: listeners must be idempotent). Socket / mail / search side effects that must not roll back the write belong in `lagoon.AfterCommit` — **except** lighthouse Bind/Emit, which already enqueue on the write tx.
|
||||
|
||||
Port PHP `ChatService` methods listed in RESEARCH (drop `getFamilyGroupChat` / `addUserToFamilyGroupChat`). `getOrCreateDm` is **context-scoped** (Pitfall 2); friendship-gated global DMs stay on the Wavepath port.
|
||||
|
||||
### `CHAT/classes/authorizer.go` (chat namespace)
|
||||
|
||||
**Analog:** `FW/modules/lighthouse/registry.go` lines 11–85; `FW/modules/lighthouse/README.md` lines 80–110 (Boot `From` + `Registry.Register`); capabilities field at `FW/modules/lighthouse/centrifugo/proxy_test.go` lines 60–61.
|
||||
|
||||
```go
|
||||
r := lighthouse.Allowed(map[string]any{"user_id": userID, "conversation_id": convID})
|
||||
r.Capabilities = []string{"prs"} // not {"prs","sub"} — no client publish
|
||||
```
|
||||
|
||||
**Footgun (Pitfall 11):** `lighthouse.ChannelID` returns **segment 1** (`FW/modules/lighthouse/channel.go` lines 35–46). PHP reads segment 2 of `chat:conversation:{id}`. Split the channel yourself:
|
||||
|
||||
```go
|
||||
parts := strings.Split(strings.TrimPrefix(channel, "presence:"), ":")
|
||||
// require len==3 && parts[0]=="chat" && parts[1]=="conversation" && digit id
|
||||
```
|
||||
|
||||
Authorizer re-checks participants on every subscribe (`registry.go` lines 36–39: no caching). User namespace (D-17, in `USR` Boot): `user:{id}` allowed only to that user. `lighthouse.ChannelID("user:9")` **does** return 9 (segment 1).
|
||||
|
||||
Do **not** mount lighthouse driver routes from the chat plugin. The **app** calls `lighthouse.Mount` (`FW/modules/lighthouse/route.go` lines 62–90) and picks `jwt.auth` (D-09, Mount surfaces). Chat only registers the authorizer.
|
||||
|
||||
### `CHAT/events/events.go` (pub-sub)
|
||||
|
||||
**Analog:** `PGSUB/events/events.go` whole file (75 lines).
|
||||
|
||||
```go
|
||||
const MessageCreatedEventName = "golem15.chat.message.created"
|
||||
const MessageUpdatedEventName = "golem15.chat.message.updated"
|
||||
const MessageDeletedEventName = "golem15.chat.message.deleted"
|
||||
|
||||
type MessageEvent struct {
|
||||
MessageID, ConversationID, ActorID uint
|
||||
RootID *uint
|
||||
Tx *gorm.DB
|
||||
}
|
||||
type MessageCreated struct{ MessageEvent }
|
||||
```
|
||||
|
||||
One exported `…EventName` per line (grep-able). Fire inside the write transaction after the row is saved; carry `Tx`. Side effects that must not run on rollback go in `lagoon.AfterCommit`. RESEARCH discretion names: `chat.MessageCreated|Updated|Deleted`.
|
||||
|
||||
### `CHAT/models/*` (CRUD)
|
||||
|
||||
**Analog:** `PGSUB/models/subscription.go` lines 43–109 (`TableName`, PHP `MorphName` const, `Fillable`, `Rules`, scopes) and `JR/models/post.go` lines 38–63 (`MorphName` PHP class, `AttachRelations`).
|
||||
|
||||
```go
|
||||
func (Conversation) TableName() string { return "golem15_chat_conversations" }
|
||||
func (Message) MorphName() string { return `Golem15\Chat\Models\Message` } // D-12 does NOT rewrite this
|
||||
func (Message) AttachRelations() []attach.Relation {
|
||||
return []attach.Relation{{Name: "attachments", Many: true, Public: false}}
|
||||
}
|
||||
```
|
||||
|
||||
**Pitfall 4:** `MorphName` stays the PHP FQCN so adopted `system_files.attachment_type` rows resolve. Pending uploads use field `chat-pending-{userID}` (PHP `ChatService`).
|
||||
|
||||
`models/` is a leaf: no lighthouse / festival / conga imports. `kindFor` (PHP `ChatAttachmentPolicy` imported by `Message.php`) lives in `classes/` or a models-local map — not a classes import into models (plugin-layout note). Broadcasting is a `lighthouse.Bind` from `classes/` / Boot, not methods on Message (RESEARCH anti-pattern: only `created` binds; delete Emits from the generic handler).
|
||||
|
||||
`models/registry.go` copy `PGSUB/models/registry.go` (`Register` / `All`).
|
||||
|
||||
Serializer goldens: PHP `Message.toApiArray` keys (`id, conversation_id, sender_id, sender_name, sender_avatar, content, type, attachments[{id,url,filename,size,mime_type,kind,width,height}], created_at, created_at_human, edited_at, deleted_at, is_mine`). Generic URL prefix is `/_chat/api/v1/…` (D-10). Add `root_id` (and `thread_summary` when present) even though PHP `toApiArray` omits them (RESEARCH Q2).
|
||||
|
||||
### `CHAT/updates/*` (migration)
|
||||
|
||||
**Analog:** `PGSUB/updates/registry.go` whole file (`Register`, `All`, `run(stmts…)`); `PGSUB/updates/202610150001_create_golem15_pgsub_baseline.go` lines 18–41 (`CREATE TABLE IF NOT EXISTS`, `TIMESTAMPTZ`, `JSONB`, `CHECK`).
|
||||
|
||||
Recommend ID `202610160001_create_golem15_chat_baseline`. Import-compatible (P18.1 D-16): same table/column/index names, Postgres types. **No `family_id`.** Do not `DROP COLUMN family_id` (D-05). Soft-delete `deleted_at` on messages; `root_id` **no FK**; thread_summaries `uq_thread_summaries_root` **no FK**. Schema source of truth is PHP migrations, not the stale Wavepath sqlite (RESEARCH).
|
||||
|
||||
Chatchannels baseline `202610160001_create_golem15_chatchannels_baseline`. **Do not** add `users.handle` here (D-15). Visibility CHECK `public|private`; `notify_pref` varchar(16) default `mentions`.
|
||||
|
||||
### `CHAT/jobs/maintain_thread_summary.go`
|
||||
|
||||
**Analog:** `PGS/subscriptions/job.go` lines 44–63 (`JobKind` const, `Kind() string`, JSON args) and 110–114 (`Jobs()` → `conga.Job`). Queue/attempts: `FW/modules/conga/job.go` lines 21–36 (`OnQueue`, `MaxAttempts`). Dispatch inside the write tx: `FW/modules/conga/conga.go` lines 135–140 (`Manager.Dispatch` joins the caller's transaction).
|
||||
|
||||
```go
|
||||
type MaintainThreadSummaryArgs struct {
|
||||
RootID uint `json:"root_id"`
|
||||
}
|
||||
func (MaintainThreadSummaryArgs) Kind() string { return "golem15.chat.maintain_thread_summary" }
|
||||
```
|
||||
|
||||
Register `conga.OnQueue("broadcasts"), conga.MaxAttempts(1)` (`FW/modules/lighthouse/lighthouse.go` line 30 `DefaultQueue = "broadcasts"`). Dispatch when `root_id != nil` after create/delete, off-request (PHP `Message.php`).
|
||||
|
||||
### `CHAT/routes.go` + `CHAT/api/*` (controller, request-response)
|
||||
|
||||
**Analog:** `PGSUB/routes.go` lines 16–85; `JR/routes.go` lines 14–35; `PG/api/auth.go` lines 60–90; `PGSUB/api/errors.go` + `api/json.go`; `USR/routes.go` lines 9–31.
|
||||
|
||||
```go
|
||||
const APIPrefix = "/_chat/api/v1"
|
||||
const APIBucket = "chat-api"
|
||||
const DMBucket = "chat-dm" // Max: 10, Decay: time.Minute, key per frontend user id
|
||||
```
|
||||
|
||||
Mount pattern (PGSUB 53–61): `r.Group(APIPrefix, surf.Use("throttle:"+APIBucket, "jwt.auth"), …)`. DM start is a nested group or per-route `throttle:chat-dm`. Prefer `surf.Use("jwt.auth")` (D-09, app-mounted guard) **or** wrap with `PG/api/auth.go` `RequireJWT` when cookies must never be read (PGSUB comment lines 49–52). `frontendUser` (PG auth.go 82–90) refuses backend principals.
|
||||
|
||||
Error envelope (PGSUB `api/errors.go` 32–46): `wire.WriteJSON` `{"error": message}`; 422 `{"error":"Validation failed","errors":{field:[msg]}}`. IDOR: missing and unauthorized collapse to **404** (PHP `downloadAttachment`, RESEARCH threat table). Unsolicited DM: 422 `CANNOT_CREATE_DM`.
|
||||
|
||||
Buckets (PGSUB 67–85 / JR 123–148): key `bouncer.User` frontend id else `surf.ClientIP(r, surf.TrustedProxies(p.app.Config))`. 429 body stays surf `{"message":"Too Many Attempts."}`. `chat-dm` = 10/min (PHP `throttle:chat-dm`).
|
||||
|
||||
Generic paths (discretion, RESEARCH): `…/contexts/{type}/{id}/conversation`, `…/conversations/{id}/messages`, `…/messages/{id}`, `…/read`, `…/typing`, `…/mute`, `…/dm`, `…/attachments`, `…/attachments/{messageId}/{fileId}`, `…/messages/{id}/replies`, `PUT …/threads/{rootId}/mute` (D-07; chat-owned `golem15_chat_thread_mutes`, not the Wavepath host table).
|
||||
|
||||
Do not port `ChatApiController` / QuestStream parent (Pitfall 8). No `/api/chat/dm` or `/api/chat/attachments/…` (D-10).
|
||||
|
||||
### `CHAT/api/download.go` + `CHAT/classes/attachment_policy.go` (file-I/O)
|
||||
|
||||
**Analog:** `FW/modules/lagoon/attach/store.go` lines 27–119 (`Limits`, `Store`, `Upload.Public`, `ErrTooLarge`/`ErrFileType`/`ErrMIMEType`); `FW/modules/lagoon/attach/relation.go` lines 9–13 (`Public: false`); `FW/modules/lagoon/attach/static.go` lines 26–80 (stream via `bucket.NewReader`; `StaticHandlerPublic` 404s `is_public=false`); `JR/classes/media_store.go` lines 19–20 (named max-bytes const); `USR/models/user.go` lines 59–71 (`MorphName` + `AttachRelations`).
|
||||
|
||||
```go
|
||||
lim := attach.Limits{
|
||||
MaxBytes: 102400 * 1024, // PHP MAX_KB
|
||||
Extensions: chatKnownExts, // ChatAttachmentPolicy KNOWN_EXTS, not attach.DefaultFileExtensions
|
||||
MIMETypes: nil, // plus a wrapper that rejects BANNED_MIMES (svg/html/xml)
|
||||
}
|
||||
f, err := attach.Store(ctx, tx, bucket, attach.Upload{FileName: name, Body: body, Public: false}, lim)
|
||||
```
|
||||
|
||||
**Pitfall 9:** `DefaultFileExtensions` allows zip and omits oga/m4a/csv. Pass an explicit list. Ban SVG/HTML/XML by extension **and** sniffed MIME. `MaxAttachments = 5`. Image `kind`/`width`/`height` from attach metadata.
|
||||
|
||||
Downloads: **never** mount chat blobs on `attach.StaticHandler` / `StaticHandlerPublic`. Stream from `/_chat/api/v1/attachments/{messageId}/{fileId}` after participation check; missing and unauthorized both 404. `Content-Disposition: attachment` for unsafe types. Two-phase delete: `attach.DeleteForOwner` (`file.go` 121–156) then `DeleteKeys` after commit (`USR/classes/admin_actions.go` 228–240).
|
||||
|
||||
### Realtime Bind vs Emit (D-16)
|
||||
|
||||
**Analog:** `FW/modules/lighthouse/broadcast.go` lines 81–143 (`Binding`, `Bind`); `FW/modules/lighthouse/README.md` lines 47–56 and 125–148; `FW/modules/lighthouse/suppress.go` lines 45–81 (`Emit`); `FW/modules/lighthouse/drivers.go` lines 124–188 (`MemoryDriver`); `FW/modules/lighthouse/lighthouse.go` lines 51–103 (`From`).
|
||||
|
||||
```go
|
||||
err := lighthouse.Bind[models.Message](svc, lighthouse.Binding[models.Message]{
|
||||
Alias: "chat.message", // ActionCreated → created.chat.message
|
||||
ShouldBroadcast: func(a lighthouse.Action) bool { return a == lighthouse.ActionCreated },
|
||||
Channels: func(ctx context.Context, tx *gorm.DB, m *models.Message) ([]string, error) {
|
||||
// context BroadcastChannel() + "chat:conversation:{id}"; empty if context unresolved
|
||||
return chans, nil
|
||||
},
|
||||
Payload: func(ctx context.Context, tx *gorm.DB, m *models.Message, ev lighthouse.Event) (any, error) {
|
||||
// PHP getBroadcastPayload: ids only — never content / names / avatars / attachments
|
||||
return map[string]any{"id": m.ID, "conversation_id": m.ConversationID, /* root_id, type, action, created_at */}, nil
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**Pitfall 5:** default Bind payload is the full model (`broadcast.go` 224–230). PHP notification payload is ids only. Full `toApiArray` travels only on `chat:message` → `user:{id}` (other participants) via `Emit`, and on REST.
|
||||
|
||||
Emit (generic handlers): `typing.chat.message`, `chat:message`, `chat:read`, `chat:unread`, `deleted.chat.message` (Wavepath host today — port on generic delete, RESEARCH A2). Channel conversations need a registered `channel` adapter or Bind no-ops (Pitfall 6). Test host registers `acme.teams`.
|
||||
|
||||
Golden tests: `realtime.driver=memory`, assert `MemoryDriver.Publications()` event / channels / payload. Publications must not contain `content`.
|
||||
|
||||
### `CH/classes/search_indexer.go` + `CH/search.go` (D-20)
|
||||
|
||||
**Analog:** `JR/search.go` whole file (14–38); `JR/models/post_search.go` lines 12–79; `FW/modules/beachcomber/searchable.go` lines 9–29 (`SearchableAs` **without** prefix); `FW/modules/beachcomber/README.md` lines 73–95; `FW/modules/beachcomber/sync.go` lines 356–364 (fail-open Warn).
|
||||
|
||||
```go
|
||||
func (MessageIndex) SearchableAs() string { return "messages" } // NOT scout.prefix + messages
|
||||
```
|
||||
|
||||
Document fields (PHP `MessageIndex`): `id` string, `content` string, `conversation_id` int, `sender_id` int, `type` string, `created_at` int Unix. `ShouldBeSearchable`: not `TYPE_SYSTEM`, content non-empty. `default_sorting_field: created_at`.
|
||||
|
||||
**Do not** implement `Searchable` on `chat.models.Message` (D-13). Chatchannels listens to festival message events and calls `beachcomber.Service.Sync` / `Remove` itself. Indexer **fails open** (log Warn, do not fail send) — same as `sync.go` `warn`. Tests use `search.driver=null` (or a fake engine). Blank-import typesense only where the host configures it (`JR/search.go` line 10).
|
||||
|
||||
### `CH/contracts/access_resolver.go` (D-14)
|
||||
|
||||
**Analog:** `PGSUB/contracts/subscriber.go` (small interface) + deny-all default. PHP `DenyAllChannelAccessResolver`.
|
||||
|
||||
```go
|
||||
type AccessResolver interface {
|
||||
CanAccessContext(ctx context.Context, userID uint, contextType string, contextID uint) (bool, error)
|
||||
CanManageContext(ctx context.Context, userID uint, contextType string, contextID uint) (bool, error)
|
||||
}
|
||||
```
|
||||
|
||||
Default both return false. `SetAccessResolver` from the host at Boot; Boot logs Warn when still default. App (later Wavepath) installs the real resolver.
|
||||
|
||||
### `CH/classes/mention_parser.go` (partial)
|
||||
|
||||
**Analog:** PHP `MentionParser` regex `'/(?<![\w.@])@([\w.-]{2,255})/u'` (RESEARCH). `USR/models/slug.go` is only a charset cousin — do not use `Slugify` as the parser. Resolve handles against **that conversation's** participants; exclude sender (threat table). Bounded `{2,255}`, no nested quantifiers (ReDoS). Identity column is `users.handle` (D-15), not `username`.
|
||||
|
||||
### `USR` additive work (D-15, D-17, D-13)
|
||||
|
||||
**Handle column — analog:** `PGSUB/updates/202610150002_add_stripe_customer_id_to_users.go` whole file.
|
||||
|
||||
```go
|
||||
ID: "202610160002_add_handle_to_users",
|
||||
Migrate: run(
|
||||
`ALTER TABLE users ADD COLUMN IF NOT EXISTS handle VARCHAR(255) NULL`,
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS users_handle_unique ON users (handle)`,
|
||||
),
|
||||
Rollback: func(*gorm.DB) error { return nil }, // shared column may predate the plugin
|
||||
```
|
||||
|
||||
**User model — analog:** `USR/models/user.go` lines 14–60. Add `Handle *string \`gorm:"column:handle"\``. Keep `MorphName() = Golem15\User\Models\User`. Do not put handle in user API payloads unless a golden requires it.
|
||||
|
||||
**Lifecycle events — analog:** `USR/classes/events.go` (`RegisterEvent` already fires after create via `USR/controllers/registration.go` lines 83–97 `FireRegisterEvent`) and `PGSUB/events/events.go`. Add typed `user.Created` / `user.Deleting` (RESEARCH A6). Fire `Created` from every create path (API register + admin). Fire `Deleting` from `USR/controllers/users_admin_controller.go` `FormAfterDelete` (lines 506–521) **before** `ForceDeleteCleanup` / unscoped delete, inside the same tx, so chatchannels can `severUserFromContext` and drop prefs/members/personal categories. Today there is **no** festival delete event (`USR/classes/registry.go` is GORM hooks only) — this is the gap, not a model-extend.
|
||||
|
||||
**User authorizer (D-17):** in `USR/plugin.go` `Boot` (after JWT guard, lines 66–109), `lighthouse.From(app)` then `Registry.Register("user", …)` if the namespace is free. Allow only `user:{id}` for that user. Idempotent: skip if already registered (herald / forum, Phase 19.1).
|
||||
|
||||
**HandleGenerator:** port PHP; charset is a subset of MentionParser `[A-Za-z0-9._-]`. Unique; backfill optional `user:backfill-handles` (`USR/console/require_password_change.go` lines 15–25 for `bonfire.Command` shape).
|
||||
|
||||
Commit and push `sm-user-plugin` first, then any app submodule pointer (core-plugins-own-repos).
|
||||
|
||||
### Test host (D-21)
|
||||
|
||||
**Analog:** `PGSUB/testmain_test.go` whole file (`flag.Parse()` then `testing.Short()` — never scan `os.Args`); `PGSUB/harness_test.go` lines 42–142 (`bootSubscriptions`, `freshDB`, memblob, `attach.Publish`, `party.Activate`, `lagoon.Migrate`); `PGSUB/fixture_plugin_test.go` lines 21–26, 270–335, 371–407.
|
||||
|
||||
```go
|
||||
const fixturePluginID = "acme.teams" // PHP Acme\Teams\Team; never leak into production sources
|
||||
```
|
||||
|
||||
Default plugins: `golem15.user`, `golem15.chat` [, `golem15.chatchannels`], `acme.teams`. Extra env: `SUMMER_REALTIME__DRIVER=memory`, `SUMMER_SEARCH__DRIVER=null`. Fixture registers a chat context adapter (and a channel-as-context adapter so channel-send goldens fire). `TestFixtureAbsentFromProduction` also greps QuestStream / family / parent / child.
|
||||
|
||||
PHP method map target: **59** chat + **106** chatchannels (CONTEXT "about 130" is stale). QuestStream-only cases get an explicit "not applicable" note. Last plan.
|
||||
|
||||
### `FW/scripts/check-phase19.sh`
|
||||
|
||||
**Analog:** `FW/scripts/check-phase18.1.sh` lines 1–57 (stages `go` / `plugins` / `docs` / `postgres`; env overrides for sibling checkouts; fail-closed named tests). Chat has no admin SPA / Nuxt layer — drop `admin` / `layers` stages. Named tests from RESEARCH validation table (`TestBaseline`, `TestChatAuthorizer`, `TestBroadcastCreated`, `TestToAPIArray`, `TestGoldenEvents`, `TestHandle`, `TestAttachmentPolicy`, `TestSearchIndexer`, `TestNoQuestStreamTrace`).
|
||||
|
||||
## Shared Patterns
|
||||
|
||||
### Authentication
|
||||
**Source:** `PG/api/auth.go` lines 66–90; `USR/routes.go` line 26 `"jwt.auth"`; `PGSUB/routes.go` lines 53–61.
|
||||
**Apply to:** every generic chat / chatchannels route.
|
||||
Bearer frontend JWT. Backend principals are not actors (`frontendUser`). App mounts the guard (D-09); plugin declares `surf.Use("jwt.auth")` or wraps `RequireJWT`. Participation / `hasMember` / `canOversee` / ChannelAccessResolver checked on every call. IDOR → 404.
|
||||
|
||||
### Rate limits
|
||||
**Source:** `PGSUB/routes.go` lines 64–85; `JR/plugin.go` lines 121–148.
|
||||
**Apply to:** both generic APIs.
|
||||
Named surf buckets. `chat-dm` = 10/min per user. `chat-api` / `chatchannels-api` = 120/min per user else IP. 429 body unchanged.
|
||||
|
||||
### Error / JSON wire
|
||||
**Source:** `PGSUB/api/errors.go` lines 12–46; `PGSUB/api/json.go` timestamps as `*wire.Time`.
|
||||
**Apply to:** all HTTP handlers.
|
||||
`wire.WriteJSON` (HTTP-06: timestamps `+00:00`, `[]` not null). Flat `{"error":…}`. 422 validation map. Never leak provider / SQL text.
|
||||
|
||||
### Festival events (in-transaction)
|
||||
**Source:** `PGSUB/events/events.go` lines 1–14; `PGSUB/classes/apply.go` lines 326–333; `FW/modules/festival/bus.go` lines 42–80 (`Listen` / `Fire`).
|
||||
**Apply to:** message create/update/delete; user Created/Deleting.
|
||||
Fire after the row is saved, inside `lagoon.Transaction`, carry `Tx`. Listener error fails the caller. No `Message::extend` / `User::extend`.
|
||||
|
||||
### Realtime
|
||||
**Source:** `FW/modules/lighthouse/{broadcast,suppress,registry,channel,drivers,route}.go` and README lines 40–56, 80–148.
|
||||
**Apply to:** chat Boot + generic write handlers.
|
||||
`From` at Boot; `Bind` created-only with ids-only payload; `Emit` for named events; `Registry.Register` per namespace; `MemoryDriver` for goldens; `Mount` is the **app's** job. Split `chat:conversation:{id}` yourself.
|
||||
|
||||
### Private attachments
|
||||
**Source:** `FW/modules/lagoon/attach/{store,file,relation,static}.go`; `USR/models/user.go` MorphName.
|
||||
**Apply to:** chat uploads and downloads.
|
||||
`attach.Store` + explicit `Limits`; `is_public=false`; MorphName PHP class; participation-gated stream; never public static; two-phase delete.
|
||||
|
||||
### Search
|
||||
**Source:** `JR/search.go`; `JR/models/post_search.go`; `FW/modules/beachcomber/{searchable,sync}.go`.
|
||||
**Apply to:** chatchannels MessageIndex only.
|
||||
`SearchableAs` = `"messages"`; fail-open; festival-driven Sync/Remove; tests use null engine.
|
||||
|
||||
### Test host
|
||||
**Source:** `PGSUB/{testmain,harness,fixture_plugin}_test.go`.
|
||||
**Apply to:** both new repos.
|
||||
`flag.Parse` + `testing.Short`; testcontainers Postgres; memblob; `acme.teams`; fixture absent from production; lighthouse memory; beachcomber null.
|
||||
|
||||
### Docs / README
|
||||
**Source:** `PGSUB/README.md` lines 1–16 (H1, one-sentence summary, import, IDs, Overview…).
|
||||
**Apply to:** both plugin READMEs + any `FW/docs/` touch.
|
||||
Neutral names (`acme`). Identifier-checked. Config keys reviewed by hand. No Wavepath / QuestStream / family.
|
||||
|
||||
## No Analog Found
|
||||
|
||||
None with zero match. These are the weakest (planner should lean on RESEARCH.md + frozen PHP):
|
||||
|
||||
| File | Role | Data Flow | Why partial |
|
||||
|------|------|-----------|-------------|
|
||||
| `CH/classes/mention_parser.go` | utility | transform | No existing @handle parser. `USR/models/slug.go` is ASCII slug only. Port PHP regex against `users.handle`. |
|
||||
| `USR/classes/handle_generator.go` | service | transform | No unique-handle generator in Go. Port PHP `HandleGenerator`; uniqueness like `stripe_customer_id` first-write-wins is the closest write pattern (`PGSUB/classes/user_subscribable.go` 113–144). |
|
||||
| `CHAT/api/download.go` | controller | file-I/O | No participation-gated private download exists. Compose `attach` Store + `static.go` `NewReader` + 404 collapse. Do not invent a second disk layout. |
|
||||
|
||||
## Metadata
|
||||
|
||||
**Analog search scope:** `sm-paymentgatewaysubscriptions-plugin`, `sm-journal-plugin`, `fonoteka.go/plugins/golem15/user` (`sm-user-plugin`), `sm-paymentgateway-plugin`, `sm-pgstripe-plugin/subscriptions`, `summercms.go/modules/{lighthouse,lagoon/attach,beachcomber,festival,conga,pact}`, `scripts/check-phase18.1.sh`, `scripts/check-phase19.3.sh`. Frozen PHP trees read only as contract (not analogs to copy Go style from).
|
||||
|
||||
**Files scanned:** ~80 analog sources Read this session; every named analog path `git ls-files`-verified.
|
||||
|
||||
**Pattern extraction date:** 2026-10-17
|
||||
806
.planning/phases/19-chat-and-chatchannels/19-RESEARCH.md
Normal file
806
.planning/phases/19-chat-and-chatchannels/19-RESEARCH.md
Normal file
@@ -0,0 +1,806 @@
|
||||
# Phase 19: Chat and ChatChannels - Research
|
||||
|
||||
**Researched:** 2026-10-16
|
||||
**Domain:** PHP-to-Go plugin port (chat + chatchannels) on lighthouse / lagoon / festival / beachcomber / conga
|
||||
**Confidence:** HIGH for frozen PHP contracts, framework seams, and analog plugin layout; MEDIUM for live Wavepath MySQL column types (stale sqlite snapshot only)
|
||||
|
||||
<user_constraints>
|
||||
## User Constraints (from CONTEXT.md)
|
||||
|
||||
### Locked 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.
|
||||
|
||||
### Deferred Ideas (OUT OF SCOPE)
|
||||
- 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.
|
||||
</user_constraints>
|
||||
|
||||
## Project Constraints (from CLAUDE.md)
|
||||
|
||||
Treat these with the same authority as locked CONTEXT decisions.
|
||||
|
||||
- One Go module for the framework (`git.golem15.com/golem15/summercms`); plugins live in their own `sm-*-plugin` repos (`git.golem15.com/golem15/sm-<name>-plugin`), plugin ID and package keep the plain names (`golem15.chat` / package `chat`).
|
||||
- Compiled plugins only. A plugin implements `pact` capability interfaces; `party` runs `Register` then `Boot`; `backpack.App` is the container. No runtime plugin loading.
|
||||
- Request path: `surf` + `bouncer` + `towel` + `wire`. Response shapes are not "improved" during a port; the existing frontend's requests define the contract.
|
||||
- Data: GORM on Postgres only; per-plugin gormigrate; YAML via `goccy/go-yaml` never `gopkg.in/yaml.v3`.
|
||||
- Standard library first. A new dependency needs a research doc or a phase/decision note (`.planning/research/STACK.md`).
|
||||
- Framework code and plugin READMEs never name a consuming application (use `blog` / `acme`).
|
||||
- Module README structure and identifier-checked docs; API/config/CLI changes update README + `docs/` in the same commit.
|
||||
- Lean planning: few large plans; present suggested plan count and wait for confirmation before writing PLAN.md files.
|
||||
- Unit tests are the last plan of every phase.
|
||||
- Update `.planning/STATE.md` only via `msd_run query state.*` handlers.
|
||||
- No co-author tags. One logical change per commit; planning docs and code in separate commits.
|
||||
- Stage files by path (never `git add -A`); leave other sessions' uncommitted edits alone.
|
||||
- Don't push, tag or deploy without the user's go-ahead.
|
||||
- `go vet` and `go test ./...` green at every commit. Docker-backed tests skip under `-short`. `TestMain` that starts containers must call `flag.Parse()` and check `testing.Short()`.
|
||||
- Plugin layout: WinterCMS directories as Go packages; `models/` is a leaf (`.planning/notes/plugin-layout-winter-directories.md`).
|
||||
- Core plugin contracts stay non-breaking unless the user asks (`.planning/notes/core-plugins-own-repos.md`).
|
||||
- Commit and push the plugin repo first, then the pointer bump in an app as a separate commit.
|
||||
|
||||
## Summary
|
||||
|
||||
Phase 19 ports Wavepath's frozen `Golem15.Chat` (`943b712a64e96f8f8849a161ec20a11eb985ad7b`) and `Golem15.ChatChannels` (`b92fb42a1492140b8bb2e2ea1167db4fde2cf2a9`) into two new shared plugin repos. The PHP plugins already speak contexts through adapters; Wavepath registers `FriendGroup` and `Channel` adapters and a deny-all-overridable `ChannelAccessResolver`. Go must keep that shape: a build-time morph registry, typed festival events instead of `Message::extend` / `User::extend`, and lighthouse instead of `Golem15.WebSockets`.
|
||||
|
||||
The planner should treat this as two tracer-first vertical slices (chat, then chatchannels), plus additive `sm-user-plugin` work (`users.handle`, `user` namespace authorizer, user lifecycle events), with unit tests last. Do not port Wavepath community host features (groups, friends, forums, video, presence, emoji, webhooks, reactions, unfurl). Thread mute is in scope (D-07 locked) as a chat-owned route and store, not the Wavepath host table. Mirror those operations' **payloads** where D-06 / D-07 owns the operation; leave `/_wave` routes to the Wavepath app port.
|
||||
|
||||
**Primary recommendation:** Create `sm-chat-plugin` then `sm-chatchannels-plugin` on the analog P18.1 layout (Winter directories, squashed `CREATE TABLE IF NOT EXISTS` baseline, test-only `acme` host). Wire realtime with `lighthouse.Bind` for `created.chat.message` and `lighthouse.Service.Emit` for typing / `chat:message` / `chat:read` / `chat:unread` / `deleted.chat.message` / `mentions.changed`. Do not change `summercms.go` unless a golden event proves a lighthouse gap — none was found.
|
||||
|
||||
## Architectural Responsibility Map
|
||||
|
||||
| Capability | Primary Tier | Secondary Tier | Rationale |
|
||||
|------------|-------------|----------------|-----------|
|
||||
| Context registry + adapters | API / Backend | — | Build-time registration; unknown `context_type` fails at read (D-11) |
|
||||
| Conversation / message writes | API / Backend | Database / Storage | ChatService successor; participation checked every call (D-08) |
|
||||
| Channel org (categories, prefs, pins, mentions) | API / Backend | Database / Storage | ChannelService successor; deny-all resolver by default (D-14) |
|
||||
| Generic JSON APIs | API / Backend | — | `/_chat/api/v1` and `/_chatchannels/api/v1`; app mounts the guard (D-09) |
|
||||
| Private attachments | API / Backend | Database / Storage | `attach.Store` + participation-gated download; never public static (D-18) |
|
||||
| Realtime publish + subscribe authz | API / Backend | — | lighthouse publisher + per-namespace Authorizer (D-16, D-17) |
|
||||
| Thread summary job | API / Backend | Database / Storage | conga / River on `broadcasts`, off-request |
|
||||
| Message search index | API / Backend | Database / Storage | beachcomber Searchable from chatchannels; fail-open (D-20) |
|
||||
| `users.handle` + user authorizer | API / Backend | Database / Storage | Additive sm-user-plugin contract (D-15, D-17) |
|
||||
| GDPR user-delete cascade | API / Backend | Database / Storage | festival user lifecycle events; chatchannels deletes prefs/members/personal cats |
|
||||
| Vue chat UI | Browser / Client | — | Deferred (`vue-chat-layer`) |
|
||||
| Wavepath `/_wave` community routes | API / Backend | — | Out of scope (Wavepath app port) |
|
||||
|
||||
<phase_requirements>
|
||||
## Phase Requirements
|
||||
|
||||
ROADMAP lists Requirements as TBD. Map the four success criteria and locked D-IDs so the planner can attach IDs later.
|
||||
|
||||
| ID | Description | Research Support |
|
||||
|----|-------------|------------------|
|
||||
| SC-19-1 | Chat schema, models, adapters, jobs, commands and routes port 1:1; ChatChannels plugs in through the same adapter contract | Frozen migrations v1.3.0 / v1.6.0; ChatContextInterface; ChannelService; analog squashed baseline |
|
||||
| SC-19-2 | Realtime broadcasts and channel authorization go through lighthouse; no WebSockets plugin | lighthouse Bind / Emit / Registry; ChatChannelAuthorizer → `chat` namespace; user namespace in sm-user-plugin |
|
||||
| SC-19-3 (amended by CONTEXT parity proof) | Golden serializers and realtime events match PHP captured from Wavepath; `/_wave` route parity is the Wavepath port | `Message.toApiArray`; event table below; first planning commit must update ROADMAP Repos + SC3 wording |
|
||||
| SC-19-4 | Unit tests in the last plan | D-21 mapping of 59 chat + 106 chatchannels PHP methods |
|
||||
| D-15 / D-17 | Additive sm-user-plugin `handle` + `user` authorizer | P18.1 `stripe_customer_id` idempotent column precedent |
|
||||
</phase_requirements>
|
||||
|
||||
## Standard Stack
|
||||
|
||||
No new third-party library. Use the analog plugin pins already in-tree.
|
||||
|
||||
### Core
|
||||
|
||||
| Library | Version | Purpose | Why Standard |
|
||||
|---------|---------|---------|--------------|
|
||||
| Go | 1.27.0 | Language | Analog `sm-journal-plugin` `go.mod` [VERIFIED: sm-journal-plugin/go.mod:3] `go 1.27.0` |
|
||||
| summercms | tagged + `replace => ../summercms.go` | Framework | Analog replace [VERIFIED: sm-journal-plugin/go.mod:148] `replace git.golem15.com/golem15/summercms => ../summercms.go` |
|
||||
| sm-user-plugin | current checkout | Users, JWT, lifecycle | Chat `$require` is `Golem15.User` [VERIFIED: chat/Plugin.php:18-21] `$require = ['Golem15.User', 'Golem15.WebSockets']` — WebSockets becomes lighthouse |
|
||||
| gorm.io/gorm | v1.31.2 | ORM | Analog require [VERIFIED: sm-journal-plugin/go.mod:18] `gorm.io/gorm v1.31.2` |
|
||||
| gormigrate/v2 | v2.1.7 | Per-plugin migrations | Analog [VERIFIED: sm-journal-plugin/go.mod:9] `github.com/go-gormigrate/gormigrate/v2 v2.1.7` |
|
||||
| jackc/pgx/v5 | v5.10.0 | Postgres | Analog [VERIFIED: sm-journal-plugin/go.mod:11] `github.com/jackc/pgx/v5 v5.10.0` |
|
||||
|
||||
### Supporting (already in the framework / analog plugins)
|
||||
|
||||
| Library | Version | Purpose | When to Use |
|
||||
|---------|---------|---------|-------------|
|
||||
| lighthouse | in-repo | Publisher, Bind, Emit, Authorizer, MemoryDriver | All realtime (D-16) |
|
||||
| lagoon + lagoon/attach | in-repo | Models, migrations, private files | Schema + attachments (D-18) |
|
||||
| festival | in-repo | Typed sync events | D-13 hooks |
|
||||
| beachcomber (+ typesense engine) | in-repo | Searchable sync | D-20; tests use a fake / null engine |
|
||||
| conga / River | river v0.47.0 (STACK.md) | Thread summary + broadcast jobs | MaintainThreadSummaryJob; lighthouse already enqueues broadcasts |
|
||||
| testcontainers-go + postgres | v0.44.0 | D-21 host | Analog [VERIFIED: sm-journal-plugin/go.mod:13-14] |
|
||||
| gocloud.dev | v0.46.0 | memblob in tests | Analog harness |
|
||||
| goccy/go-yaml | v1.19.2 | Plugin config / lang | CLAUDE.md; never yaml.v3 |
|
||||
| tea CLI | 0.16.0 (this machine) | `tea repos create --owner golem15` | P18.1 D-22 repo-create precedent |
|
||||
|
||||
### Alternatives Considered
|
||||
|
||||
| Instead of | Could Use | Tradeoff |
|
||||
|------------|-----------|----------|
|
||||
| lighthouse | Port Golem15.WebSockets | Locked out (D-16, ROADMAP SC2, inventory note) |
|
||||
| festival events | GORM callbacks on Message from chatchannels | Violates D-13 (no model patching) |
|
||||
| beachcomber | Direct Typesense client in the plugin | Framework already owns Scout successor |
|
||||
| New HTTP router | chi / gin | surf is the project router |
|
||||
|
||||
**Installation:** none beyond `go.mod` require/replace of summercms + sm-user-plugin, matching analog plugins. Create repos with `tea repos create --owner golem15`.
|
||||
|
||||
**Version verification:** analog `go.mod` files Read this session (journal). No new registry package.
|
||||
|
||||
## Package Legitimacy Audit
|
||||
|
||||
This phase installs **no new external packages**. Plugin `go.mod` files copy the analog require set (gorm, gormigrate, pgx, testcontainers, gocloud). Those packages are already approved in `.planning/research/STACK.md` and live in sibling plugin repos.
|
||||
|
||||
| Package | Registry | Age | Downloads | Source Repo | Verdict | Disposition |
|
||||
|---------|----------|-----|-----------|-------------|---------|-------------|
|
||||
| — | — | — | — | — | — | No new packages |
|
||||
|
||||
**Packages removed due to [SLOP] verdict:** none
|
||||
**Packages flagged as suspicious [SUS]:** none
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### System Architecture Diagram
|
||||
|
||||
```text
|
||||
App Boot
|
||||
├─ chat.Registry.Register(alias, loader, legacyPHPClass)
|
||||
├─ chatchannels.SetAccessResolver(appResolver) // else deny-all + Warn
|
||||
├─ lighthouse.From → Registry.Register("chat", …)
|
||||
└─ sm-user-plugin → Registry.Register("user", …) // D-17
|
||||
|
||||
HTTP (jwt.auth + chat-dm / chat-api buckets)
|
||||
POST/GET /_chat/api/v1/...
|
||||
POST/GET /_chatchannels/api/v1/...
|
||||
│
|
||||
▼
|
||||
ChatService / ChannelService
|
||||
│ write tx (lagoon.Transaction)
|
||||
├─ persist models
|
||||
├─ festival.Fire(MessageCreated|Updated|Deleted) // D-13, in-tx
|
||||
├─ lighthouse.Bind enqueue created.chat.message // after commit via conga
|
||||
└─ lighthouse.Emit typing / chat:message / chat:read / chat:unread / deleted.chat.message
|
||||
│
|
||||
▼
|
||||
chatchannels listeners
|
||||
├─ accrue / reconcile / remove mentions
|
||||
├─ publish mentions.changed → user:{id}
|
||||
└─ SearchIndexer (fail-open) → beachcomber
|
||||
|
||||
Subscribe (Centrifugo proxy)
|
||||
lighthouse Authorizer per namespace (re-checked every subscribe)
|
||||
chat:conversation:{id} → participants only, capabilities ["prs"]
|
||||
user:{id} → that user only
|
||||
```
|
||||
|
||||
### Recommended Project Structure
|
||||
|
||||
```
|
||||
sm-chat-plugin/ # module git.golem15.com/golem15/sm-chat-plugin
|
||||
├── plugin.go # ID golem15.chat; Requires golem15.user
|
||||
├── routes.go # APIPrefix /_chat/api/v1
|
||||
├── models/ # leaf: Conversation, Participant, Message, ThreadSummary
|
||||
├── classes/ # Service, Registry, Authorizer, AttachmentPolicy (or models/ for kindFor)
|
||||
├── contracts/ # Context interface
|
||||
├── jobs/ # MaintainThreadSummary
|
||||
├── console/ # chat:rewrite-context-types (discretion)
|
||||
├── updates/ # squashed baseline
|
||||
├── lang/en|pl/ # phrasebook YAML
|
||||
├── config/
|
||||
└── *_test.go / tests/ # D-21 host (acme teams)
|
||||
|
||||
sm-chatchannels-plugin/
|
||||
├── plugin.go # ID golem15.chatchannels; Requires golem15.chat, golem15.user
|
||||
├── routes.go # APIPrefix /_chatchannels/api/v1
|
||||
├── models/ # Channel, Category, Prefs, Member, Mention, Pin, UserContextPref
|
||||
├── classes/ # ChannelService, MentionParser, SearchIndexer, DenyAll resolver
|
||||
├── contracts/ # AccessResolver
|
||||
├── updates/
|
||||
└── tests/ # acme fixture
|
||||
|
||||
sm-user-plugin/ # additive only — checkout is the app submodule
|
||||
├── models/user.go # Handle *string
|
||||
├── updates/ # ADD COLUMN IF NOT EXISTS handle + unique
|
||||
├── classes/ # HandleGenerator port; user authorizer; festival events
|
||||
```
|
||||
|
||||
Plugin IDs and table prefixes stay PHP: `golem15.chat`, `golem15.chatchannels`, `golem15_chat_*`, `golem15_chatchannels_*`.
|
||||
|
||||
`models/` is a leaf. PHP `Message.php` imports `ChatAttachmentPolicy` for `kindFor` — move that mapping into `models/` or serialize from `classes/` (`.planning/notes/plugin-layout-winter-directories.md` chat evidence). Broadcasting and the thread-summary job register from `classes/`, not from the model.
|
||||
|
||||
### Pattern 1: Squashed idempotent baseline (P18.1)
|
||||
|
||||
**What:** One gormigrate ID per plugin with `CREATE TABLE IF NOT EXISTS` / `CREATE INDEX IF NOT EXISTS`, import-compatible names.
|
||||
**When to use:** Every shared-plugin schema in this project.
|
||||
**Example:**
|
||||
|
||||
```go
|
||||
// Source: sm-paymentgatewaysubscriptions-plugin/updates/202610150001_create_golem15_pgsub_baseline.go:18-22
|
||||
ID: "202610150001_create_golem15_pgsub_baseline",
|
||||
Migrate: run(
|
||||
`CREATE TABLE IF NOT EXISTS golem15_pgsub_subscription_plans (
|
||||
```
|
||||
|
||||
Chat baseline ID (recommend): `202610160001_create_golem15_chat_baseline`.
|
||||
Chatchannels: `202610160001_create_golem15_chatchannels_baseline`.
|
||||
User handle: `202610160002_add_handle_to_users` mirroring [VERIFIED: sm-paymentgatewaysubscriptions-plugin/updates/202610150002_add_stripe_customer_id_to_users.go:17-18] `ALTER TABLE users ADD COLUMN IF NOT EXISTS stripe_customer_id VARCHAR(255) NULL`.
|
||||
|
||||
P18.1 later amended D-04 to **import-compatible** rather than adopt-in-place (18.1-CONTEXT D-16): same table/column/index names, Postgres types. Wavepath is MySQL; SummerCMS is Postgres-only. Do the same here.
|
||||
|
||||
### Pattern 2: Build-time morph registry (P18.1 D-08 / D-11)
|
||||
|
||||
**What:** Alias + loader + optional `LegacyPHPClass`. Unknown alias is an error.
|
||||
**When to use:** `context_type` on conversations and channels; attachment `MorphName` stays the PHP class string.
|
||||
**Example:** P18.1 subscriber [VERIFIED: sm-paymentgatewaysubscriptions-plugin/contracts/subscriber.go:13-14] `MorphAlias is the alias stored in subscriptions.subscriber_type.` and [VERIFIED: sm-paymentgatewaysubscriptions-plugin/classes/user_subscribable.go:12-13] `const UserAlias = "user"`.
|
||||
|
||||
Recommend aliases the host registers: `friendgroup`, `channel`, `friendcontext` (userfriends, Phase 19.1). Chat itself does not register host types.
|
||||
|
||||
### Pattern 3: lighthouse Bind vs Emit
|
||||
|
||||
**What:** Bind for model `created.chat.message`; Emit for named events that are not `{action}.{alias}`.
|
||||
**When to use:** Always; do not import centrifugo from plugin code except `_` in the test host.
|
||||
**Event name:** Bind Alias `"chat.message"` + `ActionCreated` → `created.chat.message` [VERIFIED: modules/lighthouse/README.md:52] `The event name is {action}.{alias} lowercased: lighthouse.ActionCreated`.
|
||||
**Capabilities:** [VERIFIED: modules/lighthouse/centrifugo/proxy_test.go:60-61]
|
||||
|
||||
```
|
||||
r := lighthouse.Allowed(nil)
|
||||
r.Capabilities = []string{"prs", "sub"}
|
||||
```
|
||||
|
||||
Chat authorizer sets `[]string{"prs"}` only.
|
||||
|
||||
### Pattern 4: Test-only acme host (D-21 / P18.1)
|
||||
|
||||
**What:** `acme.teams` fixture plugin in `*_test.go` only; `TestFixtureAbsentFromProduction` greps production sources.
|
||||
**When to use:** Both new repos.
|
||||
**Example:** [VERIFIED: sm-paymentgatewaysubscriptions-plugin/fixture_plugin_test.go:21-26] `const fixturePluginID = "acme.members"` and `TestFixtureAbsentFromProduction keeps its name out of the production sources.`
|
||||
PHP fixture class: [VERIFIED: chatchannels/tests/fixtures/AcmeTeamsTeam.php:29-30] `public $table = 'acme_teams_teams';`
|
||||
|
||||
### Pattern 5: Generic `/_x/api/v1` prefix
|
||||
|
||||
[VERIFIED: sm-paymentgatewaysubscriptions-plugin/routes.go:16-18]
|
||||
|
||||
```
|
||||
// APIPrefix is the customer subscription API prefix (D-13, the /_x/api/v1
|
||||
// convention).
|
||||
const APIPrefix = "/_paymentgatewaysubscriptions/api/v1"
|
||||
```
|
||||
|
||||
Chat: `const APIPrefix = "/_chat/api/v1"`. ChatChannels: `const APIPrefix = "/_chatchannels/api/v1"`.
|
||||
|
||||
### Anti-Patterns to Avoid
|
||||
|
||||
- **Implementing `getContextForUser` as the generic API's address:** Wavepath registers adapters only, never a provider, because a user has N groups [VERIFIED: wavepath/Plugin.php:170-171] `registerAdapter ONLY — NEVER registerProvider (Pitfall 1: getContextForUser returns ONE context, users have N groups).` D-08 addresses `{type}/{id}`.
|
||||
- **Binding all Message actions:** PHP `afterCreate` is the only ChatBroadcasting dispatch [VERIFIED: chat/models/Message.php:74-75] `$this->dispatchBroadcast('created');`. `deleted.chat.message` is emitted by Wavepath controllers, not the chat plugin [VERIFIED: wavepath/controllers/api/CommunityChatController.php:684] `dispatch(new BroadcastEventJob([$channel], 'deleted.chat.message', $payload))`. Bind `ShouldBroadcast` = created only; the generic delete handler Emits `deleted.chat.message`.
|
||||
- **SearchableAs including scout prefix:** PHP returns `config('scout.prefix') . 'messages'` [VERIFIED: chatchannels/models/MessageIndex.php:46] `return config('scout.prefix') . 'messages';`. Go `SearchableAs` is **without** prefix [VERIFIED: modules/beachcomber/searchable.go:13-14] `SearchableAs is the index name without the search.prefix.` Return `"messages"`.
|
||||
- **Naming QuestStream / family / parent / child** in code, comments, lang, READMEs (D-02). EN source is `'Friends Chat'` [VERIFIED: chat/lang/en/lang.php:5] `name' => 'Friends Chat'`. PL source is `'Czat Rodzinny'` [VERIFIED: chat/lang/pl/lang.php:5] `name' => 'Czat Rodzinny'` — neutralize both.
|
||||
- **Public static downloads** of chat files. `attach.StaticHandlerPublic` 404s private rows; still do not mount chat blobs there (D-18).
|
||||
- **Hand-rolling Centrifugo HTTP** from the plugin. lighthouse already owns it.
|
||||
- **Creating `family_id` on fresh Postgres.** Adopted MySQL/MariaDB DBs may still have it (D-05).
|
||||
|
||||
## Don't Hand-Roll
|
||||
|
||||
| Problem | Don't Build | Use Instead | Why |
|
||||
|---------|-------------|-------------|-----|
|
||||
| Realtime publish + subscribe JWT | WebSockets plugin / gocent | lighthouse + centrifugo driver | Phase 11; inventory says websockets is covered |
|
||||
| Private uploads | Custom disk layout | `attach.Store` + `attach.Limits` | Sniff, DiskExtension, is_public, two-phase delete |
|
||||
| Search sync | Inline Typesense HTTP | beachcomber.Searchable | After-commit, fail-open, prefix, kill-switch |
|
||||
| Jobs | river.Client in the plugin | `conga.Job` + `Dispatch` in the write tx | Transactional enqueue |
|
||||
| Cross-plugin hooks | `Message::extend` | festival typed events | D-13 |
|
||||
| JSON wire format | encoding/json ad hoc | `wire` (timestamps +00:00, [] not null) | HTTP-06 |
|
||||
| Rate limits | custom counters | surf named buckets | D-09 chat-dm 10/min |
|
||||
| Handle slug | ad hoc regex | port HandleGenerator | charset subset of MentionParser |
|
||||
| Test host | hit a real app | in-repo acme fixture + testcontainers | D-21 / P18 D-03 |
|
||||
|
||||
**Key insight:** The expensive mistakes in this domain are contract drift (event names, morph strings, attachment URLs) and leaking content onto Centrifugo. Reuse the framework seams and copy PHP serializers byte-for-byte.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: CONTEXT community line numbers are stale
|
||||
**What goes wrong:** Planner implements the wrong route block.
|
||||
**Why it happens:** D-07 cites `wavepath/routes.php` 488-526 / 558-590 / 673-721. Those lines are now FriendGroups, webhooks, and presence/status. Channel CRUD lives at 754-890; DM + `threads/{rootId}/mute` at 632-655.
|
||||
**How to avoid:** Use the operation lists in D-06, not the line numbers. Capture goldens from the controllers, not from a line-range scrape.
|
||||
**Warning signs:** A plan that ports FriendGroup CRUD or forum routes into chat.
|
||||
|
||||
### Pitfall 2: Wavepath DMs are not ChatService DMs
|
||||
**What goes wrong:** Generic `/dm` implements friendship-gated global DMs.
|
||||
**Why it happens:** Wavepath `POST community/dm` is context-null and friendship-gated [VERIFIED: wavepath/routes.php:633] `GLOBAL (context-null) 1:1 DMs`. ChatService `getOrCreateDm` requires the same context [VERIFIED: chat/classes/ChatService.php:166-169] `Validate users are in the same context`.
|
||||
**How to avoid:** Generic API uses ChatService (context-scoped, D-08). Friendship DMs stay on the Wavepath port. Payloads can still match `dmApi.ts` where the operation exists.
|
||||
**Warning signs:** Generic chat importing userfriends.
|
||||
|
||||
### Pitfall 3: MySQL adopt-in-place vs Postgres
|
||||
**What goes wrong:** Baseline claims to "adopt Wavepath unchanged" but SummerCMS is Postgres-only.
|
||||
**Why it happens:** D-04 copies P18.1 D-04; P18.1 later amended to import-compatible (D-16) because Horoskopia is MariaDB. Live Wavepath MySQL `wave2` was not queryable this session. Stale sqlite `storage/database_fresh.sqlite` (2026-05-30, 1.3 MB) predates threads and channels.
|
||||
**How to avoid:** Write Postgres `IF NOT EXISTS` baselines that are **import-compatible** (same table/column/index names, compatible types: `TIMESTAMPTZ`, `JSONB` for json columns, `VARCHAR` enums as CHECK or text). Adoption of a live Wavepath DB goes through `summer import:winter` at the app port, not `migrate` against MySQL. Fresh Go apps never get `family_id` (D-05).
|
||||
**Warning signs:** A migration that `DROP COLUMN family_id` or uses MariaDB-only JSON functions.
|
||||
|
||||
### Pitfall 4: Attachment morph must stay the PHP class
|
||||
**What goes wrong:** New `MorphName` like `chat.message` orphans adopted `system_files` rows.
|
||||
**Why it happens:** D-12 rewrites **context_type** aliases, not attachment_type. User model already keeps the PHP class [VERIFIED: fonoteka.go/plugins/golem15/user/models/user.go:59-60] `func (User) MorphName() string { return \`Golem15\User\Models\User\` }`.
|
||||
**How to avoid:** `func (Message) MorphName() string { return \`Golem15\Chat\Models\Message\` }`. Pending uploads use field `chat-pending-{userID}` [VERIFIED: chat/classes/ChatService.php:97-100] `->where('field', 'chat-pending-' . $sender->id)`.
|
||||
**Warning signs:** Goldens with empty attachments after import.
|
||||
|
||||
### Pitfall 5: Content on the socket
|
||||
**What goes wrong:** Bind default payload `{model, actor, …}` leaks message bodies.
|
||||
**Why it happens:** lighthouse default payload is the full model [VERIFIED: modules/lighthouse/README.md:49] `{"model":…,"actor":…,"timestamp":"…+00:00","ttl":60}`. PHP notification payload is ids only [VERIFIED: chat/traits/ChatBroadcasting.php:110-118] `'id' => $this->id, 'conversation_id' => …, 'root_id' => $this->root_id, …` — "Content, sender names, avatars, and attachments are NOT broadcast".
|
||||
**How to avoid:** Binding.Payload = PHP `getBroadcastPayload`. Full `toApiArray` travels only on `chat:message` to `user:{id}` (participants except sender) and on REST.
|
||||
**Warning signs:** MemoryDriver publications containing `content`.
|
||||
|
||||
### Pitfall 6: Channel conversations use Channel as context
|
||||
**What goes wrong:** Broadcasts vanish on channel messages.
|
||||
**Why it happens:** `createChannel` sets `context_type = Channel::class` on the backing conversation [VERIFIED: chatchannels/classes/ChannelService.php:95-96] `$conversation->context_type = Channel::class;`. Wavepath registers `ChannelChatContextAdapter` so ChatBroadcasting can resolve [VERIFIED: wavepath/Plugin.php:185-187] `a channel owns its Conversation with context_type = Channel::class`.
|
||||
**How to avoid:** Host (later) registers alias `channel`. The chat test host must register an acme channel-or-team adapter or broadcasts no-op (PHP bails when context is null).
|
||||
**Warning signs:** Golden `created.chat.message` missing on channel send.
|
||||
|
||||
### Pitfall 7: User plugin has no festival lifecycle events yet
|
||||
**What goes wrong:** chatchannels cannot hook GDPR delete / handle-on-create without patching User.
|
||||
**Why it happens:** `RegisterHooks` is a GORM-callback list only [VERIFIED: fonoteka.go/plugins/golem15/user/classes/registry.go:13-14] `func RegisterHooks(gdb *gorm.DB) error`. No `Handle` field on User [VERIFIED: fonoteka.go/plugins/golem15/user/models/user.go:14-55]. No `user` lighthouse authorizer in the user plugin this session.
|
||||
**How to avoid:** Add typed events in sm-user-plugin (D-13), fire them from user create/delete paths (API + admin + any GORM hook the user plugin already owns). chatchannels listens. Do not `User::extend`. Edit the submodule at `fonoteka.go/plugins/golem15/user` (remote `git.golem15.com/golem15/sm-user-plugin`); there is no sibling checkout.
|
||||
**Warning signs:** Handle only generated on one registration path.
|
||||
|
||||
### Pitfall 8: ChatApiController extends QuestStream
|
||||
**What goes wrong:** Someone ports the PHP controller hierarchy.
|
||||
**Why it happens:** [VERIFIED: chat/controllers/api/ChatApiController.php:9] `use Golem15\QuestStream\Controllers\Api\ApiController;` and `class ChatApiController extends ApiController`. That is why D-10 says the legacy routes fatal, and why D-02 forbids the parent.
|
||||
**How to avoid:** New handlers on surf; wire envelopes via existing user/journal JSON helpers.
|
||||
**Warning signs:** Import path containing QuestStream.
|
||||
|
||||
### Pitfall 9: DefaultFileExtensions ≠ ChatAttachmentPolicy
|
||||
**What goes wrong:** Uploads accept zip/svg or reject oga/m4a/csv.
|
||||
**Why it happens:** `attach.DefaultFileExtensions` is Winter's list minus some script types [VERIFIED: modules/lagoon/attach/store.go:51-63]. Chat policy is a tighter curated list plus an extra ban of svg/html/xml by MIME [VERIFIED: chat/classes/ChatAttachmentPolicy.php:30-60] `MAX_KB = 102400`, `MAX_ATTACHMENTS = 5`, `KNOWN_EXTS`, `BANNED_EXTS`, `BANNED_MIMES`.
|
||||
**How to avoid:** Pass an explicit `attach.Limits` from ChatAttachmentPolicy constants. `MaxBytes = 102400 * 1024`. Ban MIME via `Limits.MIMETypes` and/or a wrapper that rejects `BANNED_MIMES` after sniff. Image dimensions for `kind==image` come from attach metadata / getimagesize equivalent.
|
||||
**Warning signs:** A test that uploads `x.svg` renamed to `x.png`.
|
||||
|
||||
### Pitfall 10: Thread mute is a Wavepath table
|
||||
**What goes wrong:** Generic API writes `golem15_wavepath_thread_subscriptions`.
|
||||
**Why it happens:** D-07 names `threads/{rootId}/mute`. That route lives at [VERIFIED: wavepath/routes.php:655] `Route::put('community/threads/{rootId}/mute', …)` and `CommunityThreadController::setMute` updateOrCreates the host table.
|
||||
**How to avoid:** Implement the D-07 operation on chat-owned `PUT /_chat/api/v1/threads/{rootId}/mute` with a chat-owned store (`golem15_chat_thread_mutes`). Do not create the Wavepath host table, and do not treat conversation `ToggleMute` as a substitute.
|
||||
**Warning signs:** A chat migration mentioning `wavepath`, or a plan that ships conversation mute only.
|
||||
|
||||
### Pitfall 11: lighthouse.ChannelID is the wrong segment
|
||||
**What goes wrong:** Chat authorizer reads conversation id 0.
|
||||
**Why it happens:** `lighthouse.ChannelID` returns **segment 1** [VERIFIED: modules/lighthouse/README.md:44] `lighthouse.ChannelID returns segment 1`. PHP uses segment 2 (`chat:conversation:{id}`) [VERIFIED: chat/classes/ChatChannelAuthorizer.php:26-34] `count($parts) !== 3 || $parts[0] !== 'chat' || $parts[1] !== 'conversation'`.
|
||||
**How to avoid:** Split the channel and take the id after `chat` + `conversation`. This is a lighthouse footgun, not a framework gap.
|
||||
**Warning signs:** Authorizer tests that pass `lighthouse.ChannelID("chat:conversation:9")` and expect 9.
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Chat context interface (recommend)
|
||||
|
||||
```go
|
||||
// Discretion names. Methods match ChatContextInterface
|
||||
// [VERIFIED: chat/contracts/ChatContextInterface.php:27-89]
|
||||
// getContextId, getContextType, getChatMembers, hasMember,
|
||||
// canOverseeChats, isChatEnabled, getGroupChatName, canActAs, getBroadcastChannel
|
||||
type Context interface {
|
||||
ID() uint
|
||||
Type() string // registered alias, never a PHP FQCN after D-12
|
||||
Members(ctx context.Context) ([]uint, error)
|
||||
HasMember(ctx context.Context, userID uint) (bool, error)
|
||||
CanOverseeChats(ctx context.Context, userID uint) (bool, error)
|
||||
ChatEnabled() bool
|
||||
GroupName() string
|
||||
CanActFor(ctx context.Context, actorID, memberID uint) bool
|
||||
BroadcastChannel() string // 2 or 3 segments
|
||||
}
|
||||
```
|
||||
|
||||
Default `CanActFor`: false except `actorID == memberID` (PHP AbstractContextAdapter returns self-only) [VERIFIED: chat/classes/AbstractContextAdapter.php:115-118] `return $authenticated->id === $target->id;`.
|
||||
|
||||
### lighthouse chat authorizer
|
||||
|
||||
```go
|
||||
// [VERIFIED: chat/classes/ChatChannelAuthorizer.php:26-54]
|
||||
// count($parts) !== 3 || $parts[0] !== 'chat' || $parts[1] !== 'conversation'
|
||||
// capabilities: ['prs']
|
||||
func authorizeChat(ctx context.Context, userID uint, channel string) lighthouse.Result {
|
||||
parts := strings.Split(channel, ":")
|
||||
// strip broadcast namespace if present, then require chat + conversation + digit
|
||||
r := lighthouse.Allowed(map[string]any{"user_id": userID, "conversation_id": convID})
|
||||
r.Capabilities = []string{"prs"}
|
||||
return r
|
||||
}
|
||||
```
|
||||
|
||||
### Message.toApiArray keys (golden source)
|
||||
|
||||
[VERIFIED: chat/models/Message.php:170-218]
|
||||
|
||||
```
|
||||
id, conversation_id, sender_id, sender_name, sender_avatar, content, type,
|
||||
attachments[]: id, url, filename, size, mime_type, kind, width, height,
|
||||
created_at, created_at_human, edited_at, deleted_at, is_mine
|
||||
```
|
||||
|
||||
Attachment URL in PHP is `/api/chat/attachments/{messageId}/{fileId}`. Generic API must emit the new `/_chat/api/v1/...` path (D-10 drops the old one). Goldens captured from Wavepath community routes already use `/_wave/...` URLs — normalize the URL host/prefix in the golden harness, keep the object keys.
|
||||
|
||||
PHP `toApiArray` has **no** `root_id` key. Wavepath host merges it. Recommend the generic serializer add `root_id` (and `thread_summary` when present) so the SPA can move onto `/_chat`.
|
||||
|
||||
### Mention parser
|
||||
|
||||
[VERIFIED: chatchannels/classes/MentionParser.php:35] `'/(?<![\w.@])@([\w.-]{2,255})/u'`
|
||||
|
||||
PHP comments say the charset tracks `User.username`; D-15 and `add_handle_to_users.php` store the identity in `users.handle`. Port against `handle`.
|
||||
|
||||
### MaintainThreadSummary job
|
||||
|
||||
```go
|
||||
// PHP: tries=1, queue broadcasts, lockForUpdate recompute
|
||||
// [VERIFIED: chat/jobs/MaintainThreadSummaryJob.php:35-36] public int $tries = 1;
|
||||
// [VERIFIED: modules/lighthouse/lighthouse.go:30] DefaultQueue = "broadcasts"
|
||||
type MaintainThreadSummaryArgs struct {
|
||||
RootID uint `json:"root_id"`
|
||||
}
|
||||
func (MaintainThreadSummaryArgs) Kind() string { return "golem15.chat.maintain_thread_summary" }
|
||||
```
|
||||
|
||||
Register with `conga.OnQueue("broadcasts"), conga.MaxAttempts(1)`. Dispatch from the message write when `root_id != nil`, same as PHP [VERIFIED: chat/models/Message.php:116-117] `MaintainThreadSummaryJob::dispatch((int) $rootId)->onQueue(config('websockets.broadcast_queue', 'broadcasts'))`.
|
||||
|
||||
### Searchable document
|
||||
|
||||
[VERIFIED: chatchannels/models/MessageIndex.php:73-80]
|
||||
|
||||
```
|
||||
id (string), content (string), conversation_id (int), sender_id (int),
|
||||
type (string), created_at (int Unix timestamp)
|
||||
```
|
||||
|
||||
`ShouldBeSearchable`: not `TYPE_SYSTEM`, content non-empty [VERIFIED: chatchannels/models/MessageIndex.php:56-60].
|
||||
|
||||
Typesense latest collections docs still describe `default_sorting_field` as an `int32 / float` field [CITED: https://typesense.org/docs/latest/api/collections.md]. v0.15.0 added int64 as a default sorting field [CITED: https://typesense.org/docs/0.15.0/]. Unix seconds fit int32 until 2038. Declare `created_at` numeric and `default_sorting_field: created_at`; if the cluster rejects int64, use int32.
|
||||
|
||||
### Channel visibility + notify_pref
|
||||
|
||||
[VERIFIED: chatchannels/models/Channel.php:46] `'visibility' => 'required|in:public,private'`
|
||||
[VERIFIED: chatchannels/updates/add_notify_pref_to_channel_user_prefs.php:15-17] `'all'` / `'mentions'` / `'none'`; column default `'mentions'`; absent row reads as `'all'`.
|
||||
|
||||
### Config
|
||||
|
||||
[VERIFIED: chat/config/config.php:19] `'use_ui_avatars' => env('CHAT_USE_UI_AVATARS', true)`
|
||||
|
||||
## State of the Art
|
||||
|
||||
| Old Approach | Current Approach | When Changed | Impact |
|
||||
|--------------|------------------|--------------|--------|
|
||||
| Golem15.WebSockets BroadcastEventJob | lighthouse Bind / Emit | Phase 11 | Do not port websockets |
|
||||
| PHP class FQCN in morph columns | Build-time alias + D-12 rewrite | P18.1 D-08 | Cutover of no return |
|
||||
| Message::extend observers | festival events | D-13 | chatchannels requires chat, never patches it |
|
||||
| Scout Searchable on a projection model | beachcomber.Searchable from chatchannels | D-20 | Fail-open, same collection name |
|
||||
| QuestStream ApiController + /api/chat/* | Generic `/_chat/api/v1` | D-02, D-06, D-10 | Drop fatal legacy routes |
|
||||
| family_id | context_type + context_id | PHP v1.1.x | Fresh Go schema omits family_id |
|
||||
|
||||
**Deprecated/outdated:**
|
||||
- `chat:migrate-attachments` (D-19)
|
||||
- ChatWindow / ChatBadge / chat.js (D-10)
|
||||
- `Conversation::LEGACY_FAMILY_CONTEXT` and family helpers (D-02)
|
||||
- CONTEXT D-07 line numbers (stale; see Pitfall 1)
|
||||
|
||||
## Assumptions Log
|
||||
|
||||
| # | Claim | Section | Risk if Wrong |
|
||||
|---|-------|---------|---------------|
|
||||
| A1 | Live Wavepath MySQL `wave2` matches PHP v1.3.0 / v1.6.0 migrations (sqlite snapshot is pre-thread and pre-channels) | Pitfall 3 | Baseline missing a column the import mapper expects |
|
||||
| A2 | `deleted.chat.message` belongs on the generic chat delete handler (Wavepath host today) | Pattern 3 | Extra/missing golden event |
|
||||
| A3 | Thread mute is chat-owned (D-07 locked); Wavepath host table stays out | Pitfall 10 / Q1 RESOLVED | Host notifications still read the Wavepath table until the app port aliases `/_chat` |
|
||||
| A4 | Reactions, unfurl, presence, emoji, video, webhooks, friends, forums stay out | Summary | D-07 "same operations" read more broadly |
|
||||
| A5 | No summercms.go lighthouse change is required | Summary | A golden may need FormatChannels / ChannelID helper |
|
||||
| A6 | User lifecycle festival events should be named `user.Created` / `user.Deleting` (discretion) | Pitfall 7 | Hosts already listening under another name (none found) |
|
||||
|
||||
## Open Questions (RESOLVED)
|
||||
|
||||
1. **Does the generic API include `PUT …/threads/{rootId}/mute`?**
|
||||
- What we know: D-07 names it; the Wavepath host route is `community/threads/{rootId}/mute` and writes `golem15_wavepath_thread_subscriptions`. Conversation mute (`participants.is_muted`) is a different resource.
|
||||
- RESOLVED: include thread mute because D-07 is locked. Chat-owned `PUT /_chat/api/v1/threads/{rootId}/mute` at full D-07 scope: body `{muted}` coerced as boolean, actor is the jwt principal never a body user id, success keys `root_id` and `is_muted`, missing and unauthorized collapse to 404. Persist per-(user_id, root_id) `is_muted` in a chat-owned store (`golem15_chat_thread_mutes`, CREATE TABLE IF NOT EXISTS). Never create `golem15_wavepath_thread_subscriptions`.
|
||||
|
||||
2. **How wide is "same operations" in D-07?**
|
||||
- What we know: D-06 lists chat + channel resources. Community routes also have reactions, unfurl, webhook-icon, presence, emoji, video, forums, friends, groups.
|
||||
- RESOLVED: goldens for **chat plugin serializers + ChannelService arrays**. Host merges (reactions, unfurl) wait for the Wavepath port. Message `root_id` / `thread_summary` / `deleted_at` **are** chat columns; add `root_id` to the generic serializer even though PHP `toApiArray` omits it.
|
||||
|
||||
3. **Import-compatible vs adopt-in-place for D-04?**
|
||||
- What we know: Wavepath is MySQL; SummerCMS is Postgres; P18.1 amended the same sentence.
|
||||
- RESOLVED: Postgres IF NOT EXISTS baselines now; import mapper deferred to the Wavepath port / Phase 20, same as P18.1 D-16.
|
||||
|
||||
4. **Who fires `updated.chat.message`?**
|
||||
- What we know: chat plugin edit does not dispatchBroadcast; Wavepath delete does dispatch `deleted.chat.message`. Festival MessageUpdated / MessageDeleted still Fire inside the write tx (D-13) regardless of lighthouse Emit.
|
||||
- RESOLVED: capture goldens from Wavepath edit/delete before writing Bind/Emit. Default: Emit `deleted.chat.message` on generic delete; Emit `updated.chat.message` on generic edit if the capture shows it.
|
||||
|
||||
5. **Provider registration (`registerProvider`) in the Go registry?**
|
||||
- What we know: PHP has both adapter and provider; Wavepath uses adapter only; userfriends uses both.
|
||||
- RESOLVED: ship both on the registry (parity with PHP ContextResolver) but do not use providers on generic routes (D-08).
|
||||
|
||||
## Environment Availability
|
||||
|
||||
| Dependency | Required By | Available | Version | Fallback |
|
||||
|------------|------------|-----------|---------|----------|
|
||||
| Go | All code | ✓ | 1.27.0-X:nodwarf5 | — |
|
||||
| Docker | D-21 testcontainers | ✓ | 29.7.2 (`docker ps` ok) | — |
|
||||
| Postgres (local) | optional; tests use testcontainers | ✓ | pg_isready accepting on /run/postgresql:5432 | testcontainers |
|
||||
| tea | create sm-*-plugin repos | ✓ | 0.16.0 | manual Gitea UI |
|
||||
| mysql/mariadb client | D-04 live Wavepath diff | ✓ | client 12.3.3 | PHP migrations + stale sqlite |
|
||||
| ctx7 CLI / Context7 MCP | docs seam | ✗ | — | Official URLs via WebSearch / WebFetch |
|
||||
| graphify | research graph | disabled | — | skipped |
|
||||
|
||||
**Missing dependencies with no fallback:**
|
||||
- Live Wavepath MySQL schema dump (not readable this session). Planner must not invent live column types; use PHP migrations.
|
||||
|
||||
**Missing dependencies with fallback:**
|
||||
- Context7: official Typesense/ASVS URLs cited below.
|
||||
|
||||
## Validation Architecture
|
||||
|
||||
`workflow.nyquist_validation` is `true` in `.planning/config.json`.
|
||||
|
||||
### Test Framework
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Framework | Go `testing` + testcontainers-go v0.44.0 (Postgres) |
|
||||
| Config file | none at repo root — each plugin is its own module (`go test ./...`) |
|
||||
| Quick run command | `go test -short ./...` (inside each plugin repo; skip Docker) |
|
||||
| Full suite command | `go test ./...` (testcontainers Postgres + lighthouse memory + beachcomber null/fake) |
|
||||
|
||||
Analog host: `bootSubscriptions` / `freshDB` in pgsub `harness_test.go` (testcontainers, memblob, JWT secrets, `golem15.user` + fixture plugin). Chat host should set `realtime.driver=memory` and `search.driver=null` (or a fake engine).
|
||||
|
||||
### Phase Requirements → Test Map
|
||||
|
||||
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|
||||
|--------|----------|-----------|-------------------|-------------|
|
||||
| SC-19-1 | Baseline creates chat v1.3.0 tables minus family_id | integration | `go test -short ./updates -run TestBaseline` | ❌ Wave 0 |
|
||||
| SC-19-1 | ChannelService.createChannel / adoptConversation | integration | `go test ./classes -run TestCreateChannel` | ❌ Wave 0 |
|
||||
| SC-19-2 | chat authorizer participants + prs | unit | `go test -short ./classes -run TestChatAuthorizer` | ❌ Wave 0 |
|
||||
| SC-19-2 | MemoryDriver sees created.chat.message notification payload | integration | `go test ./... -run TestBroadcastCreated` | ❌ Wave 0 |
|
||||
| SC-19-3 | toApiArray golden (attachments kind/width/height) | unit | `go test -short ./models -run TestToAPIArray` | ❌ Wave 0 |
|
||||
| SC-19-3 | Event name/channel/payload goldens | unit | `go test -short ./... -run TestGoldenEvents` | ❌ Wave 0 |
|
||||
| D-15 | handle unique + generator charset | unit | `go test -short ./classes -run TestHandle` in sm-user-plugin | ❌ Wave 0 |
|
||||
| D-18 | banned svg/html/xml; max 5; 100MB | unit | `go test -short ./classes -run TestAttachmentPolicy` | ❌ Wave 0 |
|
||||
| D-20 | SearchableAs `messages`; fail-open indexer | unit | `go test -short ./classes -run TestSearchIndexer` | ❌ Wave 0 |
|
||||
| D-21 | PHP method map 59 + 106 | unit (last plan) | `go test ./...` + checklist file | ❌ Wave 0 |
|
||||
| D-02 | grep production sources for family/parent/child/QuestStream | unit | `go test -short ./... -run TestNoQuestStreamTrace` | ❌ Wave 0 |
|
||||
|
||||
PHP method counts Read this session: chat **59** `function test` (matches D-21). chatchannels **106** (CONTEXT said "about 130" — use 106 as the map target; extra PHP methods if any are data providers / helpers).
|
||||
|
||||
### Sampling Rate
|
||||
|
||||
- **Per task commit:** `go vet ./...` and `go test -short ./...` in the touched plugin module
|
||||
- **Per wave merge:** `go test ./...` in that plugin (Docker)
|
||||
- **Phase gate:** `scripts/check-phase19.sh` (create in last plan) requiring named tests, analog to `scripts/check-phase18.1.sh` / `check-phase19.3.sh`
|
||||
|
||||
### Wave 0 Gaps
|
||||
|
||||
- [ ] Each new repo: `go.mod`, `plugin.go`, `updates` baseline, `harness_test.go` / `testmain_test.go` (`flag.Parse` + `testing.Short`)
|
||||
- [ ] `testdata/goldens/` for serializers and events (capture from Wavepath PHP)
|
||||
- [ ] `acme.teams` fixture plugin (test files only)
|
||||
- [ ] PHP→Go test method map markdown in the last plan
|
||||
- [ ] `scripts/check-phase19.sh` at the framework repo (names tests across the three plugin modules)
|
||||
- [ ] sm-user-plugin handle migration + authorizer tests
|
||||
|
||||
Existing framework tests (`go test ./modules/lighthouse/...`, beachcomber, attach) already cover the seams; they do **not** cover chat behavior.
|
||||
|
||||
## Security Domain
|
||||
|
||||
`security_enforcement` is not set to false. CONTEXT requires the security-review agent.
|
||||
|
||||
### Applicable ASVS Categories
|
||||
|
||||
| ASVS Category | Applies | Standard Control |
|
||||
|---------------|---------|-----------------|
|
||||
| V2 Authentication | yes | `jwt.auth` via bouncer; app mounts the guard (D-09) |
|
||||
| V3 Session Management | no | JWT, no new session store |
|
||||
| V4 Access Control | yes | participation / hasMember / canOversee / ChannelAccessResolver deny-all; IDOR collapse to 404 |
|
||||
| V5 Input Validation / File Handling | yes | ChatAttachmentPolicy + `attach.Store` limits; MentionParser bounded regex |
|
||||
| V6 Cryptography | no | no new crypto; JWT already in user plugin |
|
||||
|
||||
### Known Threat Patterns for this stack
|
||||
|
||||
| Pattern | STRIDE | Standard Mitigation |
|
||||
|---------|--------|---------------------|
|
||||
| IDOR on message / attachment / channel ids | Information disclosure | Single 404 for missing and unauthorized (PHP downloadAttachment) |
|
||||
| Unsolicited DM / user enum | Elevation / Info | `chat-dm` 10/min [VERIFIED: chat/routes.php:23-24] `Limit::perMinute(10)`; generic 422 `CANNOT_CREATE_DM` |
|
||||
| Stored XSS via SVG/HTML/XML | Tampering | Ban ext + MIME; Content-Disposition attachment for unsafe types; private store |
|
||||
| Content leak on Centrifugo | Information disclosure | Notification-only payloads; fetch-after-notify |
|
||||
| Subscribe to someone else's conversation | Elevation | Authorizer re-checks participants; capabilities `prs` only (no client publish) |
|
||||
| Cross-conversation @mention | Elevation | Resolve handles against **that conversation's** participants; exclude sender |
|
||||
| GDPR leftover rows | Information disclosure | User-deleting event → delete prefs/members/personal categories; severUserFromContext |
|
||||
| Typesense outage breaking send | Denial of service | SearchIndexer fail-open |
|
||||
| ReDoS on mention regex | Denial of service | Bounded `{2,255}`, no nested quantifiers |
|
||||
| Claim someone else's pending upload | Elevation | `field=chat-pending-{actorID}` + unattached + is_public=false |
|
||||
|
||||
ASVS file handling [CITED: https://github.com/OWASP/ASVS/blob/v5.0.0/5.0/en/0x14-V5-File-Handling.md]: V5.1.1 document types/extensions/max size; V5.2.2 extension + content correspondence. Cheat sheet [CITED: https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html]: allowlist, do not trust Content-Type, server-generated disk names (`attach.Store` already generates disk names).
|
||||
|
||||
## Frozen PHP inventory (planner cheat-sheet)
|
||||
|
||||
Frozen commits this session: chat `943b712a64e96f8f8849a161ec20a11eb985ad7b`, chatchannels `b92fb42a1492140b8bb2e2ea1167db4fde2cf2a9`.
|
||||
|
||||
### Chat schema (v1.3.0, minus family_id)
|
||||
|
||||
[VERIFIED: chat/updates/version.yaml:1-26] `v1.3.0` is the last chat version (`root_id` + `thread_summaries`).
|
||||
|
||||
| Table | Columns / notes |
|
||||
|-------|-----------------|
|
||||
| `golem15_chat_conversations` | id, type `dm\|group` default dm, name nullable, created_by FK users cascade, timestamps, context_type varchar(255) nullable, context_id uint nullable; indexes: type, `conversations_context_index` (context_type, context_id). **No family_id. No idx_conversations_family_type.** |
|
||||
| `golem15_chat_participants` | id, conversation_id FK cascade, user_id FK cascade, last_read_at, is_muted default false, joined_at; unique (conversation_id, user_id); idx user_id; `idx_participants_read_status` |
|
||||
| `golem15_chat_messages` | id, conversation_id FK cascade, sender_id FK cascade, root_id nullable **no FK**, content nullable, type `text\|image\|system`, metadata json/jsonb, timestamps, edited_at, deleted_at; indexes conversation+created_at, sender_id, `idx_messages_thread` (conversation_id, root_id, created_at), `idx_messages_unread_count` |
|
||||
| `golem15_chat_thread_summaries` | id, root_id unique `uq_thread_summaries_root` **no FK**, reply_count default 0, last_reply_at, participant_ids json, timestamps |
|
||||
|
||||
### ChatChannels schema (v1.6.0)
|
||||
|
||||
[VERIFIED: chatchannels/updates/version.yaml:1-26] `v1.6.0` adds `users.handle`. That column is **not** a chatchannels migration in Go (D-15).
|
||||
|
||||
| Table | Notes |
|
||||
|-------|-------|
|
||||
| `golem15_chatchannels_channel_categories` | context_type+id, name, slug, position, timestamps, soft delete; unique (context_type, context_id, slug) |
|
||||
| `golem15_chatchannels_channels` | context, conversation_id unique nullable FK chat_conversations cascade, default_category_id FK set null, name, slug, description, visibility default public, is_archived, archived_at, is_system, is_default, timestamps, soft delete; unique (context_type, context_id, slug) |
|
||||
| `golem15_chatchannels_personal_categories` | user_id FK cascade, context, name, position, is_collapsed, timestamps; unique (user_id, context_type, context_id, name) |
|
||||
| `golem15_chatchannels_channel_user_prefs` | user_id, channel_id, is_favorite, placement_category_id FK `chatchannels_prefs_placement_cat_foreign` set null, is_muted, has_left, notify_pref varchar(16) default mentions, timestamps; unique (user_id, channel_id) |
|
||||
| `golem15_chatchannels_channel_members` | user_id, channel_id, timestamps; unique (user_id, channel_id) |
|
||||
| `golem15_chatchannels_channel_mentions` | conversation_id, message_id, mentioned_user_id FK `chatchannels_mentions_user_foreign` cascade, created_at nullable (denormalized message time), updated_at; unique (message_id, mentioned_user_id); **no FK to messages** |
|
||||
| `golem15_chatchannels_channel_pins` | channel_id, conversation_id, message_id, pinned_by FK cascade, timestamps; unique (channel_id, message_id); **no FK to channel/message** |
|
||||
| `golem15_chatchannels_user_context_prefs` | user_id FK cascade, context, last_active_channel_id nullable **no FK**, timestamps; unique (user_id, context_type, context_id) |
|
||||
|
||||
### ChatService methods to port (drop D-02)
|
||||
|
||||
Port: `getContextForUser`, `isChatAvailable`, `sendMessage`, `getOrCreateDm`, `getGroupChat`, `markAsRead`, `getConversationsForUser`, `getMessages` (rootsOnly), `getThreadReplies`, `canActAs`, `canView`, `sendTypingIndicator`, `getTotalUnreadCount`, `publishUnreadCount`, `addUserToGroupChat`, `getAvailableDmRecipients`, `canDeleteMessage`, `deleteMessage`, `canEditMessage`, `editMessage`, `toggleMute`.
|
||||
|
||||
Drop: `getFamilyGroupChat`, `addUserToFamilyGroupChat`.
|
||||
|
||||
### ChannelService methods
|
||||
|
||||
All public methods this session: `createChannel`, `adoptConversation`, `channelsFor`, `canAccessChannel`, `createCategory`, `addMember`, `editChannel`, `setVisibility`, `archiveChannel`, `restoreChannel`, `removeMember`, `severUserFromContext`, `renameCategory`, `deleteCategory`, `setPreference`, `setMuted`, `setNotifyPref`, `setCategoryCollapsed`, `personalCategoriesFor`, `createPersonalCategory`, `renamePersonalCategory`, `deletePersonalCategory`, `joinChannel`, `leaveChannel`, `setLastActiveChannel`, `lastActiveChannel`, `unreadCountForContext`, `preferencesFor`, `membersFor`, `accrueForMessage`, `publishMentionsChanged`, `reconcileForMessage`, `removeForMessage`, `recordMentions`, `reconcileMentions`, `removeMentions`, `pinMessage`, `unpinMessage`, `pinsFor`, `isPinned`, `pinCountFor`, `removePinsForMessage`, `mentionCountFor`, `mentionCountForContext`. Access via injected resolver; default deny-all [VERIFIED: chatchannels/classes/DenyAllChannelAccessResolver.php:10-18].
|
||||
|
||||
### Realtime events (golden set)
|
||||
|
||||
| Event | Channel(s) | Payload | Source |
|
||||
|-------|------------|---------|--------|
|
||||
| `created.chat.message` | context `getBroadcastChannel()` + `chat:conversation:{id}` | id, conversation_id, sender_id, root_id, type, action, created_at | ChatBroadcasting |
|
||||
| `typing.chat.message` | `chat:conversation:{id}` | conversation_id, user_id, action=`typing-start` | ChatService |
|
||||
| `chat:message` | `user:{id}` per other participant | conversation_id, message=toApiArray | Message.publishToParticipants |
|
||||
| `chat:read` | `user:{id}` others | conversation_id, user_id, read_at | ChatService |
|
||||
| `chat:unread` | `user:{id}` | count | ChatService |
|
||||
| `deleted.chat.message` | `chat:conversation:{id}` | id, conversation_id, action=`deleted` | Wavepath host (port on generic delete) |
|
||||
| `mentions.changed` | `user:{id}` | action=`mentions` | ChannelService [VERIFIED: chatchannels/classes/ChannelService.php:1029-1031] `'mentions.changed'` / `'action' => 'mentions'` |
|
||||
|
||||
Vue listens to `created.chat.message`, `typing.chat.message`, `mentions.changed` [VERIFIED: vue-wavepath-app/src/composables/useDmAlerts.ts:52] `created.chat.message`; [VERIFIED: vue-wavepath-app/src/composables/useRealTime.ts:368] `mentions.changed`. `chat:bot` is Phase 19.1.
|
||||
|
||||
### Commands
|
||||
|
||||
| PHP | Go |
|
||||
|-----|-----|
|
||||
| `chat:migrate-attachments` | **not ported** (D-19) |
|
||||
| (none else in chat Plugin.php besides that) | `chat:rewrite-context-types` recommended (D-12 discretion) |
|
||||
| Handle backfill is inside chatchannels migration | sm-user-plugin migration + optional `user:backfill-handles` |
|
||||
|
||||
### Stale Wavepath sqlite (2026-05-30) — not the frozen contract
|
||||
|
||||
Present on disk at `/media/nvme/dev/jz/wavepath.org/storage/database_fresh.sqlite` (1.3 MB, 2026-05-30). Do not treat this file as v1.3.0/v1.6.0. Use the PHP migrations as the schema source of truth.
|
||||
|
||||
## Discretion recommendations (planner defaults)
|
||||
|
||||
| Item | Recommendation |
|
||||
|------|----------------|
|
||||
| Packages | `chat`, `chatchannels`; IDs `golem15.chat`, `golem15.chatchannels` |
|
||||
| Context API | `chat.Context`, `CanActFor`, `chat.Registry` |
|
||||
| Events | `chat.MessageCreated`, `chat.MessageUpdated`, `chat.MessageDeleted` (fields: MessageID, ConversationID, RootID, ActorID) |
|
||||
| User events | `user.Created`, `user.Deleting` in sm-user-plugin |
|
||||
| Generic chat paths | `/_chat/api/v1/contexts/{type}/{id}/conversation`, `…/conversations/{id}/messages`, `…/messages/{id}`, `…/read`, `…/typing`, `…/mute`, `…/dm` (chat-dm bucket), `…/attachments`, `…/attachments/{messageId}/{fileId}`, `…/messages/{id}/replies`, `PUT …/threads/{rootId}/mute` (D-07; chat-owned store) |
|
||||
| Generic channel paths | `/_chatchannels/api/v1/contexts/{type}/{id}/channels` + the CommunityChannelController operations (categories, personal-categories, last-active, members, prefs, notify-pref, join/leave, pins, search) re-rooted off `{type}/{id}` not `{groupId}` |
|
||||
| Job | Kind `golem15.chat.maintain_thread_summary`, queue `broadcasts`, MaxAttempts 1 |
|
||||
| Config | `golem15.chat.use_ui_avatars` (env `SUMMER_GOLEM15__CHAT__USE_UI_AVATARS` and keep reading `CHAT_USE_UI_AVATARS` if compass allows; otherwise document the SUMMER_ form) |
|
||||
| Lang | EN/PL phrasebook; plugin.name `Chat` / `Czat`; drop family/parent/oversight |
|
||||
| D-12 | **Both:** idempotent SQL UPDATE for registered `LegacyPHPClass` values **and** `chat:rewrite-context-types` for hosts that register late |
|
||||
| Plan split (present and wait) | **5 plans:** (1) chat repo + baseline + models + registry + service + Bind/Emit + job; (2) chat generic API + attachments; (3) chatchannels repo + baseline + ChannelService + festival listeners + search; (4) chatchannels generic API + sm-user-plugin handle + user authorizer + lifecycle events; (5) unit tests + goldens + PHP map + `scripts/check-phase19.sh`. First planning commit also patches ROADMAP Repos + SC3. |
|
||||
| summercms.go | **No change** unless goldens force a lighthouse helper |
|
||||
| Repo create | `tea repos create --owner golem15` then analog go.mod/replace |
|
||||
| sm-user-plugin | Edit the submodule at `fonoteka.go/plugins/golem15/user` (origin `git.golem15.com/golem15/sm-user-plugin`); commit and push that repo first |
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
|
||||
- Frozen chat `943b712` and chatchannels `b92fb42` under `/media/nvme/dev/jz/wavepath.org/plugins/golem15/{chat,chatchannels}` (rev-parse this session)
|
||||
- `modules/lighthouse/README.md`, `lighthouse.go`, `centrifugo/proxy_test.go`
|
||||
- `modules/lagoon/attach/store.go`
|
||||
- `modules/beachcomber/searchable.go`
|
||||
- `modules/festival/README.md`
|
||||
- `modules/conga/README.md`
|
||||
- Analog: `sm-journal-plugin/go.mod`, `sm-paymentgatewaysubscriptions-plugin` (baseline, subscriber, routes, fixture)
|
||||
- Wavepath `routes.php`, `Plugin.php`, `CommunityChatController.php`, `vue-wavepath-app/src/composables/{useDmAlerts,useRealTime}.ts`
|
||||
- `.planning/notes/core-plugins-own-repos.md`, `core-plugin-port-inventory.md`, `plugin-layout-winter-directories.md`
|
||||
- 18.1-CONTEXT.md D-04 / D-08 / D-09 / D-16
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
|
||||
- Typesense collections [CITED: https://typesense.org/docs/latest/api/collections.md] — `default_sorting_field` documented as `int32 / float`; int64 field type exists
|
||||
- Typesense v0.15.0 changelog [CITED: https://typesense.org/docs/0.15.0/] — "Allow default sorting field to be an int64"
|
||||
- ASVS 5.0 V5 File Handling [CITED: https://github.com/OWASP/ASVS/blob/v5.0.0/5.0/en/0x14-V5-File-Handling.md]
|
||||
- OWASP File Upload Cheat Sheet [CITED: https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html]
|
||||
- Stale Wavepath sqlite (2026-05-30) — not the frozen contract
|
||||
|
||||
### Tertiary (LOW confidence)
|
||||
|
||||
- Live MySQL `wave2` schema (not readable this session)
|
||||
- CONTEXT "about 130" chatchannels tests (counted 106)
|
||||
|
||||
## Metadata
|
||||
|
||||
**Confidence breakdown:**
|
||||
- Standard stack: HIGH — analog go.mod + STACK.md Read this session; no new packages
|
||||
- Architecture: HIGH — PHP services/migrations and lighthouse/attach/festival/beachcomber/conga Read this session
|
||||
- Pitfalls: HIGH for contract footguns (prefix, ChannelID segment, morph, content-on-socket); MEDIUM for live DB / D-07 width
|
||||
|
||||
**Research date:** 2026-10-16
|
||||
**Valid until:** 2026-11-15 (30 days; PHP sources are frozen commits)
|
||||
|
||||
**Graphify:** disabled (`graphify.enabled` false). No graph context.
|
||||
|
||||
**lighthouse gap check:** Publisher, Bind, Emit, Registry/Authorizer, Mount surfaces, MemoryDriver, `["prs"]` capabilities field, `broadcasts` default queue — all present. **No summercms.go work identified.**
|
||||
95
.planning/phases/19-chat-and-chatchannels/19-VALIDATION.md
Normal file
95
.planning/phases/19-chat-and-chatchannels/19-VALIDATION.md
Normal file
@@ -0,0 +1,95 @@
|
||||
---
|
||||
phase: "19"
|
||||
slug: "chat-and-chatchannels"
|
||||
# status lifecycle: draft (seeded by plan-phase) → validated (set by validate-phase §6)
|
||||
# audit-milestone §5.5 distinguishes NOT-VALIDATED (draft) from PARTIAL (validated + nyquist_compliant: false) (#2117)
|
||||
status: draft
|
||||
nyquist_compliant: false
|
||||
wave_0_complete: false
|
||||
created: "2026-10-16"
|
||||
---
|
||||
|
||||
# Phase 19 — Validation Strategy
|
||||
|
||||
> Per-phase validation contract for feedback sampling during execution. Source: `19-RESEARCH.md` § Validation Architecture.
|
||||
|
||||
---
|
||||
|
||||
## Test Infrastructure
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Framework** | Go `testing` + testcontainers-go v0.44.0 (Postgres); Docker tests skip under `-short` |
|
||||
| **Config file** | none at repo root — each plugin is its own module (`go test ./...`); analog `testmain_test.go` (`flag.Parse()` + `testing.Short()`) |
|
||||
| **Quick run command** | `go vet ./... && go test -short ./...` inside each touched plugin repo |
|
||||
| **Full suite command** | `go test ./...` (testcontainers Postgres + lighthouse memory + beachcomber null/fake) |
|
||||
| **Phase gate** | `scripts/check-phase19.sh` (create in last plan) requiring named tests across chat, chatchannels and sm-user-plugin |
|
||||
| **Estimated runtime** | quick ~30 s per repo; full Docker suite ~5–10 min |
|
||||
|
||||
---
|
||||
|
||||
## Sampling Rate
|
||||
|
||||
- **After every task commit:** `go vet ./...` and `go test -short ./...` in the touched plugin module
|
||||
- **After every plan wave:** `go test ./...` in that plugin (Docker)
|
||||
- **Before `/msd-verify-work`:** `scripts/check-phase19.sh` green
|
||||
- **Max feedback latency:** 60 seconds for the quick run
|
||||
|
||||
---
|
||||
|
||||
## Per-Task Verification Map
|
||||
|
||||
Seeded from RESEARCH.md until plans exist. The planner replaces Task IDs with real plan/task numbers.
|
||||
|
||||
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
||||
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
||||
| 19-W0-01 | 01 | 1 | SC-19-1 | — | Baseline creates chat v1.3.0 tables minus family_id | integration | `go test -short ./updates -run TestBaseline` | ❌ W0 | ⬜ pending |
|
||||
| 19-W0-02 | 03 | 2 | SC-19-1 | — | ChannelService.createChannel / adoptConversation | integration | `go test ./classes -run TestCreateChannel` | ❌ W0 | ⬜ pending |
|
||||
| 19-W0-03 | 01 | 1 | SC-19-2 | T-19-05 | chat authorizer participants + prs | unit | `go test -short ./classes -run TestChatAuthorizer` | ❌ W0 | ⬜ pending |
|
||||
| 19-W0-04 | 01 | 1 | SC-19-2 | T-19-04 | MemoryDriver sees created.chat.message notification payload | integration | `go test ./... -run TestBroadcastCreated` | ❌ W0 | ⬜ pending |
|
||||
| 19-W0-05 | 02 | 1 | SC-19-3 | — | toApiArray golden (attachments kind/width/height) | unit | `go test -short ./models -run TestToAPIArray` | ❌ W0 | ⬜ pending |
|
||||
| 19-W0-06 | 05 | 3 | SC-19-3 | — | Event name/channel/payload goldens | unit | `go test -short ./... -run TestGoldenEvents` | ❌ W0 | ⬜ pending |
|
||||
| 19-W0-07 | 04 | 2 | D-15 | — | handle unique + generator charset | unit | `go test -short ./classes -run TestHandle` in sm-user-plugin | ❌ W0 | ⬜ pending |
|
||||
| 19-W0-08 | 02 | 1 | D-18 | T-19-03 | banned svg/html/xml; max 5; 100MB | unit | `go test -short ./classes -run TestAttachmentPolicy` | ❌ W0 | ⬜ pending |
|
||||
| 19-W0-09 | 03 | 2 | D-20 | T-19-08 | SearchableAs `messages`; fail-open indexer | unit | `go test -short ./classes -run TestSearchIndexer` | ❌ W0 | ⬜ pending |
|
||||
| 19-W0-10 | 05 | 3 | D-21 | — | PHP method map 59 + 106 | unit (last plan) | `go test ./...` + checklist file | ❌ W0 | ⬜ pending |
|
||||
| 19-W0-11 | 05 | 3 | D-02 | — | no family/parent/child/QuestStream in production sources | unit | `go test -short ./... -run TestNoQuestStreamTrace` | ❌ W0 | ⬜ pending |
|
||||
| 19-W0-12 | 02 | 1 | D-09 | T-19-02 | chat-dm 10/min; generic 422 CANNOT_CREATE_DM | unit | `go test -short ./... -run TestDMThrottle` | ❌ W0 | ⬜ pending |
|
||||
| 19-W0-13 | 02 | 1 | D-18 | T-19-01 | attachment download 404 for missing and unauthorized | unit | `go test -short ./... -run TestAttachmentDownload` | ❌ W0 | ⬜ pending |
|
||||
|
||||
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
||||
|
||||
---
|
||||
|
||||
## Wave 0 Requirements
|
||||
|
||||
- [ ] Each new repo: `go.mod`, `plugin.go`, `updates` baseline, `harness_test.go` / `testmain_test.go` (`flag.Parse` + `testing.Short`)
|
||||
- [ ] `testdata/goldens/` for serializers and events (capture from Wavepath PHP)
|
||||
- [ ] `acme.teams` fixture plugin (test files only)
|
||||
- [ ] PHP→Go test method map markdown in the last plan
|
||||
- [ ] `scripts/check-phase19.sh` at the framework repo (names tests across the three plugin modules)
|
||||
- [ ] sm-user-plugin handle migration + authorizer tests
|
||||
|
||||
Existing framework tests (`go test ./modules/lighthouse/...`, beachcomber, attach) already cover the seams; they do **not** cover chat behavior.
|
||||
|
||||
---
|
||||
|
||||
## Manual-Only Verifications
|
||||
|
||||
| Behavior | Requirement | Why Manual | Test Instructions |
|
||||
|----------|-------------|------------|-------------------|
|
||||
| Capture Wavepath serializer and event goldens | SC-19-3 | Needs a live PHP Wavepath backend | Record `toApiArray` and Centrifugo payloads from Wavepath; store under `testdata/goldens/` |
|
||||
| Repo create / first push | Discretion | `tea repos create` and remote push need a human | `tea repos create --owner golem15` then analog go.mod/replace; push main |
|
||||
|
||||
---
|
||||
|
||||
## Validation Sign-Off
|
||||
|
||||
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
|
||||
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
|
||||
- [ ] Wave 0 covers all MISSING references
|
||||
- [ ] No watch-mode flags
|
||||
- [ ] Feedback latency < 60s
|
||||
- [ ] `nyquist_compliant: true` set in frontmatter
|
||||
|
||||
**Approval:** pending
|
||||
9
.planning/phases/19-chat-and-chatchannels/COVERAGE.md
Normal file
9
.planning/phases/19-chat-and-chatchannels/COVERAGE.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# API Coverage — Phase 19
|
||||
|
||||
No external API integration: Phase 19 ports first-party `golem15.chat` and `golem15.chatchannels` onto existing lighthouse, lagoon/attach, festival, beachcomber and conga seams. It does not call a third-party HTTP/SDK surface.
|
||||
|
||||
> Full coverage by default. Opt-outs are explicit, reasoned decisions.
|
||||
|
||||
| capability | decision | reason |
|
||||
|---|---|---|
|
||||
| (none — no external API) | — | first-party plugin port only |
|
||||
@@ -206,8 +206,8 @@
|
||||
],
|
||||
"next": {
|
||||
"command": "/msd:progress --next",
|
||||
"label": "Advance to the next step (plan phase 19)",
|
||||
"reason": "Phase 19 of 40 — needs a plan"
|
||||
"label": "Advance to the next step",
|
||||
"reason": "Phase 19 of 40 · executing"
|
||||
},
|
||||
"updated_at": "2026-10-16T19:08:06.654Z"
|
||||
"updated_at": "2026-10-16T23:27:40.776Z"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user