Files
mantra-kmp/composeApp
Kgothatso Ngako 2d0a5ce50f feat(projects): three kinds, a creator tag, and the owner role the parser needs
Phase 1 of docs/project-feature.md. Nothing emits these yet -- no screen, no
repository, no apply arm -- so this is three kinds the app can build and parse
and nothing else.

The kinds are 30330-30332, not the next free number in the nip30303 band. That
band is not merely full: 30310, 30311 and 30312 are double-booked with the
ceremony family, survivable only because none of the three nip30303 kinds has a
producer. 30317-30319 is the ceremony family's growth room. So projects start
after the highest kind in use.

A project names people two ways because they are two shapes. One creator, so
`CreatorTag` -- a single-value tag like every other field here, and
multi-character on purpose: NIP-01 indexes single-letter names, so a `p` tag is
an address, and a broadcast project should carry its creator as data rather than
as a recipient. Several owners, so `ContributorTag` with `ROLE_OWNER`, which is
what that tag is for.

`ROLE_OWNER` goes into `KNOWN_ROLES` in this same commit, which is the whole
reason that line is first. `ContributorTag.parse` drops a role it does not know
-- rightly, since a stale relay hint would otherwise render as a role -- and the
consequence for an unregistered one is that the tag parses, the role comes back
null, the string stays inside the signed event id, the event still verifies, and
the project lands owned by nobody. Nothing is logged. `ProjectEventTest` has the
three-line test that would say so.

Two other things that fail quietly, both in `ProjectEvent.build`.
`addUniqueValueIfNew` rather than `addUnique`, because `addUnique` keeps one tag
per *name* and three owners would come out as one. And sorted, because the id is
a hash over the tags as emitted and a multi-select hands its selection over in no
particular order -- the same reason `DkgSession.participantPublicKeys` sorts.

A membership is its own event rather than a line in a list the project carries.
The only list-shaped kinds in this family are the three contributor lists, which
are precisely the ones nothing applies; and one quorum per decision is the grain
this app signs at, which a replaceable list would lose. `membershipsOf` mirrors
`ArtifactVersionEvent.initialVersionOf`: built from the project *after* the
session has authored it, since the id it carries is a hash over the group's key
at the room's path and no screen can know it beforehand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 19:00:51 +02:00
..