Skip to content

feat(buzz-cli, buzz-sdk): add buzz projects command group (NIP-MP kind:30621 write path) - #4020

Open
wpfleger96 wants to merge 3 commits into
mainfrom
duncan/projects-cli
Open

feat(buzz-cli, buzz-sdk): add buzz projects command group (NIP-MP kind:30621 write path)#4020
wpfleger96 wants to merge 3 commits into
mainfrom
duncan/projects-cli

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Jul 31, 2026

Copy link
Copy Markdown
Member

Implements the buzz projects command group — the NIP-MP Phase 2 write path. The relay accepted kind:30621 in #3171; this delivers the Rust builder (required by the NIP-MP conformance obligation) and the seven CLI commands that make projects writable from the CLI.

What this adds

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

Layer A (protocol):

  • validate_project_envelope(tags, content) — enforces the 8 NIP-MP ingest rules: d-cardinality, d-empty/length, member-tag-arity (2–3 elements), member-cap (64 raw a tags before dedup), member-coordinate grammar (first-two-colons split, literal 30617, lowercase 64-hex owner, non-empty remainder), member-duplicate (by coordinate only), singleton metadata cardinality, and byte bounds (name 256 / description 2048 / buzz-channel 256 / buzz-visibility 256).
  • build_project_with_tags(content, tags) — raw Layer A builder; no canonicalization; accepts every valid fixture envelope including opaque metadata, non-empty content, relay hints, and unknown tags.
  • ProjectMemberCoord30617:<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 the single d tag, enforces UUID channel and listed|unlisted visibility, emits empty content; composes onto Layer A.

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 directly through build_project_with_tags; accepts build, rejects fail tied to the named rule; 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: collision-guarded preflight — returns Conflict if the slug already exists; requires ≥1 --repo.
  • update: at least one setter/clearer required (runtime guard before any network I/O); omission preserves; --clear-* drops the tag; setter + clearer are mutually exclusive per clap group.
  • delete: head-based tombstone at created_at = head + 1; post-submit re-query verifies tombstone landed; returns Conflict if a concurrently newer head survived.
  • All mutations: strip auth, re-validate full envelope through Layer A before publish; created_at advances from observed head, never wall-clock.
  • --repo bare form (Buzz repo-id [a-zA-Z0-9._-]{1,64}) auto-expands with caller's pubkey; full 30617:<owner-hex>:<repo-d> for cross-owner or colon-bearing IDs.
  • Relay hints on existing member tags are preserved verbatim through RMW.
  • remove-repo of an absent coordinate returns NotFound.

Shared infrastructure:

  • commands/mod.rs: parse_write_response() extracted from repos.rs; adopted by repos.rs (thin wrapper) and projects.rs directly.

Limitations (recorded, not in scope)

  • No relay-hint authoring: --repo carries a coordinate only; existing hinted a tags survive RMW unchanged. Authoring syntax deferred until there's a demonstrated need.
  • Signer-self delete only: the relay's NIP-OA owner-delete extension is not exposed; delete targets the signer's own coordinate.
  • Deletion durability: the watermark carry-over from the relay layer applies; delete is best-effort against a later-arriving replacement.

Live round-trip

Blocked pending relay deployment of #3171 to production (kind:30621 currently rejected at wss://buzz.block.builderlab.xyz with "unknown event kind"). Local Docker relay is also not available in the current environment. The round-trip will be run and the transcript posted to the thread as soon as either path is available.

…r B)

Add two-layer builder implementation per NIP-MP Phase 2 spec (plan v3):

Layer A (protocol):
- `validate_project_envelope(tags, content)` — enforces the 8 NIP-MP
  ingest rules: d-cardinality, d-empty, member-tag-arity, member-cap
  (before dedup), member-coordinate-malformed, member-duplicate,
  metadata-cardinality, metadata-length.
- `build_project_with_tags(content, tags)` — raw Layer A builder; no
  canonicalization; accepts all valid fixture envelopes including
  opaque metadata, non-empty content, relay hints, and unknown tags.
- `ProjectMemberCoord` — parsed `30617:<owner-hex>:<repo-d>` coordinate
  with optional opaque relay hint; equality/Hash by coordinate only.

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

Shared infrastructure:
- `build_delete_addressable(kind, pubkey, d)` — generic NIP-09 kind:5
  coordinate delete; validates addressable kind range and non-empty d.
  `build_workflow_delete` now delegates to this function.

Conformance:
- All 31 NIP-MP.fixtures.json cases exercised directly through
  `build_project_with_tags`; accepts build, rejects fail tied to the
  named rule; count assertion guards against omissions.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 requested a review from a team as a code owner July 31, 2026 21:50
…write path)

Seven commands for creating and managing multi-repo projects:

  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> [...]
  buzz projects remove-repo <slug> --repo <coord> [...]
  buzz projects update <slug> [--name|--clear-name] [--description|...]
                              [--channel <uuid>|--clear-channel]
                              [--visibility listed|unlisted|--clear-visibility]
  buzz projects delete <slug>

Design:
- create: collision-guarded preflight; repeated create returns Conflict.
- update: clap argument group requires at least one setter or clearer;
  omission preserves existing metadata; --clear-* drops the tag.
- delete: head-based tombstone (created_at = head + 1); post-submit
  re-query verifies the tombstone landed; Conflict if raced.
- All mutations: strip auth, re-validate full envelope through Layer A
  before publish; created_at advances from observed head, never wall-clock.
- --repo bare form (Buzz repo-id grammar) auto-expands with caller's pubkey;
  full 30617:<owner>:<repo-d> form used for cross-owner or colon-bearing ids.
- Relay hints on existing members are preserved verbatim through RMW.

Shared infrastructure:
- commands/mod.rs: extract parse_write_response() shared helper; adopted by
  repos.rs (thin wrapper) and projects.rs directly.

Limitations recorded for the PR body:
- No relay-hint authoring (read-preserved only).
- Signer-self delete only (NIP-OA owner-delete path deferred).
- Deletion durability against later arrival (watermark carry-over).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 force-pushed the duncan/projects-cli branch from 153db41 to 4e5e4e9 Compare July 31, 2026 22:07
…d-repo no-op

IMPORTANT: fix clap mutation group — add required(true).multiple(true) to the
Update ArgGroup so multiple independent setters/clearers are accepted and an
empty update is rejected at parse time. Pairwise setter/clearer conflicts are
retained. Add four parser-level tests covering all four cases Thufir specified.

IMPORTANT: move member-cap check before arity loop in validate_project_envelope,
matching the relay's ingest rule order (NIP-MP rule 3=cap, rule 4=arity).
Correct rule-number doc comments. Add cap-wins-over-arity test with 65 members
plus a malformed tag asserting member-cap fires first.

MINOR: remove redundant double build_project_with_tags call in cmd_remove_repo —
single rebuild_project call matches add-repo pattern and centralises auth strip.

MINOR: return Conflict when add-repo adds zero new coordinates (all requested
repos were already members). Consistent with create/update race reporting; add
focused test verifying the error kind and message.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant