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
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:
- Duplicates standard Nostr kinds.
ConcordKinds.MESSAGE=9,REACTION=7,DELETE=5,COMMENT=1111,EDIT=3302shadowChatEvent.KIND,ReactionEvent.KIND,DeletionEvent.KIND,CommentEvent.KIND. Concord chat rumors are those standard events (they already parse back asChatEventetc. on read) — the build side should reuse the classes, not re-derive by number. - No per-event structure. Every other protocol in quartz gives each event
kind a package:
XxxEvent.kt(theEventsubclass +build), plusTagArrayBuilderExt.kt/TagArrayExt.ktand atags/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)
- ✅ Chat plane reuse —
ChatEvent/ReactionEvent+cord03Channels/tags+ ext; droppedMESSAGE/REACTION/DELETE/COMMENTaliases. (commitc2cbd602) - ✅ Control plane 3308 →
cord04Roles/control/ControlEditionEventpackage +tags/(vsk/eid/ev/ep/vac) + ext;EventFactoryregisters 3308. (commitdc2abc0e) - Invites: ✅ 33301 bundle →
cord05Invites/bundle/ConcordInviteBundleEvent(addressable, reusesDTag+VskTag; registered inEventFactory). Remaining: 3313 direct-invite →ConcordDirectInviteEvent(the rumor has empty tags; thep/kindex lives on the giftwrap). 13303 invite-list has no implementation yet (a bareConcordKindsconstant) — build it when the private-invite feature lands. - Guestbook (3306/3309/3312) + voice (23313/23311) + rekey (3303). ← next
- Envelope seals (20013/20014) / wrap (1059/21059).
- Shrink
ConcordKindsto only kinds without their own event class; auditEventFactorycoverage.