Files
amethyst/quartz/plans/2026-07-11-concord-event-classes.md
T
Claude bc365c8ca2 refactor(concord): invite bundle (33301) gets its own addressable event
Third refactor slice (plan: quartz/plans/2026-07-11-concord-event-classes.md).
The kind-33301 public invite bundle is now a proper addressable Event class:

- New cord05Invites/bundle/ConcordInviteBundleEvent (BaseAddressableEvent), with a
  build{} template that emits the ['d',''] + ['vsk','6'] tags via the standard DTag
  ext and the shared VskTag (ControlEntityKind.INVITE_LIVE). Registered kind 33301
  -> ConcordInviteBundleEvent in EventFactory so fetched bundles parse as the class.
- ConcordInviteBundle.build now signs ConcordInviteBundleEvent.build(...) with the
  per-link key; the raw TAG_D/TAG_VSK/VSK_LIVE constants are gone. Tag order (d, vsk)
  is preserved so the bundle event id / naddr are unchanged — the cord05Invites
  tests (bundle round-trip + join flow) pass.

Remaining invite kinds: 3313 direct-invite (rumor has empty tags; p/k index is on
the giftwrap) and 13303 invite-list (unimplemented stub) — tracked in the plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-11 12:00:42 +00:00

4.5 KiB

Concord event layer → per-kind Event classes (nip88Polls structure)

Why

The Concord quartz layer currently centralizes wire kinds in a single concord/events/ConcordKinds.kt constant object and hand-rolls rumors with raw string tags (see cord03Channels/ChannelChat.kt: RumorAssembler.assembleRumor(kind = ConcordKinds.MESSAGE, tags = arrayOf(arrayOf("q", …)))). Two problems:

  1. Duplicates standard Nostr kinds. ConcordKinds.MESSAGE=9, REACTION=7, DELETE=5, COMMENT=1111, EDIT=3302 shadow ChatEvent.KIND, ReactionEvent.KIND, DeletionEvent.KIND, CommentEvent.KIND. Concord chat rumors are those standard events (they already parse back as ChatEvent etc. on read) — the build side should reuse the classes, not re-derive by number.
  2. No per-event structure. Every other protocol in quartz gives each event kind a package: XxxEvent.kt (the Event subclass + build), plus TagArrayBuilderExt.kt / TagArrayExt.kt and a tags/ folder of typed tag classes (nip88Polls/poll/… is the reference). Concord instead has loose builders (ChannelChat, ControlEditionBuilder) and stringly-typed tags.

Target: match the nip88Polls shape. Reuse standard events where the protocol uses standard kinds; give each genuinely-Concord kind its own package.

Reuse standard events (chat plane, CORD-03)

A Concord chat rumor is a standard event + a ["channel", id] + ["epoch", n] binding. Introduce a binding-tag package and reuse the standard builders:

  • cord03Channels/tags/ChannelTag.kt, tags/EpochTag.kt — typed tags.
  • cord03Channels/TagArrayBuilderExt.kt — channel(id), epoch(n) on the DSL.
  • cord03Channels/TagArrayExt.kt — channelId(), epoch(), isBoundTo(...).
rumor reuse binding
message (9) ChatEvent.build channel + epoch
reply (9 + q) ChatEvent.build + q/p channel + epoch
reaction (7) ReactionEvent.build channel + epoch
delete (5) DeletionEvent.build channel + epoch

ChannelChat keeps its public API (returns unsigned rumors via RumorAssembler) but builds tags from the standard event's DSL + the binding ext. Delete ConcordKinds.MESSAGE/REACTION/DELETE/COMMENT/EDIT.

Interop guard: the exact on-wire tags must stay byte-identical to today's output (Armada compat). Keep ["q", parentId] + ["p", parentAuthor] for replies and ["e"/"p"/"k"] for reactions — do not switch to QEventTag's 3-element form. Verify with ConcordPlaneRegistryTest + an amy↔Armada round-trip.

New per-kind Event classes (genuinely Concord)

Each gets concord/<cordXX>/<name>/XxxEvent.kt + TagArrayBuilderExt + TagArrayExt + tags/:

kind event CORD
3308 ControlEditionEvent (from ControlEdition/ControlEditionBuilder) 02/04/06
3303 RekeyEvent 06
3306 / 3309 / 3312 GuestbookJoinLeaveEvent / KickEvent / SnapshotEvent 02
3313 DirectInviteEvent 05
23311 / 23313 TypingEvent / VoicePresenceEvent 03/07
3310 WebxdcEvent 03
33301 InviteBundleEvent (from ConcordInviteBundle) 05
13303 InviteListEvent 05
20013 / 20014 / 1059 / 21059 envelope seals + inverted wrap 01

ConcordCommunityListEvent (13302) is already an Event class (now a BaseReplaceableEvent) — keep, just move under a per-kind package if desired.

After the move, ConcordKinds retains only the truly-Concord kinds (envelope, control, rekey, guestbook, invites, voice), not the standard-Nostr aliases.

Sequencing (incremental, compile + interop-test each)

  1. ✅ Chat plane reuse — ChatEvent/ReactionEvent + cord03Channels/tags + ext; dropped MESSAGE/REACTION/DELETE/COMMENT aliases. (commit c2cbd602)
  2. ✅ Control plane 3308 → cord04Roles/control/ControlEditionEvent package + tags/ (vsk/eid/ev/ep/vac) + ext; EventFactory registers 3308. (commit dc2abc0e)
  3. Invites: ✅ 33301 bundle → cord05Invites/bundle/ConcordInviteBundleEvent (addressable, reuses DTag + VskTag; registered in EventFactory). Remaining: 3313 direct-invite → ConcordDirectInviteEvent (the rumor has empty tags; the p/k index lives on the giftwrap). 13303 invite-list has no implementation yet (a bare ConcordKinds constant) — build it when the private-invite feature lands.
  4. Guestbook (3306/3309/3312) + voice (23313/23311) + rekey (3303). ← next
  5. Envelope seals (20013/20014) / wrap (1059/21059).
  6. Shrink ConcordKinds to only kinds without their own event class; audit EventFactory coverage.