docs(19.1): record planned phase
This commit is contained in:
@@ -3,16 +3,16 @@ msd_state_version: "1.0"
|
||||
milestone: v1.0
|
||||
current_phase: "19.1"
|
||||
current_phase_name: Chat satellites
|
||||
status: planning
|
||||
status: executing
|
||||
stopped_at: Phase 19.7 context gathered
|
||||
last_updated: "2026-10-18T10:41:48.616Z"
|
||||
last_updated: "2026-10-18T10:56:18.421Z"
|
||||
last_activity: 2026-10-17
|
||||
last_activity_desc: Phase 19 complete, transitioned to Phase 19.1
|
||||
state_head: b8f90749c714a0e83c8648315729d05a2639888a
|
||||
state_head: cddf3a5008d9832a881f89900a961aa6281c05d8
|
||||
progress:
|
||||
total_phases: 41
|
||||
completed_phases: 33
|
||||
total_plans: 198
|
||||
total_plans: 204
|
||||
completed_plans: 198
|
||||
gsd_state_version: "1.0"
|
||||
milestone_name: milestone
|
||||
@@ -29,9 +29,9 @@ See: .planning/PROJECT.md (updated 2026-10-12)
|
||||
|
||||
## Current Position
|
||||
|
||||
Phase: 19.1 — Chat satellites
|
||||
Phase: 19.1 (Chat satellites) — READY TO EXECUTE
|
||||
Plan: Not started
|
||||
Status: Ready to plan
|
||||
Status: Ready to execute
|
||||
Last activity: 2026-10-17 — Phase 19 complete, transitioned to Phase 19.1
|
||||
|
||||
Progress: [████████░░] 80%
|
||||
|
||||
619
.planning/phases/19.1-chat-satellites/19.1-PATTERNS.md
Normal file
619
.planning/phases/19.1-chat-satellites/19.1-PATTERNS.md
Normal file
@@ -0,0 +1,619 @@
|
||||
# Phase 19.1: Chat satellites - Pattern Map
|
||||
|
||||
**Mapped:** 2026-10-17
|
||||
**Files analyzed:** 52 (grouped by the RESEARCH six-plan split)
|
||||
**Analogs found:** 51 / 52 (1 with no analog: D-17 markdown consume — forum plan blocks; 8 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/` | ROADMAP SC2/SC3/Repos, three `.planning/notes/` decision notes, `scripts/check-phase19.1.sh`. **No lighthouse / fetchguard / conga / festival / seaglass / flare change** (RESEARCH: no satellite gap). D-17 markdown module is a **separate `/msd-quick`**, not these plans. |
|
||||
| `CHAT/` | `sm-chat-plugin/` | Primary analog (Phase 19 shipped). Winter dirs, squashed `IF NOT EXISTS` baseline, `/_x/api/v1`, jwt.auth groups, Buckets, lighthouse authorizer that splits segment 2, `user.Deleting` listener, test-only `acme` host. |
|
||||
| `CH/` | `sm-chatchannels-plugin/` | Primary analog for deny-all seam + setter + Boot Warn, `Requires()` of chat+user, context-scoped generic API, `SetAccessResolver`. |
|
||||
| `USR/` | `fonoteka.go/plugins/golem15/user/` | Tracked checkout of `sm-user-plugin` (`git ls-files` from inside the submodule). **Do not edit.** `CreatedEventName` / `DeletingEventName` already exist. Satellites only `Listen[*userclasses.DeletingEvent]`. |
|
||||
| `PG/` | `sm-paymentgateway-plugin/` | `pact.HasSchedule` + `pact.Every` (video reconciler 5 min). Decision-note analog for a plugin third-party dep. |
|
||||
| `UF/` | `sm-userfriends-plugin/` | **Does not exist yet.** Create with `tea repos create --owner golem15 sm-userfriends-plugin`. |
|
||||
| `FORUM/` | `sm-forum-plugin/` | **Does not exist yet.** Same. Execution **blocks on D-17**. |
|
||||
| `BOTS/` | `sm-chatbots-plugin/` | **Does not exist yet.** Same. |
|
||||
| `HERALD/` | `sm-herald-plugin/` | **Does not exist yet.** Same. Decision note **before** importing webpush-go. |
|
||||
| `VIDEO/` | `sm-chatvideo-plugin/` | **Does not exist yet.** Same. Decision note **before** importing livekit SDK. |
|
||||
| `PHP-*` | `/media/nvme/dev/jz/wavepath.org/plugins/golem15/{chatbots,chatvideo,forum,herald,userfriends}` | Frozen contract (read-only). HEADs: chatbots `1ecf8c8`, chatvideo `25135b4`, forum `ad10b54`, herald `a749f83`, userfriends `182b514`. |
|
||||
|
||||
**Replace paths (locked, Phase 19 pitfall 9 / `CHAT/go.mod:147`):** sibling plugin `go.mod` uses `replace git.golem15.com/golem15/summercms => ../summercms.go`. Do **not** copy `core-plugins-own-repos.md`'s `../../../../summercms.go` into the sibling checkout (that path is for an app submodule mount). `go.work` also replaces `sm-user-plugin` → `../fonoteka.go/plugins/golem15/user`. Plugins that hard-require chat add `replace …/sm-chat-plugin => ../sm-chat-plugin` (`CH/go.work:7`).
|
||||
|
||||
**Plugin IDs / packages (discretion, RESEARCH default):** `golem15.userfriends` / `userfriends`; `golem15.forum` / `forum`; `golem15.chatbots` / `chatbots`; `golem15.herald` / `herald`; `golem15.chatvideo` / `chatvideo`. Tables keep PHP prefixes. `models/` is a leaf (`.planning/notes/plugin-layout-winter-directories.md`).
|
||||
|
||||
**D-03 / P19 D-02:** no production identifier, string, comment or README names QuestStream, family, parent, child, Wavepath, or Fonoteka. Copy `CHAT/fixture_plugin_test.go` `TestFixtureAbsentFromProduction` (retired-token grep) into every satellite.
|
||||
|
||||
Suggested plan split from RESEARCH (present at the plan-count checkpoint; unit tests last): **19.1-01** userfriends repo + baseline + services + events + generic API tracer; **19.1-02** forum repo + baseline + ForumService + DeleteContext + generic API tracer (**blocks on D-17**); **19.1-03** chatbots repo + flags + registry/dispatcher + generic API tracer; **19.1-04** **webpush-go decision note** then herald repo + subscriptions + sender job + generic push tracer; **19.1-05** **LiveKit (+ bluemonday if D-17 has not written it) decision notes** then chatvideo repo + sessions + wrapper + authorizer + reconciler + generic video tracer; **19.1-06** unit tests + goldens + PHP maps + `scripts/check-phase19.1.sh`. First planning commit also patches ROADMAP Repos + SC2 (drop Golem; name webpush-go / LiveKit / bluemonday notes) + SC3 (already met).
|
||||
|
||||
## File Classification
|
||||
|
||||
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|
||||
|-------------------|------|-----------|----------------|---------------|
|
||||
| **Plan 1 — userfriends** | | | | |
|
||||
| `UF/go.mod`, `UF/go.work` | config | — | `CHAT/go.mod` + `CHAT/go.work` (sibling replace `../summercms.go`; user → fonoteka submodule) | exact |
|
||||
| `UF/README.md` | docs | — | `CHAT/README.md` (neutral names; never Wavepath / Fonoteka) | exact |
|
||||
| `UF/plugin.go` | provider | request-response | `CH/plugin.go` (`HasRoutes`/`Buckets`/`HasCommands` optional; **no** hard `golem15.chat` in `Requires`) | exact |
|
||||
| `UF/boot.go` | provider | event-driven (boot) | `CHAT/boot.go` `registerBootStep` + `CH/boot.go` `Listen[*userclasses.DeletingEvent]` | exact |
|
||||
| `UF/models/{context,friendship,group,group_member,group_invite,group_ban}.go` | model | CRUD | `CHAT/models/conversation.go` (`TableName`, `init(){Register}`, PHP column tags) | exact |
|
||||
| `UF/models/registry.go` | utility | — | `CHAT/models/registry.go` | exact |
|
||||
| `UF/classes/{friendship,group}_service.go` | service | CRUD | `CHAT/classes/service.go` (`New`, `lagoon.Transaction`, typed Fire, `WithDB`) | exact |
|
||||
| `UF/classes/{group_policy,group_visibility_gate}.go` | service | request-response | `CH/classes/channel_service.go` access checks + frozen PHP GroupPolicy matrix | role-match |
|
||||
| `UF/classes/friends_context_adapter.go` | provider | — | `CHAT/fixture_plugin_test.go` `reg.Add` + `CHAT/contracts/registry.go` `SetProvider` (soft: `app.Lookup[*chatcontracts.Registry]`, no `Requires` chat) | exact (soft) |
|
||||
| `UF/events/events.go` | model (event types) | pub-sub | `CHAT/events/events.go` (in-tx Fire, `Tx` field, grep-able `…EventName`) | exact |
|
||||
| `UF/routes.go` + `UF/api/*.go` | route/controller | request-response | `CHAT/routes.go` + `CHAT/api/{auth,errors,json,conversations}.go` | exact |
|
||||
| `UF/updates/2026*_create_golem15_userfriends_baseline.go` | migration | CRUD | `CHAT/updates/202610160001_create_golem15_chat_baseline.go` (`IF NOT EXISTS`; demo-seed v1.0.1 ID as comment, **no rows**) | exact |
|
||||
| `UF/config/config.yaml` + `UF/lang/{en,pl}/lang.yaml` | config | transform | `CHAT/config/config.yaml` + `CHAT/lang/en/lang.yaml` (plugin.name `Friends` / `Przyjaciele`) | exact |
|
||||
| **Plan 2 — forum (blocks on D-17)** | | | | |
|
||||
| `FORUM/go.mod` … `plugin.go` | config/provider | — | same as userfriends; `Requires()` = `golem15.user` only (never `sm-userfriends-plugin`) | exact |
|
||||
| `FORUM/contracts/access_resolver.go` + `FORUM/classes/deny_all.go` | model/interface | request-response | `CH/contracts/access_resolver.go` + `CH/classes/deny_all.go` | exact |
|
||||
| `FORUM/classes/forum_service.go` (`DeleteContext`) | service | CRUD | `CHAT/classes/service.go` + `CH/boot.go` `onUserDeleting` cascade. **Go adds** `DeleteContext` (PHP had a `class_exists(FriendGroup)` hook) | role-match |
|
||||
| `FORUM/console/rewrite_context_types.go` | utility | batch | `CHAT/console/rewrite_context_types.go` + `CHAT/classes/writes.go` `RewriteContextTypes` (forum **owns** `golem15_forum_categories.context_type`) | exact |
|
||||
| `FORUM/routes.go` + `FORUM/api/*.go` | route/controller | request-response | `CH/routes.go` (`/_x/api/v1/contexts/{type}/{id}/…`) | exact |
|
||||
| `FORUM/models/{category,thread,post,thread_read}.go` + baseline | model/migration | CRUD | `CHAT/models/*` + chat baseline (soft deletes; `user_id` nullable SET NULL) | exact |
|
||||
| Forum write-path `Render`/`Purify` | service | transform | **none in-tree.** D-17 not landed. Do **not** copy `FW/modules/cabana/field_markdown.go` or `FW/modules/seaglass/seaglass.go` | none |
|
||||
| **Plan 3 — chatbots** | | | | |
|
||||
| `BOTS/plugin.go` | provider | request-response | `CH/plugin.go`; `Requires()` = `golem15.chat`, `golem15.user`. **No Golem.** | exact |
|
||||
| `BOTS/contracts/{slash_command,bot_auth_resolver}.go` + `BOTS/classes/deny_all.go` | model/interface | request-response | `CH/contracts/access_resolver.go` + `CH/classes/deny_all.go` (`BotAuthResolver` deny-all + setter + Boot Warn) | exact |
|
||||
| `BOTS/classes/{registry,dispatcher}.go` | service | request-response | `CHAT/contracts/registry.go` (`Add` rejects duplicates) + PHP reserved `help` / `registerBuiltin` | role-match |
|
||||
| `BOTS/models/bot_flag.go` + GDPR listener | model | event-driven | `CHAT/models/conversation.go` + `CHAT/boot.go` `onUserDeleting` (delete `bot_flags` row) | exact |
|
||||
| `BOTS/routes.go` + `BOTS/api/*.go` | route/controller | request-response | `CHAT/api/auth.go` (`frontendUser`; actor from JWT, never body — CMD-06) | exact |
|
||||
| **Plan 4 — herald (note first)** | | | | |
|
||||
| `FW/.planning/notes/2026-10-*-webpush-go.md` | docs | — | `FW/.planning/notes/2026-10-13-paymentgateway-hashids-go-hashids.md` (plugin dep, version, why not hand-roll). **Not** `FW/modules/flare`. | exact |
|
||||
| `HERALD/plugin.go` | provider | request-response | `CHAT/plugin.go` (`HasJobs` + `HasCommands` + `HasRoutes`). PHP `$require` Apparatus → **empty** `Requires()`; no user-model import beyond id | exact |
|
||||
| `HERALD/contracts/{herald_profile,presence_gate}.go` + deny-all PresenceGate | model/interface | request-response | `CH/classes/deny_all.go` for PresenceGate. HeraldProfile default is PHP `NeutralHeraldProfile` (app-name-only), **not** deny-all | role-match |
|
||||
| `HERALD/classes/subscription_service.go` | service | CRUD | `CHAT/classes/service.go` (subscribe validates HTTPS+public; unique `(user_id, endpoint)`) | exact |
|
||||
| `HERALD/jobs/send.go` | service (job) | event-driven | `CHAT/jobs/maintain_thread_summary.go` + `CHAT/classes/service.go:336-343` `Enqueue` on caller `tx` | exact |
|
||||
| `HERALD/console/vapid_generate.go` | utility | — | `CHAT/console/rewrite_context_types.go` (bonfire.Command). Name `herald:vapid-generate`. **Not** `flare.GenerateVAPIDKeysCommandName` | role-match |
|
||||
| `HERALD/routes.go` + `HERALD/api/*.go` | route/controller | request-response | `CHAT/routes.go` (`/_herald/api/v1/push/…`) | exact |
|
||||
| Herald outbound HTTP | service | request-response | `FW/modules/fetchguard/{client,policy}.go` `PublicOnlyMode`; `Client.Do` is webpush-go `HTTPClient` (no adapter type) | exact |
|
||||
| **Plan 5 — chatvideo (notes first)** | | | | |
|
||||
| `FW/.planning/notes/2026-10-*-livekit-*.md` | docs | — | hashids note + RESEARCH LiveKit v2 path `github.com/livekit/server-sdk-go/v2` v2.19.0; flag pion/media-sdk/buildkit | exact |
|
||||
| `FW/.planning/notes/2026-10-*-bluemonday-forum-*.md` | docs | — | `FW/.planning/notes/2026-10-11-framework-html-sanitizer-bluemonday.md` for version pin **only**. Forum policy is **not** seaglass. Skip if D-17 already wrote the named-policy note | role-match |
|
||||
| `VIDEO/plugin.go` | provider | request-response | `CHAT/plugin.go` (`HasJobs` + `HasCommands` + `HasSchedule` + `HasRoutes`). `Requires()` = `golem15.chat` (PHP WebSockets → lighthouse, not a plugin require) | exact |
|
||||
| `VIDEO/classes/livekit.go` (`RoomAPI`) | service | request-response | new SDK behind an interface; HTTP through `fetchguard.TrustedMode`. Golden JWT analog is PHP MapClaims, mint with jwt/v5 if SDK grant JSON differs | role-match |
|
||||
| `VIDEO/classes/authorizer.go` | middleware | request-response | `CHAT/classes/authorizer.go` (`parseChatConversationChannel` three segments; **do not** `lighthouse.ChannelID`) | exact (with footgun) |
|
||||
| `VIDEO/classes/{video_service,notifier}.go` | service | pub-sub | `CHAT/classes/{service,emit}.go`; publish failure **logged not raised** (PHP VideoCallNotifier) — chat `SendMessage` returns Emit errors; video swallows | role-match |
|
||||
| `VIDEO/jobs/reconcile.go` + `VIDEO/console/reconcile.go` + `HasSchedule` | service (job) | batch | `CHAT/jobs/maintain_thread_summary.go` + `PG/schedule.go` `pact.Every(5*time.Minute)`. Generic: empty/gone room or `max_duration` only (D-14). No GuestCall / call cards | exact |
|
||||
| `VIDEO/models/{session,participant}.go` + baseline | model/migration | CRUD | `CHAT/models/*` + chat baseline. Record demo-seed v1.0.1 ID as comment. `active_conversation_id` unique; no `updated_at`; `CREATED_AT=started_at` | exact |
|
||||
| `VIDEO/routes.go` + `VIDEO/api/*.go` | route/controller | request-response | `CHAT/routes.go` (named buckets: `video-initiate` 5/min on **start only**, `call-link` 10/min, general API bucket) | exact |
|
||||
| **Plan 6 — tests + gate** | | | | |
|
||||
| `{UF,FORUM,BOTS,HERALD,VIDEO}/testmain_test.go` | test | — | `CHAT/testmain_test.go` (`flag.Parse` + `testing.Short`) | exact |
|
||||
| `{…}/harness_test.go` | test | — | `CHAT/harness_test.go` + `CH/harness_test.go` (`realtime.driver=memory`; chatbots/chatvideo also boot `golem15.chat`) | exact |
|
||||
| `{…}/fixture_plugin_test.go` (`acme`) | test | — | `CHAT/fixture_plugin_test.go` (`acme.teams`, `TestFixtureAbsentFromProduction`) | exact |
|
||||
| `{…}/http_tracer_test.go` | test | request-response | `CHAT/http_tracer_test.go` (`bootWeb`, `frontendToken`, `call`, IDOR → 404) | exact |
|
||||
| `{…}/testdata/goldens/` + `golden_test.go` | test | transform | `CHAT/golden_test.go` + `CHAT/testdata/goldens/` | exact |
|
||||
| `{…}/testdata/PHP_METHOD_MAP.md` + `php_method_map_test.go` | test | — | `CHAT/php_method_map_test.go` + `CHAT/testdata/PHP_METHOD_MAP.md` | exact |
|
||||
| `{…}/updates/baseline_test.go` | test | — | `CHAT/updates/baseline_test.go` | exact |
|
||||
| LiveKit Twirp fake + push `HTTPClient` stub | test | request-response | `FW/modules/fetchguard/client.go` `WithTransport` for push; LiveKit is a new `RoomAPI` fake (do not import pion) | role-match |
|
||||
| `FW/scripts/check-phase19.1.sh` | test gate | batch | `FW/scripts/check-phase19.sh` (stages, sibling env overrides, fail-closed named tests; five plugin roots) | exact |
|
||||
| `FW/.planning/ROADMAP.md` Repos + SC2 + SC3 | docs | — | same file (first planning commit) | exact (extend) |
|
||||
| `USR/classes/events.go` | — | — | **do not touch** (Pitfall 14) | n/a |
|
||||
|
||||
## Pattern Assignments
|
||||
|
||||
### `UF|FORUM|BOTS|HERALD|VIDEO/go.mod` + `go.work` (config)
|
||||
|
||||
**Analog:** `CHAT/go.mod` lines 1–14 and 147; `CHAT/go.work` whole file; `CH/go.work` for chat replace.
|
||||
|
||||
```
|
||||
module git.golem15.com/golem15/sm-userfriends-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
|
||||
gorm.io/gorm v1.31.2
|
||||
)
|
||||
replace git.golem15.com/golem15/summercms => ../summercms.go
|
||||
```
|
||||
|
||||
`go.work`:
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
- chatbots and chatvideo also `require` `sm-chat-plugin` and `replace … => ../sm-chat-plugin` (`CH/go.work:7`).
|
||||
- userfriends **must not** require chat in `go.mod` (soft adapter). Test files may import chat only behind build tags / the optional Lookup; if the adapter test needs chat, the **test host** `go.work` may replace chat without the production `plugin.go` `Requires()`.
|
||||
- herald `go.mod` adds `github.com/SherClockHolmes/webpush-go v1.4.0` **after** the decision note commit.
|
||||
- chatvideo `go.mod` adds `github.com/livekit/server-sdk-go/v2 v2.19.0` **after** the decision note. Production sources talk to `RoomAPI` only; tests must not import `pion`.
|
||||
- forum `go.mod` must **not** require bluemonday or goldmark; it consumes D-17.
|
||||
|
||||
### `*/plugin.go` (provider)
|
||||
|
||||
**Analog:** `CHAT/plugin.go` (112 lines) and `CH/plugin.go` (95 lines).
|
||||
|
||||
Interface assertions (`CHAT/plugin.go:29-39`); drop `HasJobs`/`HasCommands`/`HasSchedule` when the plugin has none:
|
||||
|
||||
```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)
|
||||
_ surf.BucketProvider = (*Plugin)(nil)
|
||||
)
|
||||
```
|
||||
|
||||
- `const PluginID = "golem15.<name>"` (`CHAT/plugin.go:42`).
|
||||
- `//go:embed config` / `//go:embed lang` (`CHAT/plugin.go:48-52`).
|
||||
- `Register` stores `p.app`; `Boot` → `p.runBootSteps(app)` (`CHAT/plugin.go:66-75`).
|
||||
- `Migrations() → updates.All()`, `Models() → models.All()`.
|
||||
- `init() { party.Register(&Plugin{}) }`.
|
||||
- Package doc: one paragraph, no consuming-app name.
|
||||
|
||||
**Requires() (PHP → Go, RESEARCH verified):**
|
||||
|
||||
| Plugin | `Requires()` |
|
||||
|--------|----------------|
|
||||
| userfriends | `{"golem15.user"}` only — chat is soft |
|
||||
| forum | `{"golem15.user"}` |
|
||||
| chatbots | `{"golem15.chat", "golem15.user"}` — **never** Golem |
|
||||
| herald | `nil` / empty (PHP Apparatus dissolved; no user-model import beyond id) |
|
||||
| chatvideo | `{"golem15.chat"}` (PHP also `$require` WebSockets → lighthouse, not a plugin) |
|
||||
|
||||
**Setter analog** (`CH/plugin.go:83-90`) for ForumAccessResolver, BotAuthResolver, PresenceGate:
|
||||
|
||||
```go
|
||||
func (p *Plugin) SetAccessResolver(r contracts.AccessResolver) {
|
||||
if p == nil || p.service == nil {
|
||||
return
|
||||
}
|
||||
p.service.SetAccessResolver(r)
|
||||
}
|
||||
```
|
||||
|
||||
**Jobs analog:** `CHAT/plugin.go:85-100` + `FW/modules/conga/job.go:53-64`:
|
||||
|
||||
```go
|
||||
func (p *Plugin) Jobs() []pact.Job {
|
||||
return []pact.Job{
|
||||
conga.Job(p.maintainThreadSummary, conga.OnQueue(jobs.Queue), conga.MaxAttempts(1)),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Herald: Kind `golem15.herald.send`, queue `default` or `push`. Video reconcile job: Kind `golem15.chatvideo.reconcile_sessions`, `MaxAttempts(1)`.
|
||||
|
||||
**Schedule analog:** `PG/plugin.go:41` `_ pact.HasSchedule` and `PG/schedule.go:15-22`. Video:
|
||||
|
||||
```go
|
||||
func (p *Plugin) Schedule() []pact.ScheduledCommand {
|
||||
return []pact.ScheduledCommand{{
|
||||
Command: "chatvideo:reconcile-sessions",
|
||||
Cadence: pact.Every(5 * time.Minute),
|
||||
}}
|
||||
}
|
||||
```
|
||||
|
||||
`pact.Every` must divide 24h (`FW/modules/pact/capabilities.go:131-136`; `FW/modules/conga/schedule.go:84-86`). 5 minutes does.
|
||||
|
||||
### `*/boot.go` (provider, boot)
|
||||
|
||||
**Analog:** `CHAT/boot.go:22-52` (`registerBootStep` / `runBootSteps`, errors `"%s: boot %s: %w"`) and `CH/boot.go:49-68` (deny-all Warn + `lagoon.OnDatabase`).
|
||||
|
||||
```go
|
||||
func (p *Plugin) runBootSteps(app *backpack.App) error {
|
||||
for _, step := range bootSteps {
|
||||
if err := step.fn(p, app); err != nil {
|
||||
return fmt.Errorf("%s: boot %s: %w", PluginID, step.name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
Deny-all Warn (`CH/boot.go:64-66`):
|
||||
|
||||
```go
|
||||
if _, ok := svc.Resolver().(classes.DenyAll); ok {
|
||||
slog.Default().Warn(PluginID + ": ChannelAccessResolver is deny-all; the host must SetAccessResolver")
|
||||
}
|
||||
```
|
||||
|
||||
Copy that string shape for `ForumAccessResolver`, `BotAuthResolver`, `PresenceGate`. HeraldProfile default is Neutral (app-name-only) — **no** Warn for the profile; Warn only while PresenceGate is DenyAll.
|
||||
|
||||
**GDPR listener analog:** `CHAT/boot.go:176-193`:
|
||||
|
||||
```go
|
||||
app.Events.Listen[*userclasses.DeletingEvent](PluginID, p.onUserDeleting)
|
||||
```
|
||||
|
||||
Import `userclasses "git.golem15.com/golem15/sm-user-plugin/classes"`. Use `svc.WithDB(ev.Tx)` (`CHAT/classes/service.go:95-108`). Cascades (PHP, D-03):
|
||||
|
||||
- chatbots: delete `golem15_chatbots_bot_flags` for that `user_id`
|
||||
- forum: `user_id = null` on threads and posts; keep bodies
|
||||
- userfriends: heir oldest admin else oldest member else hard-delete empty group; then delete member and ban rows (order by `role`, not MySQL `FIELD()`)
|
||||
|
||||
**Soft chat adapter (userfriends only):** `CHAT/README.md:44-50` + `CHAT/contracts/registry.go:132-165`. In Boot, `reg, ok := app.Lookup[*chatcontracts.Registry]()`; if `!ok` skip. If ok, `reg.Add(PluginID, Registration{Alias: "friendcontext", …})` and optionally `reg.SetProvider(...)`. Never `Requires()` chat.
|
||||
|
||||
**Video authorizer registration analog:** `CHAT/boot.go:83-86`:
|
||||
|
||||
```go
|
||||
return svc.Registry().Register("video", classes.AuthorizeVideoLookup(func() *gorm.DB {
|
||||
gdb, _ := app.Lookup[*gorm.DB]()
|
||||
return gdb
|
||||
}))
|
||||
```
|
||||
|
||||
`user:{id}` for `video:call:incoming` is already authorized by `USR/classes/authorizer.go:16-30`. Satellites must **not** re-register `user`.
|
||||
|
||||
### Deny-all seams
|
||||
|
||||
**Analog:** `CH/contracts/access_resolver.go` (whole file) and `CH/classes/deny_all.go` (whole file). Short-test analog: `CH/tracer_test.go:67-76` `TestAccessResolverDefault`.
|
||||
|
||||
```go
|
||||
type DenyAll struct{}
|
||||
var _ contracts.AccessResolver = DenyAll{}
|
||||
func (DenyAll) CanAccessContext(context.Context, uint, string, uint) bool { return false }
|
||||
func (DenyAll) CanManageContext(context.Context, uint, string, uint) bool { return false }
|
||||
```
|
||||
|
||||
`CH/classes/channel_service.go:60-117`: `New` installs DenyAll when Resolver is nil; `SetAccessResolver(nil)` becomes DenyAll.
|
||||
|
||||
Apply:
|
||||
|
||||
| Seam | Default | Methods |
|
||||
|------|---------|---------|
|
||||
| `ForumAccessResolver` | DenyAll | can-view / can-moderate (names at executor discretion; PHP ForumAccessResolver) |
|
||||
| `BotAuthResolver` | DenyAll | resolve bearer → user id (deny = no bot) |
|
||||
| `PresenceGate` | DenyAll | `ShouldSend` always false until host installs |
|
||||
| `HeraldProfile` | Neutral (app name only) | **not** DenyAll |
|
||||
|
||||
### `*/classes/*_service.go` (service, CRUD)
|
||||
|
||||
**Analog:** `CHAT/classes/service.go:1-108` and `CH/classes/channel_service.go:1-117`.
|
||||
|
||||
- Package doc: published as `*classes.Service` (or named type), found with `app.Lookup[*classes.Service]()`.
|
||||
- Sentinel errors prefixed `golem15.<name>:` (`CHAT/classes/service.go:26-40`).
|
||||
- `Options{DB, Bus, …}`; `New` refuses nil DB.
|
||||
- Writes go through `lagoon.Transaction`. Fire festival events **inside** the tx (`CHAT/classes/service.go:175-201`):
|
||||
|
||||
```go
|
||||
err = lagoon.Transaction(ctx, s.db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
if err := tx.Create(msg).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if s.bus != nil {
|
||||
if err := s.bus.Fire(ctx, &events.MessageCreated{MessageEvent: events.MessageEvent{
|
||||
MessageID: msg.ID, /* … */ Tx: tx,
|
||||
}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return s.jobs.Enqueue(ctx, tx, jobs.MaintainThreadSummaryArgs{RootID: *rootID}, conga.EnqueueOpts{
|
||||
Queue: jobs.Queue, MaxAttempts: 1,
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
Herald `SendEvent` / `SendUnconditional` must Enqueue on the **caller's tx** the same way (`CHAT/classes/service.go:336-343`) so a rolled-back write pushes nothing (D-11).
|
||||
|
||||
**Forum `DeleteContext`:** no PHP method. Implement on ForumService: soft-delete categories (and cascaded threads/posts) for morph `(type, id)`. Do not import userfriends. Analog for the cascade style: `CH/boot.go:85-101` (`SeverUserFromContext` loop) — own the SQL in forum.
|
||||
|
||||
### `*/events/events.go` (userfriends)
|
||||
|
||||
**Analog:** `CHAT/events/events.go` whole file.
|
||||
|
||||
```go
|
||||
const GroupCreatedEventName = "userfriends.GroupCreated"
|
||||
const GroupDeletedEventName = "userfriends.GroupDeleted"
|
||||
const MemberJoinedEventName = "userfriends.MemberJoined"
|
||||
const MemberLeftEventName = "userfriends.MemberLeft"
|
||||
|
||||
type GroupEvent struct {
|
||||
GroupID uint
|
||||
ActorID uint
|
||||
MemberID uint
|
||||
Tx *gorm.DB
|
||||
}
|
||||
```
|
||||
|
||||
Grep-able one-constant-per-line. Fire inside the write tx. RESEARCH field set: GroupID, ActorID, MemberID.
|
||||
|
||||
**Do not add** `user.Created` / `user.Deleting` — already at `USR/classes/events.go:9-12`:
|
||||
|
||||
```go
|
||||
const CreatedEventName = "user.Created"
|
||||
const DeletingEventName = "user.Deleting"
|
||||
```
|
||||
|
||||
### `*/models/*.go` + `updates/*baseline.go`
|
||||
|
||||
**Analog:** `CHAT/models/conversation.go` (whole file) + `CHAT/models/registry.go` + `CHAT/updates/registry.go` + `CHAT/updates/202610160001_create_golem15_chat_baseline.go:12-28`.
|
||||
|
||||
```go
|
||||
func (Conversation) TableName() string { return "golem15_chat_conversations" }
|
||||
func init() { Register(&Conversation{}) }
|
||||
```
|
||||
|
||||
Baseline: one gormigrate ID, every statement `CREATE TABLE IF NOT EXISTS` / `CREATE INDEX IF NOT EXISTS`, PHP table/column/index/constraint names. Types TIMESTAMPTZ / JSONB. Enums → `varchar` + `CHECK`. `CHAT/updates/registry.go:27-36` `run(stmts...)`. `init() { Register(createBaseline...) }`.
|
||||
|
||||
Demo-seed version IDs (chatvideo `v1.0.1`, userfriends `v1.0.1`): comment in the baseline file; **do not** `INSERT` demo users (D-02 / D-07).
|
||||
|
||||
Herald FK to `users` only if `users` exists — PHP `create_push_subscriptions_table.php` is conditional; Go test host always has `golem15.user` so the FK can stay. Production adopt of a DB without users is out of scope (herald still must not import user **models**).
|
||||
|
||||
Forum `golem15_forum_categories`: `context_type` NOT NULL, **no FK**; unique `(context_type, context_id, slug)`.
|
||||
|
||||
Chatvideo session: no `updated_at`; GORM `CREATED_AT=started_at` (PHP VideoSession). Unique `active_conversation_id`. `join_code` alphabet `abcdefghijklmnopqrstuvwxyz0123456789` length 10.
|
||||
|
||||
**Baseline short test analog:** `CHAT/updates/baseline_test.go` — grep table names in the source file; runs under `-short`.
|
||||
|
||||
### `*/routes.go` + `*/api/*.go` (controller, request-response)
|
||||
|
||||
**Analog:** `CHAT/routes.go` (whole file) + `CH/routes.go`; handlers `CHAT/api/auth.go`, `CHAT/api/errors.go`, `CHAT/api/json.go`, `CHAT/api/conversations.go`.
|
||||
|
||||
```go
|
||||
const APIPrefix = "/_userfriends/api/v1"
|
||||
const APIBucket = "userfriends-api"
|
||||
|
||||
func (p *Plugin) Routes(r pact.Router) error {
|
||||
r.Group(APIPrefix, surf.Use("jwt.auth", "throttle:"+APIBucket), func(g pact.Router) {
|
||||
// …
|
||||
})
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
Discretion paths (RESEARCH defaults):
|
||||
|
||||
| Plugin | Prefix / routes |
|
||||
|--------|-----------------|
|
||||
| userfriends | `/_userfriends/api/v1/friends` (list, requests, sent, request, accept, reject, cancel, unfriend) + `/groups` (members, invites, bans) |
|
||||
| forum | `/_forum/api/v1/contexts/{type}/{id}/categories` + threads/posts/read/unread |
|
||||
| chatbots | `GET /_chatbots/api/v1/commands`, `POST /_chatbots/api/v1/dispatch` body `{input}` |
|
||||
| herald | `POST/PATCH/DELETE /_herald/api/v1/push/subscription`, `GET /_herald/api/v1/push/vapid-public-key` |
|
||||
| chatvideo | `/_chatvideo/api/v1/conversations/{id}/video/{start,join,leave,end,status}` + `/_chatvideo/api/v1/call-links/{code}` (conversation id primary; optional context lookup through chat registry) |
|
||||
|
||||
**Auth analog** (`CHAT/api/auth.go:16-45`): actor id from `bouncer.User`; backend principal is not an actor; missing JWT → 401 `"Authentication required"`. CMD-06: never take invoker id from the body.
|
||||
|
||||
**Error analog** (`CHAT/api/errors.go:32-50`, `CH/api/errors.go:41-46`): `wire.WriteJSON`; missing / unauthorized / unknown type → **404** `"Not found"` (IDOR collapse); 422 validation map; 500 logs `golem15.<name> api:`.
|
||||
|
||||
**Buckets analog** (`CHAT/routes.go:54-70`). Key = frontend user id else IP. RESEARCH rate table:
|
||||
|
||||
| Bucket | Max/min | Where |
|
||||
|--------|---------|-------|
|
||||
| general per API | 120 (chat analog) | remaining routes |
|
||||
| `community-join` | **10** | friend request; group join/preview if in generic API. **Not** 5 |
|
||||
| `video-initiate` | 5 | generic video **start only** (PHP lived on dropped `/api/video/initiate`; D-05 still carries it) |
|
||||
| `call-link` | 10 | `GET call-links/{code}` |
|
||||
|
||||
`lighthouse.Mount` (`FW/modules/lighthouse/route.go:62-90`) is the **app's** job: plugin only declares `surf.Use("jwt.auth")`. Guest-call buckets are out of scope.
|
||||
|
||||
### lighthouse video authorizer (do not use ChannelID)
|
||||
|
||||
**Analog:** `CHAT/classes/authorizer.go:17-99`. Footgun: `FW/modules/lighthouse/channel.go:35-46` — `ChannelID("video:session:9")` reads segment 1 `"session"` → 0.
|
||||
|
||||
```go
|
||||
func parseVideoSessionChannel(channel string) (uint, bool) {
|
||||
channel = strings.TrimPrefix(channel, "presence:")
|
||||
parts := strings.Split(channel, ":")
|
||||
if len(parts) != 3 || parts[0] != "video" || parts[1] != "session" {
|
||||
return 0, false
|
||||
}
|
||||
// digits-only parts[2], ParseUint, id != 0 — copy parseChatConversationChannel
|
||||
}
|
||||
```
|
||||
|
||||
Allow only participants of the session's **conversation**; capabilities `["prs"]` (`CHAT/classes/authorizer.go:42-48`). Register namespace `"video"` (`FW/modules/lighthouse/registry.go:64-84` — second Register of the same namespace errors).
|
||||
|
||||
**Emit analog:** `CHAT/classes/emit.go:21-26` + `FW/modules/lighthouse/suppress.go:56-81`. Video notifier: if `Emit` fails, `slog` and return nil (PHP VideoCallNotifier 73-75). Events/channels from RESEARCH table (`video:call:incoming` → `user:{id}`; others → `video:session:{id}`).
|
||||
|
||||
### Morph rewrite (`FORUM/console/rewrite_context_types.go`)
|
||||
|
||||
**Analog:** `CHAT/console/rewrite_context_types.go` whole file + `CHAT/classes/writes.go:220-241`.
|
||||
|
||||
Forum ships `forum:rewrite-context-types` for `golem15_forum_categories.context_type` so forum can adopt without chat. Walk `LegacyPHPClass` → alias. Do not extend chat's command.
|
||||
|
||||
userfriends FriendContext adapter registers alias `friendcontext` with `LegacyPHPClass` `Golem15\UserFriends\Models\FriendContext` (and groups `friendgroup` / `Golem15\UserFriends\Models\FriendGroup` if it registers those). Chat's rewrite already covers conversation rows; forum's covers category rows.
|
||||
|
||||
### `*/console/*.go` (commands)
|
||||
|
||||
**Analog:** `CHAT/console/rewrite_context_types.go:16-35` (`bonfire.Command{Name, Description, Run}`).
|
||||
|
||||
| PHP / D-ID | Go name |
|
||||
|------------|---------|
|
||||
| `herald:vapid-generate` | `summer herald:vapid-generate` — prints `VAPID_PUBLIC_KEY=` / `VAPID_PRIVATE_KEY=` lines; **never log** the private key |
|
||||
| (none in chatvideo plugin) | `summer chatvideo:reconcile-sessions` |
|
||||
| `wave:reconcile-video-sessions` | **not this phase** (Wavepath thin alias later) |
|
||||
|
||||
Anti-analog: `FW/modules/flare/commands.go:25-28` `websockets:generate-vapid-keys`. Herald must not import `modules/flare`.
|
||||
|
||||
### fetchguard (herald send + LiveKit HTTP)
|
||||
|
||||
**Analog:** `FW/modules/fetchguard/policy.go:17-30` and `FW/modules/fetchguard/client.go:40-51, 79-86`.
|
||||
|
||||
```go
|
||||
public, err := fetchguard.NewClient(fetchguard.Policy{Mode: fetchguard.PublicOnlyMode}, cfg)
|
||||
// webpush-go Options.HTTPClient = public // Do(*http.Request) (*http.Response, error)
|
||||
// NO wrapper type (Pitfall 8)
|
||||
```
|
||||
|
||||
Subscribe-time: reject non-HTTPS / non-public hosts with the same policy (fail-closed; PHP `AllowAllEndpointGuard` was fail-open). Tests: `fetchguard.WithTransport` (`client.go:54-68`). EndpointGuard PHP tests → N/A + pointer to fetchguard's own tests (Pitfall 13).
|
||||
|
||||
LiveKit admin HTTP: `Policy{Mode: fetchguard.TrustedMode}` (operator URL, may be `http://localhost:7880`). Never TrustedMode for a user-supplied push endpoint.
|
||||
|
||||
### LiveKit wrapper (`VIDEO/classes/livekit.go`)
|
||||
|
||||
No in-tree SDK analog. Copy the **service wrapper** shape from `CHAT/classes/service.go` (`Options`, refuse nil, log). RESEARCH interface:
|
||||
|
||||
```go
|
||||
type RoomAPI interface {
|
||||
CreateRoom(ctx context.Context, name string) error
|
||||
DeleteRoom(ctx context.Context, name string) error // missing-room swallow (D-13), even though PHP LiveKitService::deleteRoom does not
|
||||
RemoveParticipant(ctx context.Context, room, identity string) error
|
||||
ParticipantCount(ctx context.Context, name string) (int, error) // missing-room → 0 (PHP does swallow)
|
||||
ActiveRoomNames(ctx context.Context) ([]string, error)
|
||||
}
|
||||
```
|
||||
|
||||
Token claims golden vs PHP `LiveKitService.php:80-99` (HS256, `iss` API key, `sub` user id string, `name`, `iat`/`nbf`/`exp`, nested `video` grant, optional JSON `metadata`). If `protocol/auth` `ToJWT` differs, mint with `jwt/v5` MapClaims **inside the same wrapper**. Admin URL fallback: `livekit_admin_url` else rewrite `wss://`/`ws://` → `https://`/`http://`. Room name: `chat-conversation-{id}-{16 hex}`. Do not construct `lksdk.Room`. Tests ship a fake; do not import pion.
|
||||
|
||||
D-14 reconciler: if room missing/empty or `started_at + max_duration` elapsed → end session + deleteRoom (swallowed) + `video:call:ended`. **No** GuestCall, call cards, CallNotifier, `start_grace_seconds` (Wavepath-only).
|
||||
|
||||
### Decision notes (before importing commits)
|
||||
|
||||
**Analog:** `FW/.planning/notes/2026-10-13-paymentgateway-hashids-go-hashids.md` (plugin-owned library: version, license, why not hand-roll, exact import path, consequences for `go.mod`).
|
||||
|
||||
| Note | Must name | Must not |
|
||||
|------|-----------|----------|
|
||||
| webpush-go | `github.com/SherClockHolmes/webpush-go` **v1.4.0**; `Options.HTTPClient` = `*fetchguard.Client`; TTL 2419200 / urgency `normal`; prune 404/410 | `flare`, `push.*`, `websockets:generate-vapid-keys` |
|
||||
| LiveKit | module path **`github.com/livekit/server-sdk-go/v2` v2.19.0** (not v1); `RoomAPI` interface; golden JWT; pion/media-sdk/buildkit flagged | constructing `lksdk.Room` in tests |
|
||||
| bluemonday forum policy | v1.0.27 already in framework `go.mod`; **named forum policy** (AllowElements PHP list, schemes http/https/mailto, `RequireNoFollowOnLinks(true)`, `AddTargetBlankToFullyQualifiedLinks(true)`) | `seaglass.UGCPolicy`, `cabana.RenderMarkdown`, forum-owned goldmark |
|
||||
|
||||
Seaglass anti-analog (`FW/modules/seaglass/seaglass.go:22-32`): `RequireNoFollowOnLinks(false)`, allows `tel:`. Cabana anti-analog (`FW/modules/cabana/field_markdown.go:14-34`): goldmark **without** GFM + regex gate. Existing `2026-10-11-framework-html-sanitizer-bluemonday.md` is the richeditor/seaglass decision — do not reuse it as the forum policy note.
|
||||
|
||||
### Test host
|
||||
|
||||
**Analog:** `CHAT/testmain_test.go` (whole file) + `CHAT/harness_test.go:51-134` + `CH/harness_test.go` (adds `golem15.chat` + `search.driver=null`) + `CHAT/fixture_plugin_test.go:24-125, 141-185`.
|
||||
|
||||
```go
|
||||
func TestMain(m *testing.M) {
|
||||
flag.Parse()
|
||||
if !testing.Short() {
|
||||
// testcontainers postgres
|
||||
}
|
||||
code := m.Run()
|
||||
stopPostgres()
|
||||
os.Exit(code)
|
||||
}
|
||||
```
|
||||
|
||||
Default plugins:
|
||||
|
||||
| Repo | `party.Activate` ids |
|
||||
|------|----------------------|
|
||||
| userfriends | `golem15.user`, `golem15.userfriends`, `acme.*` (fixture if groups need a context) |
|
||||
| forum | `golem15.user`, `golem15.forum`, `acme.*` (allow-all test resolver installed after Boot) |
|
||||
| chatbots | `golem15.user`, `golem15.chat`, `golem15.chatbots` |
|
||||
| herald | `golem15.user` (JWT only), `golem15.herald` — plugin itself still must not import user models |
|
||||
| chatvideo | `golem15.user`, `golem15.chat`, `golem15.chatvideo` |
|
||||
|
||||
Env: `SUMMER_REALTIME__DRIVER=memory`. Fixture plugin ID `acme.teams` / `acme` — `TestFixtureAbsentFromProduction` greps it out of production sources plus QuestStream/family/parent/child.
|
||||
|
||||
**HTTP tracer analog:** `CHAT/http_tracer_test.go:35-79, 91-149` (`surf.Assemble`, `usercontrollers.IssueToken`, `Authorization: Bearer`, stranger → 404). RESEARCH tracers: userfriends A→B request/accept; forum GFM table + no `<script>`; chatbots `/ping` ephemeral; herald POST sub + SendUnconditional + 410 prune + rollback sends 0; chatvideo unique active session + JWT golden + authorizer stranger deny + reconcile empty room.
|
||||
|
||||
**PHP method map analog:** `CHAT/php_method_map_test.go:19-78` + `CHAT/testdata/PHP_METHOD_MAP.md`. Counts: chatbots 22, forum 25, herald 14, userfriends 31, chatvideo **0 in-plugin** (map Wavepath host methods to generic-API Go or explicit N/A: guest, call cards, CallNotifier). Herald EndpointGuard pin/reject → `not-applicable (D-10)`. Components / demo seeders → `not-applicable (D-07)`.
|
||||
|
||||
**Goldens analog:** `CHAT/golden_test.go:16-58`. Capture serializer payloads, lighthouse `MemoryDriver.Publications()`, LiveKit JWT claims, forum semantic HTML (normalized DOM, not byte-identical — D-18). Adopted `content_html` is served stored, never re-rendered on read.
|
||||
|
||||
### `FW/scripts/check-phase19.1.sh`
|
||||
|
||||
**Analog:** `FW/scripts/check-phase19.sh:1-75` (stages `self-test` / `plugins` / `docs` / `postgres`; env overrides `CHAT=` style; fail-closed named tests; a skip or miss fails).
|
||||
|
||||
Five sibling roots (`USERFRIENDS`, `FORUM`, `CHATBOTS`, `HERALD`, `CHATVIDEO`). A task that only touches forum does **not** stand in for herald (Nyquist). Named short tests from RESEARCH validation table (`TestBaseline` ×5, `TestHTTPTracer` ×5, `TestAccessResolverDefault`, `TestLiveKitTokenClaims`, `TestVideoAuthorizer`, `TestSubscribeEndpoint`, `TestNoGolem`, `TestNoUserfriendsImport`, `TestPHPMethodMap`). Postgres stage: tracers + `TestSendEventRollback` + GDPR cascades. Decision-note paths grepped for locked module path + version.
|
||||
|
||||
### `FW/.planning/ROADMAP.md` (first planning commit)
|
||||
|
||||
**Analog:** same file lines 1223-1236. Patch in the planning-docs commit (not a code commit):
|
||||
|
||||
- **Repos:** add `summercms.go` for D-17.
|
||||
- **SC2:** drop "Chatbots is wired to `sm-golem-plugin`…". Name webpush-go / LiveKit / bluemonday notes.
|
||||
- **SC3:** mark already met (fonoteka PHP tree = Wavepath `182b514`).
|
||||
|
||||
## Shared Patterns
|
||||
|
||||
### Authentication
|
||||
**Source:** `CHAT/api/auth.go:16-45`; `CHAT/routes.go:23-28`; `USR/classes/authorizer.go:16-30` (`user:{id}` already registered).
|
||||
**Apply to:** every generic satellite route.
|
||||
Bearer frontend JWT. Backend principals are not actors. Actor id from the token, never the body (CMD-06). App mounts `jwt.auth`; plugin declares the guard name. IDOR → 404. Bot bearer (chatbots) goes through `BotAuthResolver` deny-all until the host installs one.
|
||||
|
||||
### Rate limits
|
||||
**Source:** `CHAT/routes.go:54-70`.
|
||||
**Apply to:** all five generic APIs.
|
||||
Named surf buckets. Key = frontend user id else client IP. 429 body stays surf `{"message":"Too Many Attempts."}`. `community-join` is 10/min, not 5. `video-initiate` 5/min on start only.
|
||||
|
||||
### Error / JSON wire
|
||||
**Source:** `CHAT/api/errors.go:14-50`; `CHAT/api/conversations.go:13-31`; `wire.WriteJSON`.
|
||||
**Apply to:** all HTTP handlers.
|
||||
Flat `{"error":…}`. 422 `{"error":"Validation failed","errors":{field:[message]}}`. Missing, unknown type, and unauthorized collapse to 404. Never leak SQL / vendor text. One serializer set for the generic API and future `/_wave` aliases.
|
||||
|
||||
### Festival events (in-transaction)
|
||||
**Source:** `CHAT/events/events.go`; `CHAT/classes/service.go:195-201`; `CHAT/boot.go:183`; `USR/classes/events.go:9-54`.
|
||||
**Apply to:** userfriends group lifecycle; all GDPR listeners; herald must Enqueue inside the same tx.
|
||||
`Listen[*userclasses.DeletingEvent]`. Fire after the row is saved, carry `Tx`. Listener error fails the caller. No model patching of another plugin (P19 D-13). Satellites do not import each other's types (D-08): forum `DeleteContext` is generic; the **app** wires group-deleted.
|
||||
|
||||
### Realtime
|
||||
**Source:** `CHAT/classes/authorizer.go`; `CHAT/classes/emit.go`; `CHAT/boot.go:62-86`; `FW/modules/lighthouse/{registry,channel,suppress,route}.go`.
|
||||
**Apply to:** chatvideo (and any satellite Emit).
|
||||
`lighthouse.From` at Boot; `Registry.Register` per namespace (once); split three-segment channels yourself; `Emit` on the write tx; `MemoryDriver` for goldens; `Mount` is the app's job. `user:{id}` is owned by sm-user-plugin.
|
||||
|
||||
### Outbound HTTP
|
||||
**Source:** `FW/modules/fetchguard/{client,policy}.go`.
|
||||
**Apply to:** herald send + subscribe URL check (PublicOnly); LiveKit admin URL (TrustedMode only).
|
||||
`Client.Do` matches webpush-go `HTTPClient`. No adapter type. No `EndpointGuard` seam.
|
||||
|
||||
### Jobs
|
||||
**Source:** `CHAT/plugin.go:85-100`; `CHAT/jobs/maintain_thread_summary.go:16-29`; `CHAT/classes/service.go:336-343`; `FW/modules/conga/job.go:53-64`; `PG/schedule.go:15-22`.
|
||||
**Apply to:** herald send (one job per send, enqueue in caller tx); video reconcile (periodic 5 min + console command).
|
||||
Plugins return `conga.Job(...)` from `HasJobs` and never import River. Kind strings `golem15.<plugin>.<verb>`.
|
||||
|
||||
### Test host
|
||||
**Source:** `CHAT/{testmain,harness,fixture_plugin,http_tracer}_test.go`; `CH/harness_test.go`.
|
||||
**Apply to:** all five new repos.
|
||||
`flag.Parse` + `testing.Short`; testcontainers Postgres; lighthouse memory; `acme` fixture absent from production; `IssueToken` for frontend JWT. Fakes for LiveKit `RoomAPI` and push HTTP.
|
||||
|
||||
### Docs / README
|
||||
**Source:** `CHAT/README.md:1-16` (H1, one-sentence summary, import, `**Name:**` mnemonic, IDs, Overview…).
|
||||
**Apply to:** all five plugin READMEs. Decision notes copy the hashids note structure.
|
||||
Neutral names (`acme`, `blog`). Identifier-checked. Config keys reviewed by hand. No Wavepath / Fonoteka / QuestStream / family.
|
||||
|
||||
### Plugin layout
|
||||
**Source:** `FW/.planning/notes/plugin-layout-winter-directories.md:11-26`.
|
||||
**Apply to:** all five repos.
|
||||
Winter directories as Go packages; `models/` is a leaf (no sibling imports). `api/` for generic JSON (chat analog; PHP used `controllers/api` in the host).
|
||||
|
||||
## No Analog Found
|
||||
|
||||
| File | Role | Data Flow | Reason |
|
||||
|------|------|-----------|--------|
|
||||
| Forum write-path consume of D-17 `Render`/`Purify` | service | transform | No dedicated markdown module this session (`modules/*markdown*` absent). `cabana.RenderMarkdown` is a regex gate without GFM. `seaglass` turns nofollow off and allows `tel:`. Forum plan **blocks** until the quick task exports `Render`/`Purify` with a named forum policy. Do not vendor goldmark/bluemonday in `sm-forum-plugin`. |
|
||||
|
||||
These are the weakest matches (planner should lean on RESEARCH.md + frozen PHP; still copy the listed Go analog for layout):
|
||||
|
||||
| File | Role | Data Flow | Why partial |
|
||||
|------|------|-----------|-------------|
|
||||
| `BOTS/classes/{registry,dispatcher}.go` | service | request-response | No slash-command registry in Go. Duplicate rejection copies `CHAT/contracts/registry.go`. Reserved `help` + `registerBuiltin` + modes `ephemeral`/`announce` port from PHP. Unrecognized trigger returns nil (fall through). |
|
||||
| `HERALD` NeutralHeraldProfile | service | transform | Deny-all analog does not apply. Port PHP NeutralHeraldProfile (app-name-only). PresenceGate is the fail-closed gate. |
|
||||
| `VIDEO/classes/livekit.go` | service | request-response | No LiveKit client in-tree. Wrap official v2 SDK behind `RoomAPI`; golden-test PHP JWT claims. |
|
||||
| `HERALD` webpush send | service | request-response | Do not copy `flare`. webpush-go + fetchguard PublicOnly. OBS-01 cooldown: process-local `sync.Map` 60s (PHP in-process fallback), not Redis. |
|
||||
| `FORUM/classes/forum_service.go` `DeleteContext` | service | CRUD | PHP had a `class_exists(FriendGroup)` hook, not this method. New Go API; cascade style from chatchannels GDPR loop. |
|
||||
| `VIDEO/classes/notifier.go` publish errors | service | pub-sub | Chat `SendMessage` returns `Emit` errors. PHP VideoCallNotifier logs and does not raise — video follows PHP. |
|
||||
| LiveKit Twirp fake | test | request-response | New. Interface fake only; no pion, no `lksdk.Room`. |
|
||||
| `UF/classes/{group_policy,group_visibility_gate}.go` | service | request-response | No matrix policy in Go chat. Copy ChannelService "check then 404" and port PHP GroupPolicy tests 1:1. |
|
||||
|
||||
## Anti-patterns (planner / executor)
|
||||
|
||||
- Forum-owned `classes/markdown.go` or `import seaglass` / `cabana.RenderMarkdown` / `html.WithUnsafe`.
|
||||
- Herald `import …/modules/flare` or config keys `push.*`.
|
||||
- `lighthouse.ChannelID` on `video:session:{id}`.
|
||||
- `userfriends` `Requires()` containing `golem15.chat`.
|
||||
- `forum/go.mod` requiring `sm-userfriends-plugin`.
|
||||
- Chatbots importing Golem.
|
||||
- Editing `USR/classes/events.go` / adding handle-style user migrations.
|
||||
- Porting Wavepath `ReconcileVideoSessions` GuestCall / call cards / CallNotifier / `start_grace_seconds`.
|
||||
- Re-rendering adopted `content_html` on read (D-18).
|
||||
- `video-initiate` missing on generic start; `community-join` copied as 5/min.
|
||||
- Extra `type fetchguardDoer struct` around `*fetchguard.Client`.
|
||||
- LiveKit v1 module path (`github.com/livekit/server-sdk-go` without `/v2`).
|
||||
|
||||
## Metadata
|
||||
|
||||
**Analog search scope:** `sm-chat-plugin`, `sm-chatchannels-plugin`, `fonoteka.go/plugins/golem15/user` (`sm-user-plugin` submodule), `sm-paymentgateway-plugin`, `summercms.go/modules/{lighthouse,fetchguard,conga,pact,seaglass,flare,cabana}`, `scripts/check-phase19.sh`, `.planning/notes/{2026-10-13-paymentgateway-hashids-go-hashids,2026-10-11-framework-html-sanitizer-bluemonday,plugin-layout-winter-directories,core-plugins-own-repos}.md`. Frozen PHP trees used only as contract (not analogs to copy Go style from).
|
||||
|
||||
**Files scanned:** ~55 analog sources Read this session; every named analog path `git ls-files`-verified (plugin repos from inside that repo; user plugin from inside the submodule).
|
||||
|
||||
**Pattern extraction date:** 2026-10-17
|
||||
Reference in New Issue
Block a user