| M1-58/0 |
2026-10-15 |
Base int/m1-wave19 at b380a4e, branch feat/M1-58-messaging-edit-delete in its own worktree; the branch is not merged locally, the integration lane ports it. The brief's contract (the operations, schemas and parameters of openapi.yaml, the actions of actions.yaml, 02 4.3 and 4.5 naming eventlog.Redactor) is the same on the base and on main, so no contract commit is brought in. The port reuses messaging.Message as the MessageView of the brief. Update, Delete and Revisions take the caller from the context (kernel actor), as the brief's signatures state; Create and CreateThread keep their explicit actor. |
Instructions of the launcher, which override the brief's merge step; one message DTO per module (M1-57 reuses it too). |
| M1-58/1 |
2026-10-15 |
No-op rule: an edit whose resulting content equals the stored content byte for byte and whose resolved mention set equals the stored one (order ignored), an empty body included, answers 200 with the current message and its ETag and writes nothing: no revision, no event, no new version. Mentions that resolve differently than before (a person left or joined) make an edit with the same content a change, which stores the unchanged content as a revision. |
Brief design note 2 point 5; the resulting state is what the edit compares. |
| M1-58/2 |
2026-10-15 |
Error precedence of updateMessage: 428 (the precondition stage of the chain, before any lookup) > 404 (missing and invisible share the path: the message is read in the visible channels of Scope(message.read), and authorize gets an unknown thread hint when none is found) > 403 (author, write, channel_read_only) > 410 > 412 > 413 > 400. An If-Match that is not a version ("x", a weak tag) is 412 like a stale one, because version 0 never exists. A system line names a person in its author column but is the server's: editing it is 403 forbidden (the contract lists no 409 for updateMessage, and the content CHECK of a system line would otherwise fail with 500). |
Brief design note 2; 06 "Concurrency" item 2; the response list of updateMessage in openapi.yaml. |
| M1-58/3 |
2026-10-15 |
The visible channels of an edit, a deletion and a revision list come from Authorizer.Scope for message.read, the list action of reading messages; message.update and message.delete are not read-level actions, which Scope refuses. An app needs messages:read for them to find its messages, so an app with only messages:write gets 404 on its own message. |
07 section 7.4 rule 1; the predicate is the read scope, the decision is authorize with the action itself. |
| M1-58/4 |
2026-10-15 |
Lock order (design note 4): the thread row through chat.Threads.LockForMessageChange, then the message row (LockVisibleMessage ... FOR UPDATE), several message rows in ascending id, the counters event_seq and audit_seq last through the phase hooks. An edit locks only the message row; a deletion re-reads the row under its lock and ends without a write when it was deleted meanwhile, so parallel deletions count the message down once and an edit that waited for a deletion gets 410. |
03 section 6.3; M1-62 locks the thread first too, so a deletion and a move cannot deadlock. |
| M1-58/5 |
2026-10-15 |
Redaction (design note 6): domain.RedactPayload is a pure function that returns the eventlog.RedactFunc for the event form of the tombstone; message.created and message.updated become {"message": T}, reaction.added and reaction.removed keep message_id, thread_id and principal_id with emoji: "" and count: 0 and drop every other member, pin.added and pin.removed stay, any other type or a schema version other than 1 is an internal error that fails the deletion. sync writes the payloads in one statement and sets redacted_at only when unset; bothy_app gets the column right UPDATE (payload, redacted_at) on event_log in its own migration 00041 (module sync), message_revisions is 00040 (module messaging); numbered 00040 and 00041 on the ticket branch, INT-W22/1. |
04 section 2.2 rule 5, R-DM-18; a column grant makes a change of the envelope impossible for the server role. The migration numbers are the next free ones of the base; a parallel ticket that takes them makes the later one renumber at integration. |
| M1-58/6 |
2026-10-15 |
Reactions and pins (design note 8): their tables come with M1-59, so the deletion has a marked step where M1-59 adds the two DELETE statements and TestDeleteMessage_RemovesReactionsAndPin; the redaction of their events is already defined and tested. messages.search_vector does not exist on the base (M3-13), so DP-T01 cannot check it yet; the tombstone's empty content makes it empty by itself when the column comes. |
Out of scope of the brief; the tests hold what exists. |
| M1-58/7 |
2026-10-15 |
The deletion of another principal's message writes one allow entry through auditing.Recorder in the unit of work, with resource kind message, the message id and only thread_id, channel_id and author_principal_id; an own deletion and an already deleted message write none. A refusal is recorded once by the deny path of authz (R-AZ-04). wire.NewMessaging takes the write ports as MessagingWrites (appender, redactor, recorder). |
05 message.delete, column Audit "yes for other people's"; SEC-LOG-4 and 20 section 4.5: no content in audit details. |
| M1-58/8 |
2026-10-15 |
Test first: the tests of the redactor, the thread count, the migration, the domain rules and the HTTP use cases ran red before their code (compile failures on the missing symbols, and the migration schema test on the missing table); TestEventLogRedactionGrant and TestEnvelope_RedactedFlag were written after the grant and with an envelope mapping that already existed, so they passed on their first run. |
Honest record of the order of work. |
| M1-58/9 |
2026-10-16 |
Port onto main: code and tests as in the integration squash except: the migrations are 00039 and 00040 (after 00034); M3-13 is not on main, so LockVisibleMessage, UpdateMessageContent and TombstoneMessage keep * (INT-W22/3 is not ported), wire.NewMessaging takes no directory and the M3-13 world is not touched; M2-24 and M2-39 parts stay out. From wave 22: the unions of INT-W22/1, INT-W22/4 and the Redactor and Audit of INT-W22/5 in filter_test.go and mentions_test.go. |
main holds M1-56, M1-57 and M1-60 only. |