Files
b7bb15122e feat(projects): add buzz projects CLI commands (NIP-MP kind:30621) (#4020)
Implements the `buzz projects` command group — the NIP-MP Phase 2 write
path for kind:30621 multi-repo projects. The relay accepted kind:30621
in #3171; this adds the two-layer Rust builder in `buzz-sdk` and the
seven CLI commands.

## What this adds

### `crates/buzz-sdk/src/builders.rs` — two-layer builder

**Layer A (protocol):**
- `validate_project_envelope(tags, content)` — 8 NIP-MP rules in relay
order: `d`-cardinality, `d`-empty/length, member-cap (≤64 `a` tags,
checked before per-tag parse), member-tag-arity (2–3 elements),
member-coordinate grammar (first-two-colons split, literal `30617`,
lowercase 64-hex owner, non-empty remainder), member-duplicate
(coordinate only, hint ignored), singleton metadata cardinality, byte
bounds (`name` ≤256 / `description` ≤2048 / `buzz-channel` ≤256 /
`buzz-visibility` ≤256).
- `build_project_with_tags(content, tags)` — raw Layer A builder; RMW
mutations path.
- `ProjectMemberCoord` — `30617:<owner-hex>:<repo-d>` + optional opaque
relay hint; equality/Hash by coordinate only.

**Layer B (writer policy):**
- `build_project(slug, name, description, members, channel, visibility)`
— constructs `d` tag, enforces UUID channel and `listed|unlisted`
visibility, forces empty content; composes onto Layer A. This is the
`create` path.

**Shared:**
- `build_delete_addressable(kind, pubkey, d)` — generic NIP-09 kind:5
coordinate delete; `build_workflow_delete` now delegates to this.
- All 31 `NIP-MP.fixtures.json` cases exercised through
`build_project_with_tags`; count assertion guards against omissions.

### `crates/buzz-cli/` — seven commands

```
buzz projects create <slug> --repo <coord> [--name] [--description] [--channel <uuid>] [--visibility listed|unlisted]
buzz projects get <slug> [--owner <pubkey>]
buzz projects list [--owner <pubkey>] [--limit <n>]
buzz projects add-repo <slug> --repo <coord> [--repo <coord>]...
buzz projects remove-repo <slug> --repo <coord> [--repo <coord>]...
buzz projects update <slug> [--name|--clear-name] [--description|--clear-description] [--channel <uuid>|--clear-channel] [--visibility listed|unlisted|--clear-visibility]
buzz projects delete <slug>
```

Command semantics:
- **`create`**: all local validation (slug, repos, channel, visibility,
name length) fires before the collision preflight — invalid input
returns `Usage` without a network call. Routes through Layer B
(`build_project`).
- **`update`**: at least one setter/clearer required — enforced by a
clap `ArgGroup` with `required(true).multiple(true)`, with a runtime
backstop for programmatic callers; setter + own clearer are mutually
exclusive per clap conflicts.
- **`add-repo`/`remove-repo`**: coordinate expansion and dedup fire
before head fetch — malformed or duplicate `--repo` values return
`Usage` without touching the relay.
- **`delete`**: head-based tombstone at `created_at = head + 1`;
post-submit re-query verifies tombstone landed.
- All mutations: strip `auth`, re-validate full envelope through Layer
A; `created_at` advances from observed head, never wall-clock.
- Relay hints on existing member tags preserved verbatim through RMW.

## Limitations (recorded, not in scope)

- **No relay-hint authoring**: `--repo` carries a coordinate only;
existing hinted `a` tags survive RMW unchanged.
- **Signer-self delete only**: NIP-OA owner-delete extension not
exposed; `delete` targets the signer's own coordinate.
- **Deletion durability**: watermark carry-over applies; `delete` is
best-effort against a later-arriving replacement.

## Live round-trip

21-step transcript executed against a relay built from `origin/main`
`b1b283cd4`, covering create, get, multi-field update (name +
description + channel in one call), channel set/clear, add-repo,
remove-repo, delete (tombstone verified at `head+1`, repeated delete →
`NotFound`). Delta transcript confirmed multi-field update, channel
set/clear, no-op add-repo → `Conflict` exit 5, empty update and
setter+own-clearer both rejected at parse time. Duplicate create →
`Conflict`. Cross-owner `add-repo` with full coordinate exercised.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-02 12:48:49 -04:00
..