test(19): verify Chat and ChatChannels phase

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jakub Zych
2026-10-17 22:18:23 +02:00
parent aaf75cfb41
commit f3d5db8cba

View File

@@ -0,0 +1,301 @@
---
phase: 19-chat-and-chatchannels
verified: 2026-10-17T20:17:10Z
status: gaps_found
score: 23/26 must-haves verified
covered_files:
- .planning/phases/19-chat-and-chatchannels/19-01-PLAN.md
- .planning/phases/19-chat-and-chatchannels/19-01-SUMMARY.md
- .planning/phases/19-chat-and-chatchannels/19-02-PLAN.md
- .planning/phases/19-chat-and-chatchannels/19-02-SUMMARY.md
- .planning/phases/19-chat-and-chatchannels/19-03-PLAN.md
- .planning/phases/19-chat-and-chatchannels/19-03-SUMMARY.md
- .planning/phases/19-chat-and-chatchannels/19-04-PLAN.md
- .planning/phases/19-chat-and-chatchannels/19-04-SUMMARY.md
- .planning/phases/19-chat-and-chatchannels/19-05-PLAN.md
- .planning/phases/19-chat-and-chatchannels/19-05-SUMMARY.md
- scripts/check-phase19.sh
covered_digest: "v2:sha256:1410fefe1f88be6c4049cf116774fdb44c4ecdf205ed983b8580bfba08138c96"
covered_files_note: "verification.fingerprint covers only paths under the summercms.go root. Sibling implementation read this pass: sm-chat-plugin 4d0e764 (plugin, boot, routes, classes/service.go canSend, authorizer, goldens, PHP_METHOD_MAP); sm-chatchannels-plugin 9026cd6 (plugin, boot, ChannelService, search.go, baselines, goldens); sm-user-plugin under fonoteka.go/plugins/golem15/user c077029 (handle migration, HandleGenerator, user authorizer, user.Deleting). Orchestrator recorded bash scripts/check-phase19.sh --all green; this pass did not re-run the postgres suite."
behavior_unverified: 0
overrides_applied: 0
decision_coverage:
honored: 21
total: 21
not_honored: []
gaps:
- truth: "D-08 / SC-19-1: conversation- and message-level routes check participation on every call; ChatService.sendMessage ports 1:1 (PHP requires hasParticipant)"
status: failed
reason: "CR-01 still open. canSend returns true for a non-participant when the conversation host reports HasMember. GetOrCreateDm stores the host alias on the DM row, so any teammate can POST into another pair's DM. PHP ChatService::sendMessage requires $conversation->hasParticipant($sender) with no HasMember fallback. HTTP tracers only prove a non-member of the context is 404."
artifacts:
- path: "../sm-chat-plugin/classes/service.go"
issue: "canSend (lines 255-273) counts participants then falls back to host.HasMember"
- path: "../sm-chat-plugin/api/messages.go"
issue: "PostMessage calls svc.SendMessage with no extra participant check; a context member who is not a DM participant gets 201"
missing:
- "Require participation only in canSend (return isParticipant); keep GetGroupChat lazy-join via addParticipant"
- "Postgres case: two context members open a DM; a third context member POSTs to that conversation id and must get 404"
- truth: "D-13 GDPR: deleting a user severs that user from channels without CASCADE-wiping conversations remaining members still use"
status: failed
reason: "CR-02 still open. golem15_chat_conversations.created_by is NOT NULL ON DELETE CASCADE. golem15_chatchannels_channels.conversation_id is also ON DELETE CASCADE. FormAfterDelete fires user.Deleting then force-deletes the user. SeverUserFromContext removes that user's members/prefs/participants and does not reassign created_by. Deleting the conversation creator therefore deletes the conversation, then the channel, then remaining members' history. TestDeletingCascade deletes Bob as a member of Alice's channel, so it never observes creator deletion."
artifacts:
- path: "../sm-chat-plugin/updates/202610160001_create_golem15_chat_baseline.go"
issue: "created_by REFERENCES users (id) ON DELETE CASCADE (lines 26-27)"
- path: "../sm-chatchannels-plugin/updates/202610160001_create_golem15_chatchannels_baseline.go"
issue: "conversation_id REFERENCES golem15_chat_conversations (id) ON DELETE CASCADE (lines 48-49)"
- path: "../sm-chatchannels-plugin/classes/channel_service.go"
issue: "SeverUserFromContext (lines 754-787) does not reassign created_by"
- path: "../fonoteka.go/plugins/golem15/user/controllers/users_admin_controller.go"
issue: "FormAfterDelete fires user.Deleting then unscoped-deletes the user row"
missing:
- "Before the user row is deleted, reassign created_by to another participant on every conversation the user created (keep PHP constraint names; do not ALTER the IF NOT EXISTS baseline CASCADE)"
- "Extend TestDeletingCascade so Alice creates the channel, Alice is deleted, and Bob's member row and the channel row still exist"
deferred:
- truth: "Route-level parity of /_wave is checked at the Wavepath port"
addressed_in: "Wavepath app port (not a numbered SummerCMS phase; SC-19-3 last sentence)"
evidence: "ROADMAP SC-19-3: Route-level parity of /_wave is checked at the Wavepath port."
- truth: "Vue chat UI, satellites (chatbots, chatvideo, forum, herald, userfriends), and chat:migrate-attachments stay out"
addressed_in: "Phase 19.1 (satellites) and D-19 (migrate-attachments run on PHP before cutover)"
evidence: "Phase 19.1 goal: remaining shared plugins Wavepath requires. D-19: chat:migrate-attachments is not ported."
---
# Phase 19: Chat and ChatChannels Verification Report
**Phase Goal:** `Golem15.Chat` and `Golem15.ChatChannels` run on SummerCMS as shared plugins, with realtime delivery through `lighthouse`, ready for the Wavepath port.
**Verified:** 2026-10-17T20:17:10Z
**Status:** gaps_found
**Re-verification:** No — initial verification
**MVP note:** ROADMAP marks this phase `mode: mvp` but the ROADMAP goal is not in user-story form (`user-story.validate` → `valid: false`). PLAN.md goals are valid user stories. This report uses the PLAN story for User Flow Coverage and does not refuse verification.
**User story (from plans):** As an 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.
Code review `19-REVIEW.md` (2026-10-17T20:12:19Z) and `19-REVIEW-DISPOSITION.md` list CR-01 and CR-02 as **open criticals**. This pass re-read the cited files; both defects are still in the tree.
## User Flow Coverage
User story: «As an 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.»
| Step | Expected | Evidence | Status |
|------|----------|----------|--------|
| Depend on shared plugins | `golem15.chat` and `golem15.chatchannels` compile as sibling modules with Winter layout | `sm-chat-plugin` module `git.golem15.com/golem15/sm-chat-plugin`, `PluginID` `golem15.chat`, `Requires` `golem15.user`; `sm-chatchannels-plugin` `golem15.chatchannels`, `Requires` `golem15.chat` + `golem15.user` | ✓ |
| Mount generic APIs | `/_chat/api/v1` and `/_chatchannels/api/v1` behind `jwt.auth` + named buckets | `sm-chat-plugin/routes.go` `APIPrefix`, `chat-api`/`chat-dm`; `sm-chatchannels-plugin/routes.go` `chatchannels-api` | ✓ |
| Realtime through lighthouse | `created.chat.message` Bind + `chat` / `user` authorizers; no WebSockets plugin | `boot.go` `lighthouse.Bind` + `Register("chat")`; user plugin `Register("user")`; no WebSockets package | ✓ |
| Ready for the Wavepath port | PHP send and GDPR delete behave the same so Wavepath can thin-alias | CR-01: context member can POST into another pair's DM. CR-02: deleting the conversation creator CASCADE-wipes the channel | ✗ |
Outcome clause is **not** observably true until CR-01 and CR-02 close.
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | SC-19-1: chat schema, models, adapters, jobs, commands and routes port 1:1; ChatChannels plugs in through the same adapter | ✗ FAILED | Schema/models/jobs/commands/routes exist and ChatChannels uses `chat.Context`. Send authorization is not 1:1: PHP `sendMessage` requires `hasParticipant`; Go `canSend` accepts `HasMember` (CR-01) |
| 2 | SC-19-2: realtime broadcasts and channel authorization go through lighthouse; no WebSockets plugin | ✓ VERIFIED | `bootRealtime` binds `chat.message` and `Registry.Register("chat", AuthorizeChatLookup)`; `AuthorizeChat` splits `chat:conversation:{id}` locally, participants only, capabilities `["prs"]`; does not call `lighthouse.ChannelID`. No WebSockets plugin in either repo |
| 3 | SC-19-3: golden tests match PHP serializers and every realtime event; `/_wave` deferred to the Wavepath port | ✓ VERIFIED | Chat goldens: `message_to_api_array`, `created_chat_message` (ids-only, no `content` key), typing/read/unread/deleted. Channel goldens: channel/category/pref/pin/mention/`mentions.changed`. `TestToAPIArray`, `TestGoldenEvents`, `TestChannelGoldenEvents` are required names in `scripts/check-phase19.sh`. Orchestrator: `--all` green |
| 4 | SC-19-4: unit tests delivered in the last plan | ✓ VERIFIED | Plan 19-05: goldens, `PHP_METHOD_MAP.md` (59+106), `TestPHPMethodMap`, `TestNoQuestStreamTrace`, `scripts/check-phase19.sh` fail-closed |
| 5 | D-01 chat: module `git.golem15.com/golem15/sm-chat-plugin`, package `chat`, plugin ID `golem15.chat`, Requires `golem15.user`; tables `golem15_chat_conversations`, `_participants`, `_messages`, `_thread_summaries` | ✓ VERIFIED | `go.mod`, `plugin.go` `PluginID`, baseline CREATE TABLE names |
| 6 | D-04 / D-05: one gormigrate ID `202610160001_create_golem15_chat_baseline`, `IF NOT EXISTS`, TIMESTAMPTZ/JSONB, never adds or drops the legacy owner column | ✓ VERIFIED | Baseline ID and SQL; `family_id` absent; `updates/baseline_test.go` asserts no `family_id` |
| 7 | D-11: `Registry.Add` stores the alias; `Resolver` returns `ErrUnknownContextType` and is never skipped; chat registers no host aliases | ✓ VERIFIED | `contracts/registry.go`; `bootRegistry` publishes an empty registry; only the test fixture calls `Add` |
| 8 | D-03: `Context.CanActFor`; default adapter true only when `actorID == memberID` | ✓ VERIFIED | `contracts/context.go` interface + `DefaultCanActFor` |
| 9 | D-13 / D-16 chat writes: `SendMessage` / `EditMessage` / `DeleteMessage` persist in `lagoon.Transaction` and Fire `MessageCreated` / `Updated` / `Deleted` inside that tx; Bind alias `chat.message`; ids-only payload | ✓ VERIFIED | `classes/service.go` and `writes.go` Fire inside the tx; `boot.go` Bind `Alias: "chat.message"`; golden `created_chat_message.json` payload has id/conversation_id/sender_id/root_id/type/action/created_at — no `content` |
| 10 | D-06 / D-08: `/_chat/api/v1`; contexts as `/contexts/{type}/{id}/…`; participation checked on every call; unknown type 404 | ✗ FAILED | Prefix and addressing hold (`routes.go`, `api/errors.go` maps `ErrUnknownContextType` to 404). **Read** path uses `CanView` (participant or overseer). **Write** path uses `canSend` HasMember fallback — CONTEXT D-08 says participation on every call. Plan 02's "participation or HasMember" wording reduces D-08 and is rejected |
| 11 | D-09: groups use `jwt.auth` and named buckets; DM start `chat-dm` Max 10 / minute keyed by frontend user id else ClientIP | ✓ VERIFIED | `routes.go` `surf.Use("jwt.auth", "throttle:chat-api")` and `throttle:chat-dm`; `Buckets()` Max 120 / 10, Decay `time.Minute`, key `u:{id}` else `ClientIP` |
| 12 | D-07: operations match Wavepath community chat including `PUT /threads/{rootId}/mute` with body `muted`; persistence is `golem15_chat_thread_mutes` | ✓ VERIFIED | Route mounted; mute table in chat baseline |
| 13 | D-10: `/api/chat/dm` and `/api/chat/attachments/{messageId}/{fileId}` are not registered | ✓ VERIFIED | Only `/_chat/api/v1` groups in `routes.go`; golden test rewrites old prefixes |
| 14 | D-18: uploads through `attach.Store` with ChatAttachmentPolicy (MaxBytes `102400*1024`, max 5, KNOWN_EXTS, SVG/HTML/XML banned); downloads stream from `/_chat/api/v1/attachments/{messageId}/{fileId}` after participation check; missing and unauthorized 404 | ✓ VERIFIED | `attachment_policy.go`, `api/download.go` `NewReader` after `AttachmentForDownload`. WR-04 (filename header) does not undo the participation gate |
| 15 | D-01 / D-04 chatchannels: module `git.golem15.com/golem15/sm-chatchannels-plugin`, package `chatchannels`, `golem15.chatchannels`, Requires chat+user; baseline eight PHP v1.6.0 tables, never adds `users.handle` | ✓ VERIFIED | `go.mod`, `plugin.go`, `202610160001_create_golem15_chatchannels_baseline.go`; handle comments only, no ALTER |
| 16 | D-14: `AccessResolver` CanAccess/CanManage default false; `SetAccessResolver` installs the host; Boot `slog.Warn` while DenyAll remains | ✓ VERIFIED | `classes/deny_all.go`; `boot.go` Warn + Publish |
| 17 | D-13 chatchannels message listeners: Boot listens `MessageCreated` / `Updated` / `Deleted`; accrue/reconcile/remove mentions and indexer; does not import chat models | ✓ VERIFIED | `boot.go` `Listen[*chatevents.MessageCreated]` etc.; `models/message_index.go` duplicates `messageTypeSystem` so models stay a leaf |
| 18 | D-20: `MessageIndex.SearchableAs` returns `messages`; document fields id/content/conversation_id/sender_id/type/created_at Unix; `ShouldBeSearchable` excludes TYPE_SYSTEM and empty content; indexer fail-open | ✓ VERIFIED | `models/message_index.go`; `search_indexer.go` Warn on Sync/Remove error. WR-01 (HTTP search not context-filtered) is a warning on the handler, not this contract |
| 19 | D-12: rewrite of `golem15_chatchannels_channels.context_type` uses the same `LegacyPHPClass` map as chat; command is idempotent | ✓ VERIFIED | `chat:rewrite-context-types` and `chatchannels:rewrite-context-types` both walk `LegacyPHPClass` |
| 20 | D-15: `users.handle VARCHAR(255) NULL` + unique index `users_handle_unique` IF NOT EXISTS; HandleGenerator charset subset of `[A-Za-z0-9._-]`; MentionParser `@([\w.-]{2,255})` resolves among that conversation's participants | ✓ VERIFIED | `202610160002_add_handle_to_users.go`; `handle_generator.go`; `mention_parser.go`; `ResolveMentionedParticipantIDs` joins `users.handle` to `golem15_chat_participants`, excludes sender |
| 21 | D-13 GDPR: `user.Created` / `user.Deleting` fire from create paths and `FormAfterDelete` before unscoped delete; chatchannels listens and `SeverUserFromContext` preserves remaining members' channels | ✗ FAILED | Events and the listener exist (`classes/events.go`, `FormAfterDelete`, `boot.go` `onUserDeleting`). Sever does not reassign `created_by`, so CASCADE still wipes the shared channel (CR-02) |
| 22 | D-17: sm-user-plugin Boot registers lighthouse namespace `user` if free; `user:{id}` allowed only to that user | ✓ VERIFIED | `classes/authorizer.go` `RegisterUserAuthorizer` / `AuthorizeUser` |
| 23 | D-06 / D-08 / D-09 chatchannels API: `/_chatchannels/api/v1`, `jwt.auth`, bucket `chatchannels-api` 120/min, contexts at `/contexts/{type}/{id}/…` | ✓ VERIFIED | `routes.go` full ChannelService resource set. WR-01..WR-03 are handler/service bugs on search/join/pin, not missing prefix/auth |
| 24 | D-21: `testdata/PHP_METHOD_MAP.md` lists 59 chat and 106 chatchannels PHP test methods, each mapped to a Go case; `TestPHPMethodMap` fails if a PHP method is missing | ✓ VERIFIED | 59 and 106 `test_*` rows; `php_method_map_test.go` compares the map to `php_methods.txt` and optionally the frozen PHP tree |
| 25 | D-02: `TestNoQuestStreamTrace` greps production sources for the four locked tokens and fails on a hit; fixture id `acme.teams` stays out of production | ✓ VERIFIED | Both plugins' `queststream_trace_test.go`; required by `check-phase19.sh` |
| 26 | `scripts/check-phase19.sh` is fail-closed: named tests must pass; skipped/missing/red required test fails; no admin or layers stage | ✓ VERIFIED | Script lists named short + postgres suites; `--self-test` passed this pass (`phase19 self-test passed`). Orchestrator: `--all` green |
**Score:** 23/26 truths verified (0 present, behavior-unverified)
CONTEXT D-08 is the contract for item 10, not the Plan 02 restatement that added "or HasMember".
### Deferred Items
| # | Item | Addressed In | Evidence |
|---|------|-------------|----------|
| 1 | `/_wave` route-level parity | Wavepath app port | SC-19-3 last sentence |
| 2 | Vue chat UI, satellites, `chat:migrate-attachments` | Phase 19.1 / D-19 | ROADMAP 19.1; CONTEXT D-19 |
CR-01 and CR-02 are **not** deferred. Phase 19.2 replays Wavepath traffic; it does not name DM write-IDOR or GDPR CASCADE.
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `../sm-chat-plugin/plugin.go` | golem15.chat plugin | ✓ VERIFIED | `PluginID`, Requires, config/lang/migrations/models/jobs/commands/routes |
| `../sm-chat-plugin/updates/202610160001_create_golem15_chat_baseline.go` | idempotent v1.3.0 baseline minus D-05 column | ✓ VERIFIED | `IF NOT EXISTS`; CASCADE on `created_by` is the CR-02 schema (PHP-named constraint kept) |
| `../sm-chat-plugin/contracts/context.go` | Context including CanActFor and BroadcastChannel | ✓ VERIFIED | |
| `../sm-chat-plugin/classes/service.go` | ChatService successor | ✓ VERIFIED | Substantive; `canSend` is the CR-01 defect, not a stub |
| `../sm-chat-plugin/events/events.go` | MessageCreated/Updated/Deleted | ✓ VERIFIED | `golem15.chat.message.created` etc. |
| `../sm-chat-plugin/routes.go` | APIPrefix, buckets, jwt.auth | ✓ VERIFIED | `/_chat/api/v1` |
| `../sm-chat-plugin/api/download.go` | participation-gated private blob stream | ✓ VERIFIED | `Content-Disposition`; WR-04 unsanitized filename |
| `../sm-chat-plugin/classes/attachment_policy.go` | Limits and banned MIME | ✓ VERIFIED | `102400` |
| `../sm-chatchannels-plugin/plugin.go` | golem15.chatchannels | ✓ VERIFIED | |
| `../sm-chatchannels-plugin/updates/202610160001_create_golem15_chatchannels_baseline.go` | eight v1.6.0 tables | ✓ VERIFIED | `golem15_chatchannels_channels` |
| `../sm-chatchannels-plugin/classes/channel_service.go` | ChannelService | ✓ VERIFIED | `CreateChannel`; WR-02/WR-03 in Join/Pin |
| `../sm-chatchannels-plugin/classes/search_indexer.go` | fail-open beachcomber | ✓ VERIFIED | `SearchableAs` |
| `../fonoteka.go/plugins/golem15/user/updates/202610160002_add_handle_to_users.go` | handle column | ✓ VERIFIED | `ADD COLUMN IF NOT EXISTS handle` |
| `../fonoteka.go/plugins/golem15/user/classes/handle_generator.go` | unique handle generation | ✓ VERIFIED | |
| `../sm-chatchannels-plugin/routes.go` | `/_chatchannels/api/v1` | ✓ VERIFIED | |
| `../sm-chat-plugin/testdata/PHP_METHOD_MAP.md` | 59-row map | ✓ VERIFIED | 59 `test_*` rows |
| `../sm-chatchannels-plugin/testdata/PHP_METHOD_MAP.md` | 106-row map | ✓ VERIFIED | 106 `test_*` rows |
| `scripts/check-phase19.sh` | phase gate | ✓ VERIFIED | Contains `TestGoldenEvents`; `--self-test` green |
**Artifacts:** 18/18 exist and are substantive. None are stubs. Goal failure is behavioral (authorization / CASCADE), not missing files.
### Key Link Verification
| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `../sm-chat-plugin/boot.go` | `../sm-chat-plugin/classes/authorizer.go` | `lighthouse.From` then `Registry.Register("chat", …)` | ✓ WIRED | `boot.go` `Register("chat", classes.AuthorizeChatLookup(…))` |
| `../sm-chat-plugin/classes/service.go` | `../sm-chat-plugin/events/events.go` | Fire MessageCreated/Updated/Deleted inside the write tx | ✓ WIRED | `MessageUpdated` in `writes.go` |
| `../sm-chat-plugin/console/rewrite_context_types.go` | `../sm-chat-plugin/contracts/registry.go` | D-12 rewrite uses `LegacyPHPClass` | ✓ WIRED | |
| `../sm-chat-plugin/api/messages.go` | `../sm-chat-plugin/classes/service.go` | handlers call `classes.Service`; they do not write rows | ✓ WIRED | `api/auth.go` `Lookup[*classes.Service]`; `PostMessage` → `SendMessage` |
| `../sm-chat-plugin/api/download.go` | `modules/lagoon/attach` | `bucket.NewReader` after participant check | ✓ WIRED | `download.go` and `classes/attachments.go` |
| `../sm-chatchannels-plugin/boot.go` | `../sm-chat-plugin/events/events.go` | Listen MessageCreated/Updated/Deleted | ✓ WIRED | |
| `../sm-chatchannels-plugin/classes/channel_service.go` | `../sm-chatchannels-plugin/contracts/access_resolver.go` | every access check through resolver; default deny-all | ✓ WIRED | `CanAccessContext` |
| `../sm-chatchannels-plugin/classes/search_indexer.go` | `modules/beachcomber` | `SearchableAs` `"messages"`; fail-open Sync | ✓ WIRED | |
| `../fonoteka.go/plugins/golem15/user/plugin.go` | `modules/lighthouse` | Boot `Register("user")` if free | ✓ WIRED | `classes/authorizer.go` `Register("user", AuthorizeUser())` |
| `../sm-chatchannels-plugin/boot.go` | `../fonoteka.go/plugins/golem15/user/classes/events.go` | Listen `user.Deleting` then `SeverUserFromContext` | ✓ WIRED | Listener is wired; GDPR outcome still fails (CR-02) |
| `../sm-chatchannels-plugin/api/channels.go` | `../sm-chatchannels-plugin/classes/channel_service.go` | handlers call ChannelService only | ✓ WIRED | `api/auth.go` `Lookup[*classes.ChannelService]` |
| `../sm-chat-plugin/golden_test.go` | `../sm-chat-plugin/testdata/goldens` | byte-compare event name, channels, payload | ✓ WIRED | `TestGoldenEvents` |
| `../sm-chatchannels-plugin/golden_test.go` | `../sm-chatchannels-plugin/testdata/goldens` | byte-compare arrays and `mentions.changed` | ✓ WIRED | `TestChannelGoldenEvents` |
| `scripts/check-phase19.sh` | `../sm-chat-plugin` | `go -C` named tests; fail-closed detectors | ✓ WIRED | `TestNoQuestStreamTrace` in required lists |
**Wiring:** 14/14 connections present. CR-01/CR-02 are wrong behavior on wired paths, not missing links.
### Data-Flow Trace (Level 4)
| Artifact | Data Variable | Source | Produces Real Data | Status |
|----------|---------------|--------|--------------------|--------|
| `api/messages.go` ListMessages | `out []MessageAPI` | `svc.GetMessages` → GORM `golem15_chat_messages` then `MessageAPI` | Yes | ✓ FLOWING |
| `api/messages.go` PostMessage | created message | `svc.SendMessage` → `tx.Create(msg)` | Yes (also the CR-01 write) | ✓ FLOWING |
| `api/download.go` | blob bytes | `bucket.NewReader` after participant check | Yes | ✓ FLOWING |
| `api/search.go` | `ids` | `beachcomber.Engine().SearchIDs` on collection `messages` | Yes, but unscoped (WR-01) | ✓ FLOWING |
| lighthouse Bind payload | created.chat.message | `broadcastPayload` ids-only | Yes; golden has no `content` | ✓ FLOWING |
No hollow/static serializer path. Search is real engine data without a context filter.
### Behavioral Spot-Checks
| Behavior | Command | Result | Status |
|----------|---------|--------|--------|
| Fail-closed detectors | `bash scripts/check-phase19.sh --self-test` | `phase19 self-test passed` | ✓ PASS |
| Phase gate (orchestrator evidence) | `bash scripts/check-phase19.sh --all` | recorded green (self-test, plugins, docs, postgres); not re-run here | ✓ PASS (cited) |
| Framework short tests (orchestrator) | `go test -short ./...` in summercms.go | recorded green; not re-run here | ✓ PASS (cited) |
| PHP method map counts | grep `^\| test_` on both `PHP_METHOD_MAP.md` | 59 chat, 106 chatchannels | ✓ PASS |
| CR-01 canSend HasMember fallback | read `classes/service.go:255-273` | participant count then `host.HasMember` | ✗ FAIL (code) |
| CR-02 created_by CASCADE + no reassignment | read chat baseline + `SeverUserFromContext` | CASCADE present; no `created_by` update | ✗ FAIL (code) |
Postgres suite was not re-run (orchestrator evidence + CR-01/CR-02 are proven by source).
### Probe Execution
| Probe | Command | Result | Status |
|-------|---------|--------|--------|
| — | — | No `scripts/*/tests/probe-*.sh`; plans record spec-less probe fallback skipped | SKIP |
### Decision Coverage
All 21 trackable CONTEXT.md decisions (D-01..D-21) are honored by shipped artifacts (`check decision-coverage-verify`: honored 21 / total 21, `not_honored: []`). Honoring a decision in artifacts is not the same as the decision holding at runtime: D-08 and D-13 are present in code and still fail the observable truths above.
### Requirements Coverage
REQUIREMENTS.md has **no** `SC-19-*` rows (ROADMAP Phase 19 Requirements line is TBD). Plans declare SC-19-1..SC-19-4 and D-01..D-21. No REQUIREMENTS.md IDs are orphaned onto this phase.
| Requirement | Source Plan | Description | Status | Evidence |
|-------------|-------------|-------------|--------|----------|
| SC-19-1 | ROADMAP + 19-01..04 | 1:1 schema/models/adapters/jobs/commands/routes | ✗ BLOCKED | CR-01 sendMessage authorization |
| SC-19-2 | ROADMAP + 19-01 | lighthouse realtime, no WebSockets | ✓ SATISFIED | Bind + authorizers |
| SC-19-3 | ROADMAP + 19-02, 19-05 | goldens; `/_wave` deferred | ✓ SATISFIED | goldens + named tests; `/_wave` deferred |
| SC-19-4 | ROADMAP + 19-05 | unit tests last plan | ✓ SATISFIED | 19-05 + check-phase19.sh |
| D-01 | 19-01, 19-03 | module/package/plugin IDs and table names | ✓ SATISFIED | |
| D-02 | 19-05 | no QuestStream tokens | ✓ SATISFIED | TestNoQuestStreamTrace |
| D-03 | 19-01 | CanActFor | ✓ SATISFIED | |
| D-04 / D-05 | 19-01, 19-03 | squashed IF NOT EXISTS baselines; no family_id | ✓ SATISFIED | |
| D-06 / D-07 / D-09 / D-10 | 19-02, 19-04 | generic APIs, throttle, no legacy /api/chat | ✓ SATISFIED | |
| D-08 | 19-02 | participation on every call | ✗ BLOCKED | CR-01 |
| D-11 / D-12 | 19-01, 19-03 | registry + rewrite | ✓ SATISFIED | |
| D-13 | 19-01, 19-03, 19-04 | festival hooks + GDPR sever | ✗ BLOCKED | events/listeners yes; GDPR wipe CR-02 |
| D-14 | 19-03 | deny-all AccessResolver | ✓ SATISFIED | |
| D-15 | 19-04 | users.handle | ✓ SATISFIED | |
| D-16 / D-17 | 19-01, 19-04 | lighthouse chat + user namespaces | ✓ SATISFIED | |
| D-18 | 19-02 | private attachments | ✓ SATISFIED | |
| D-19 | 19-01 prohibition | migrate-attachments not ported | ✓ SATISFIED | `TestMigrateAttachmentsAbsent`; command not in `Commands()` |
| D-20 | 19-03 | beachcomber MessageIndex | ✓ SATISFIED | |
| D-21 | 19-05 | PHP method map 59+106 | ✓ SATISFIED | |
### Anti-Patterns Found
No `TBD` / `FIXME` / `XXX` in plugin production sources or `scripts/check-phase19.sh`.
| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| `../sm-chat-plugin/classes/service.go` | 255-273 | `canSend` HasMember fallback (CR-01) | 🛑 Blocker | Write IDOR into another pair's DM; fails D-08 / SC-19-1 |
| `../sm-chat-plugin/updates/202610160001_create_golem15_chat_baseline.go` | 26-27 | `created_by` ON DELETE CASCADE without reassignment (CR-02) | 🛑 Blocker | Deleting the creator wipes the channel; fails D-13 GDPR |
| `../sm-chatchannels-plugin/api/search.go` | 25-38 | `SearchIDs` with only `q` after context access check (WR-01) | ⚠️ Warning | Cross-context existence oracle; not a locked must-have |
| `../sm-chatchannels-plugin/classes/channel_service.go` | 958-968 | `JoinChannel` only clears `has_left` (WR-02) | ⚠️ Warning | Public join does not grant send rights |
| `../sm-chatchannels-plugin/classes/channel_service.go` | 1332-1344 | `PinMessage` does not load the message (WR-03) | ⚠️ Warning | Pin IDOR across conversations |
| `../sm-chat-plugin/api/download.go` | 63-68 | `Content-Disposition` interpolates unsanitized filename (WR-04) | ⚠️ Warning | Header injection via upload name |
| `../sm-chat-plugin/classes/writes.go` | 38-40 | `GetGroupChat` ignores `addParticipant` error (IN-01) | ℹ️ Info | After CR-01, a failed join 200s then 404s on send |
| `../fonoteka.go/plugins/golem15/user/controllers/users_admin_controller.go` | 516-532 | `user.Deleting` only from admin `FormAfterDelete` (IN-02) | ℹ️ Info | A later API/console hard-delete would skip Sever (and CR-02 reassignment) |
Open review findings WR-01..WR-04 and IN-01..IN-02 are recorded here and in `19-REVIEW-DISPOSITION.md`. They do not independently flip a must-have; they remain open.
### Test Quality Audit
| Test File | Linked Req | Active | Skipped | Circular | Assertion Level | Verdict |
|-----------|-----------|--------|---------|----------|-----------------|---------|
| `sm-chat-plugin/golden_test.go` | SC-19-3 | yes | no | no — goldens `_source` names frozen PHP @943b712 | Value (byte-compare) | OK |
| `sm-chatchannels-plugin/golden_test.go` | SC-19-3 | yes | no | no — frozen PHP @b92fb42 | Value | OK |
| `sm-chat-plugin/php_method_map_test.go` | D-21 | yes | no | no | Value (map completeness) | OK; many PHP methods share one Go case (coarse, still mapped) |
| `sm-chat-plugin/queststream_trace_test.go` | D-02 | yes | no | no | Behavioral (walk sources) | OK |
| `sm-chatchannels-plugin` TestDeletingCascade | D-13 | yes (postgres) | skips under `-short` via TestMain | no | Behavioral | INSUFFICIENT for CR-02 — deletes a member, not the creator |
| HTTP tracers | D-08 | yes (postgres) | `-short` skip | no | Behavioral | INSUFFICIENT for CR-01 — stranger-of-context 404, not teammate-of-DM |
**Disabled tests on requirements:** 0 requirement-linked skips (Docker skip in TestMain is the documented `-short` gate).
**Circular patterns detected:** 0.
**Insufficient assertions:** 2 (the two critical gaps: no test for DM write-IDOR; no test for creator-delete CASCADE).
### Prohibitions
| Statement | Tier | Status | Evidence |
|-----------|------|--------|----------|
| Production sources never contain the four D-02 tokens; plugin.name is Chat / Czat | grep | held | `TestNoQuestStreamTrace`; `lang/en/lang.yaml` `plugin.name: "Chat"` / `"ChatChannels"` |
| summercms.go modules are not edited; no new third-party require beyond the analog pin set | grep | held | Framework artifact is `scripts/check-phase19.sh` only |
| `chat:migrate-attachments` is not ported; `/_wave` and friendship DMs stay out | grep / judgment | held | No command; no `/_wave` routes; `GetOrCreateDm` is context-scoped |
| Chat blobs never on `StaticHandler` / `StaticHandlerPublic` | grep | held | zero matches in sm-chat-plugin |
| `users.handle` is not a chatchannels migration; Searchable not on `chat.models.Message` | grep | held | |
| `User.MorphName` stays `Golem15\User\Models\User` | judgment | held | `models/user.go` |
| Plugins never patch each other's models | grep | held | festival listeners only |
### Human Verification Required
N/A — status is `gaps_found`. This is an infrastructure/plugin phase with no user-facing SPA. Realtime payloads are proven by goldens. The two blockers are source-visible; they do not need a human to confirm they exist.
### Gaps Summary
The plugins, generic APIs, lighthouse authorizers, goldens, PHP method maps, handle column, and fail-closed gate are in place. The phase goal's outcome — **ready for the Wavepath port** — does not hold:
1. **CR-01 / D-08 / SC-19-1** — `canSend` treats host-context membership as send rights. A DM stores that host alias, so any context member can POST into another pair's thread. PHP refuses that. Wavepath clients that assume participant-only send will not match.
2. **CR-02 / D-13 GDPR** — deleting the user who created a conversation CASCADE-deletes the conversation and the channel. `SeverUserFromContext` never reassigns `created_by`. Remaining members lose the channel. `TestDeletingCascade` does not cover this.
Close those two with the review's fixes and named postgres cases, then re-verify. WR-01..WR-04 should be fixed in the same round or explicitly accepted; they are not must-have blockers on this pass.
---
_Verified: 2026-10-17T20:17:10Z_
_Verifier: Claude (msd-verifier)_