mirror of
https://github.com/block/buzz.git
synced 2026-10-28 13:01:10 +00:00
Define conversation subject protocols and core domain types
Signed-off-by: Bradley Axen <baxen@squareup.com>
This commit is contained in:
@@ -38,6 +38,8 @@ pub mod presence;
|
||||
pub mod private_managed_agent;
|
||||
/// Canonical relay runtime identities.
|
||||
pub mod relay;
|
||||
/// Conversation subject types and structural validation.
|
||||
pub mod subject;
|
||||
/// Tenant identity — the server-resolved community key carried on scoped paths.
|
||||
pub mod tenant;
|
||||
/// Schnorr signature and event ID verification.
|
||||
|
||||
220
crates/buzz-core/src/subject.rs
Normal file
220
crates/buzz-core/src/subject.rs
Normal file
@@ -0,0 +1,220 @@
|
||||
//! Conversation subjects: domain state independent of its conversation home.
|
||||
//!
|
||||
//! Domain values decoded from subject declarations. Transport tags,
|
||||
//! authorization, and transactional relationship validation are separate from
|
||||
//! structural validation.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// A typed thing discussed in a channel or thread.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum Subject {
|
||||
/// An outcome grouping repository-independent tasks.
|
||||
Project {
|
||||
/// Human-readable project name.
|
||||
title: String,
|
||||
},
|
||||
/// A unit of work belonging to a project, not a repository.
|
||||
Task {
|
||||
/// Human-readable task title.
|
||||
title: String,
|
||||
/// Stable Buzz project identity.
|
||||
project_id: Uuid,
|
||||
/// Optional parent task in the same project.
|
||||
parent_task_id: Option<Uuid>,
|
||||
/// Task workflow state.
|
||||
status: TaskStatus,
|
||||
},
|
||||
/// A Buzz repository whose Git representation is NIP-34.
|
||||
Repository {
|
||||
/// Human-readable repository name.
|
||||
title: String,
|
||||
/// Address of the upstream repository announcement, not an event ID.
|
||||
nip34_coordinate: String,
|
||||
},
|
||||
/// A branch incarnation, distinct from a ref-state snapshot.
|
||||
Branch {
|
||||
/// Stable Buzz repository identity.
|
||||
repository_id: Uuid,
|
||||
/// Full Git branch ref, for example `refs/heads/fix-login`.
|
||||
ref_name: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Initial task workflow vocabulary.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TaskStatus {
|
||||
/// Work has not started.
|
||||
Open,
|
||||
/// Work is underway.
|
||||
InProgress,
|
||||
/// Work is complete.
|
||||
Done,
|
||||
/// Work was abandoned.
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// A conversation location. Threads inherit their channel's access rules.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum Conversation {
|
||||
/// Canonical home for a project or repository.
|
||||
Channel {
|
||||
/// Authorization channel.
|
||||
channel_id: Uuid,
|
||||
},
|
||||
/// Canonical discussion for a task or branch.
|
||||
Thread {
|
||||
/// Authorization channel, not necessarily the branch repository's channel.
|
||||
channel_id: Uuid,
|
||||
/// Stable conversation anchor, independent of metadata revisions.
|
||||
root_event_id: nostr::EventId,
|
||||
},
|
||||
}
|
||||
|
||||
/// Internal structural input. Relationships are decoded from event tags,
|
||||
/// rather than duplicated in a second JSON wire representation.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SubjectDeclaration {
|
||||
/// Stable identity shared by all revisions.
|
||||
pub id: Uuid,
|
||||
/// Canonical conversation location.
|
||||
pub conversation: Conversation,
|
||||
/// Type-specific state.
|
||||
pub subject: Subject,
|
||||
}
|
||||
|
||||
/// A malformed declaration. Existence and permission errors belong to the relay.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
#[error("invalid conversation subject: {0}")]
|
||||
pub struct SubjectError(pub &'static str);
|
||||
|
||||
impl SubjectDeclaration {
|
||||
/// Validate shape only. This does not establish acceptance or authority.
|
||||
pub fn validate(&self) -> Result<(), SubjectError> {
|
||||
non_nil(self.id)?;
|
||||
let channel_id = match self.conversation {
|
||||
Conversation::Channel { channel_id } => channel_id,
|
||||
Conversation::Thread { channel_id, .. } => channel_id,
|
||||
};
|
||||
non_nil(channel_id)?;
|
||||
match (&self.subject, &self.conversation) {
|
||||
(
|
||||
Subject::Project { .. } | Subject::Repository { .. },
|
||||
Conversation::Channel { .. },
|
||||
)
|
||||
| (Subject::Task { .. } | Subject::Branch { .. }, Conversation::Thread { .. }) => {}
|
||||
_ => {
|
||||
return Err(SubjectError(
|
||||
"subject type does not match conversation type",
|
||||
))
|
||||
}
|
||||
}
|
||||
match &self.subject {
|
||||
Subject::Project { title } => validate_title(title),
|
||||
Subject::Task {
|
||||
title,
|
||||
project_id,
|
||||
parent_task_id,
|
||||
..
|
||||
} => {
|
||||
validate_title(title)?;
|
||||
non_nil(*project_id)?;
|
||||
if *project_id == self.id {
|
||||
return Err(SubjectError("task cannot be its own project"));
|
||||
}
|
||||
if let Some(parent) = parent_task_id {
|
||||
non_nil(*parent)?;
|
||||
if *parent == self.id {
|
||||
return Err(SubjectError("task cannot parent itself"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Subject::Repository {
|
||||
title,
|
||||
nip34_coordinate,
|
||||
} => {
|
||||
validate_title(title)?;
|
||||
validate_repository_coordinate(nip34_coordinate)
|
||||
}
|
||||
Subject::Branch {
|
||||
repository_id,
|
||||
ref_name,
|
||||
} => {
|
||||
non_nil(*repository_id)?;
|
||||
if *repository_id == self.id {
|
||||
return Err(SubjectError("branch cannot be its own repository"));
|
||||
}
|
||||
validate_branch_ref(ref_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn non_nil(id: Uuid) -> Result<(), SubjectError> {
|
||||
if id.is_nil() {
|
||||
Err(SubjectError("nil identity"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_title(title: &str) -> Result<(), SubjectError> {
|
||||
if title.trim().is_empty() || title.len() > 512 {
|
||||
return Err(SubjectError(
|
||||
"title must contain text and be at most 512 bytes",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate a NIP-34 repository address without resolving it.
|
||||
/// The identifier may contain colons; only the first two delimiters are structural.
|
||||
pub fn validate_repository_coordinate(value: &str) -> Result<(), SubjectError> {
|
||||
let mut parts = value.splitn(3, ':');
|
||||
if parts.next() != Some("30617") {
|
||||
return Err(SubjectError("expected a kind-30617 repository coordinate"));
|
||||
}
|
||||
let key = parts
|
||||
.next()
|
||||
.ok_or(SubjectError("missing repository author"))?;
|
||||
if key.len() != 64
|
||||
|| !key
|
||||
.bytes()
|
||||
.all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(&c))
|
||||
|| nostr::PublicKey::from_hex(key).is_err()
|
||||
{
|
||||
return Err(SubjectError(
|
||||
"repository author must be a lowercase hex public key",
|
||||
));
|
||||
}
|
||||
if parts.next().is_none() {
|
||||
return Err(SubjectError("missing repository identifier"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate a fully-qualified branch ref using Git's ref-name restrictions.
|
||||
pub fn validate_branch_ref(value: &str) -> Result<(), SubjectError> {
|
||||
let Some(name) = value.strip_prefix("refs/heads/") else {
|
||||
return Err(SubjectError("branch ref must start with refs/heads/"));
|
||||
};
|
||||
if name.is_empty()
|
||||
|| value.ends_with('.')
|
||||
|| value.contains("..")
|
||||
|| value.contains("@{")
|
||||
|| value
|
||||
.bytes()
|
||||
.any(|c| c <= b' ' || c == 127 || b"~^:?*[\\".contains(&c))
|
||||
|| value
|
||||
.split('/')
|
||||
.any(|part| part.is_empty() || part.starts_with('.') || part.ends_with(".lock"))
|
||||
{
|
||||
return Err(SubjectError("invalid Git branch ref"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
147
docs/nips/NIP-CS.md
Normal file
147
docs/nips/NIP-CS.md
Normal file
@@ -0,0 +1,147 @@
|
||||
NIP-CS
|
||||
======
|
||||
|
||||
Conversation Subjects
|
||||
---------------------
|
||||
|
||||
`draft` `optional` `relay`
|
||||
|
||||
**Depends on:** NIP-01 (signed events and filters), NIP-29 (channels).
|
||||
|
||||
## Abstract
|
||||
|
||||
A **subject** is an identifiable object with a canonical conversation location.
|
||||
This NIP defines tags for that relationship, how several subjects may occupy the
|
||||
same location, and how declarations are validated. It allocates no event kind.
|
||||
Each participating kind specifies its content, permitted relationship mode, and
|
||||
additional authorization requirements.
|
||||
|
||||
MUST, MUST NOT, SHOULD, and MAY are interpreted as in RFC 2119.
|
||||
|
||||
## Declaration tags
|
||||
|
||||
A declaration is a complete state revision, not a patch. All defined tags have
|
||||
exactly two string elements. Required tags occur exactly once; optional tags at
|
||||
most once. Duplicate tags are invalid even when their values agree.
|
||||
|
||||
| Tag | Cardinality | Value |
|
||||
|-----|-------------|-------|
|
||||
| `cs` | 1 | Protocol version: `1` |
|
||||
| `d` | 1 | Subject UUID, lowercase canonical hyphenated form, non-nil |
|
||||
| `h` | 1 | Authorization channel UUID, same encoding as `d` |
|
||||
| `location` | 1 | `channel` or `thread` |
|
||||
| `root` | 0 or 1 | Thread anchor event ID: 64 lowercase hex characters |
|
||||
| `claim` | 1 | `exclusive`, `primary`, or `attachment` |
|
||||
| `prev` | 0 or 1 | Previous accepted revision's event ID |
|
||||
|
||||
`root` MUST be absent for a channel location and present for a thread location.
|
||||
The location key is `(community, h)` for a channel, or `(community, h, root)` for
|
||||
a thread. A channel claim does not reserve every thread within that channel.
|
||||
|
||||
`d` identifies the subject within its community, independent of author and kind.
|
||||
It is not a NIP-01 addressable coordinate. Participating kinds use regular stored
|
||||
events; the relay retains revisions and maintains their current heads separately.
|
||||
|
||||
Unknown tags MAY carry non-authoritative annotations and MUST NOT change these
|
||||
semantics. Unsupported `cs` versions MUST be rejected. Type specifications may
|
||||
reserve additional tags and restrict their cardinality. Relationships MUST NOT
|
||||
also be encoded as independently authoritative copies in `content`.
|
||||
|
||||
## Claim modes
|
||||
|
||||
| Existing claim | New exclusive | New primary | New attachment |
|
||||
|----------------|---------------|-------------|----------------|
|
||||
| None | Allow | Allow | Allow |
|
||||
| Exclusive | Reject | Reject | Reject |
|
||||
| Primary | Reject | Reject | Allow |
|
||||
| Attachment(s) only | Reject | Allow | Allow |
|
||||
|
||||
An **exclusive** subject is the only subject at its location. A **primary**
|
||||
subject is the only primary subject, but permits attachments. An **attachment**
|
||||
shares a location without reserving its primary position. Attachment does not
|
||||
require a primary subject to exist.
|
||||
|
||||
The concrete kind fixes its mode; a writer MUST NOT select a weaker mode to
|
||||
bypass a constraint. Revisions of the same subject do not compete with its own
|
||||
claim. Claims are checked across all participating kinds, not per kind.
|
||||
|
||||
## Identity and revisions
|
||||
|
||||
Creation omits `prev`. It reserves an unused `d`; its signer becomes the subject
|
||||
controller. An update includes `prev` naming the current accepted head, uses the
|
||||
same kind and `d`, and is signed by that controller. Channel administration alone
|
||||
does not confer control of another subject.
|
||||
|
||||
The relay MUST compare the head, validate relationships, store the event, and
|
||||
advance the head in one transaction. Competing updates to the same head cannot
|
||||
both succeed. A repeated event ID is an idempotent duplicate, not a new revision.
|
||||
`created_at` does not determine the winning head. Rejected events are not
|
||||
broadcast or included in queries.
|
||||
|
||||
The channel, location, root, claim mode, and controller are immutable. Type
|
||||
specifications identify additional immutable fields. Retirement is a
|
||||
kind-specific state transition, not a transfer of the conversation. Deletion
|
||||
requests MUST NOT release claims or remove a subject's anchor while references
|
||||
remain; clients use retirement instead of NIP-09 deletion for subject state.
|
||||
|
||||
## Thread anchors
|
||||
|
||||
`root` names an existing, live, channel-scoped conversation anchor in `h`.
|
||||
It MUST resolve within the same community. An anchor's event ID remains stable
|
||||
when any subject metadata changes. Subject declarations are not conversation
|
||||
replies: `e` root/reply markers MUST NOT be used to express the home relationship.
|
||||
They MUST NOT increment reply counters or become ordinary timeline messages.
|
||||
|
||||
Replies use the channel's existing thread protocol and reference the anchor,
|
||||
not a subject's latest revision. For nested conversations, the home anchor can
|
||||
be a descendant; its outer root and immediate-parent ancestry remain intact.
|
||||
A subject declaration does not rewrite that ancestry or introduce membership.
|
||||
|
||||
## Authorization
|
||||
|
||||
The relay MUST check access to `h` on creation and every update. Exclusive and
|
||||
primary claims additionally require channel administration; attachments require
|
||||
channel write permission. Concrete kinds may require additional source-object
|
||||
authority. Being able to read an object is not permission to disclose it to a
|
||||
different channel.
|
||||
|
||||
Every subject revision is channel-scoped by `h`. Queries, direct-ID reads,
|
||||
counts, search, and live delivery MUST apply that channel's visibility rules.
|
||||
The existence of an unreadable subject MUST NOT be disclosed through a failed
|
||||
relationship lookup. A link grants neither source-object capabilities nor
|
||||
conversation access. Thread access always derives from its one containing
|
||||
channel, never the union or intersection of linked objects' audiences.
|
||||
|
||||
## Retrieval
|
||||
|
||||
Writes use Nostr `EVENT`; the generic HTTP equivalent is `POST /events`.
|
||||
History and live updates use `REQ`; `POST /query` provides the corresponding
|
||||
bounded historical query. No type-specific CRUD endpoint is required.
|
||||
|
||||
For a participating kind `K`:
|
||||
|
||||
```jsonc
|
||||
// Subject revisions, with live updates after EOSE.
|
||||
["REQ", "subject", {"kinds": ["<K>"], "#h": ["<channel>"], "#d": ["<subject>"]}]
|
||||
```
|
||||
|
||||
`<K>` denotes a numeric kind, not a literal string on the wire. Filters on `d`
|
||||
MUST be applied before LIMIT even though these kinds are not addressable events.
|
||||
History is revision history, not a current-state list. Clients fold accepted
|
||||
revisions using `prev`, deduplicating by event ID; an incomplete chain requires
|
||||
fetching missing revisions rather than selecting the largest timestamp.
|
||||
|
||||
Location queries use `#h` and `#root` semantics through the relay's generic query
|
||||
extension; multi-character tag filtering is a Buzz extension, not guaranteed by
|
||||
NIP-01. All relationship predicates and authorization MUST run before pagination.
|
||||
A relay MUST reject unsupported relationship filters rather than ignore them.
|
||||
EOSE means the historical response ended; it is not a durable replay cursor.
|
||||
|
||||
## Invalid declarations
|
||||
|
||||
- Two `h` tags, even if identical.
|
||||
- A thread location without `root`, or a channel location with `root`.
|
||||
- An exclusive claim at a location already occupied by an attachment.
|
||||
- A second primary subject at a location with an existing primary.
|
||||
- An update with a stale `prev`, different signer, or changed home.
|
||||
- A thread anchor in a different channel or community.
|
||||
121
docs/nips/NIP-PT.md
Normal file
121
docs/nips/NIP-PT.md
Normal file
@@ -0,0 +1,121 @@
|
||||
NIP-PT
|
||||
======
|
||||
|
||||
Projects and Tasks
|
||||
------------------
|
||||
|
||||
`draft` `optional` `relay`
|
||||
|
||||
**Depends on:** NIP-CS (conversation subjects).
|
||||
|
||||
## Kinds
|
||||
|
||||
| Kind | Name | Location | Claim |
|
||||
|------|------|----------|-------|
|
||||
| `45010` | Project | channel | exclusive |
|
||||
| `45012` | Task | thread | primary |
|
||||
|
||||
These are regular stored events, not addressable replacements. Identity,
|
||||
controller authority, revision chaining, and common tags follow NIP-CS.
|
||||
A project groups tasks, not repositories. Neither type requires a Git reference.
|
||||
|
||||
## Project event
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"kind": 45010,
|
||||
"tags": [
|
||||
["cs", "1"],
|
||||
["d", "<project-uuid>"],
|
||||
["h", "<channel-uuid>"],
|
||||
["location", "channel"],
|
||||
["claim", "exclusive"]
|
||||
],
|
||||
"content": "{\"title\":\"Checkout reliability\",\"description\":\"Reduce failed checkouts\",\"archived\":false}"
|
||||
}
|
||||
```
|
||||
|
||||
Standard signed-event fields are omitted in examples. An update also supplies
|
||||
`prev` as defined in NIP-CS.
|
||||
|
||||
| Content field | JSON type | Constraints |
|
||||
|---------------|-----------|-------------|
|
||||
| `title` | string | Required; non-whitespace; at most 512 UTF-8 bytes |
|
||||
| `description` | string | Required; Markdown; at most 65536 UTF-8 bytes |
|
||||
| `archived` | boolean | Required |
|
||||
|
||||
Content MUST be a JSON object without duplicate or unknown fields. Archiving
|
||||
prevents creation of additional tasks but does not archive the channel, hide
|
||||
existing tasks, revoke access, or release the channel claim. Unarchiving is an
|
||||
ordinary authorized revision.
|
||||
|
||||
## Task event
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"kind": 45012,
|
||||
"tags": [
|
||||
["cs", "1"],
|
||||
["d", "<task-uuid>"],
|
||||
["h", "<project-channel-uuid>"],
|
||||
["location", "thread"],
|
||||
["root", "<discussion-anchor-event-id>"],
|
||||
["claim", "primary"],
|
||||
["project", "<project-uuid>"]
|
||||
],
|
||||
"content": "{\"title\":\"Handle payment timeout\",\"description\":\"Preserve the pending payment\",\"status\":\"open\"}"
|
||||
}
|
||||
```
|
||||
|
||||
| Additional tag | Cardinality | Meaning |
|
||||
|----------------|-------------|---------|
|
||||
| `project` | 1 | Project subject UUID |
|
||||
| `parent` | 0 or 1 | Parent task subject UUID |
|
||||
|
||||
Both tags have exactly two elements and use NIP-CS UUID encoding. They are
|
||||
immutable after creation. A subtask remains a task, with its own primary thread
|
||||
claim. Parentage alone does not require nested thread ancestry or create a new
|
||||
authorization boundary.
|
||||
|
||||
| Content field | JSON type | Constraints |
|
||||
|---------------|-----------|-------------|
|
||||
| `title` | string | Required; non-whitespace; at most 512 UTF-8 bytes |
|
||||
| `description` | string | Required; Markdown; at most 65536 UTF-8 bytes |
|
||||
| `status` | string | Required; `open`, `in_progress`, `done`, or `cancelled` |
|
||||
|
||||
Content MUST reject duplicate and unknown fields. Any listed status may
|
||||
transition to any other listed status under ordinary update authority.
|
||||
Completion or cancellation does not delete the discussion or retire attached
|
||||
subjects. There is no implied transition from Git merge state to task status.
|
||||
|
||||
## Relationship validation
|
||||
|
||||
The relay MUST resolve `project` to an accepted kind-45010 subject in the same
|
||||
community. Its home channel MUST equal the task's `h`. At creation, the project
|
||||
MUST NOT be archived. `parent`, when present, MUST resolve to a different
|
||||
kind-45012 subject in the same project. Cyclic parentage MUST be rejected.
|
||||
|
||||
A task's primary claim allows attachment subjects, but not another primary or
|
||||
exclusive subject. Project and task creation require the NIP-CS claim authority;
|
||||
updates require the controller and continued channel access. A task's controller
|
||||
need not be the project's controller.
|
||||
|
||||
Example rejections include a missing project, a parent from another project,
|
||||
a task rooted in another channel, a second task on an occupied primary thread,
|
||||
and an update changing `project` or `parent`.
|
||||
|
||||
## Queries
|
||||
|
||||
Project listing filters kind 45010 by `h`. Task listing filters kind 45012 by
|
||||
`project`; subtask listing additionally filters `parent`. These relationship
|
||||
filters use the generic relay query extension and MUST run before LIMIT.
|
||||
Results are signed revisions under NIP-CS history semantics, not a separate
|
||||
project-maintained task list. A client materializes current task state by
|
||||
subject ID and accepted revision chain.
|
||||
|
||||
## Compatibility
|
||||
|
||||
Kind 30621 retains its multi-repository grouping semantics. Kind 1621 retains
|
||||
its NIP-34 issue semantics. Neither is a project or task under this NIP merely
|
||||
because it carries a channel reference. Conversion creates an explicit new
|
||||
subject and preserves old event identities and links.
|
||||
133
docs/nips/NIP-RB.md
Normal file
133
docs/nips/NIP-RB.md
Normal file
@@ -0,0 +1,133 @@
|
||||
NIP-RB
|
||||
======
|
||||
|
||||
Repositories and Branches
|
||||
-------------------------
|
||||
|
||||
`draft` `optional` `relay`
|
||||
|
||||
**Depends on:** NIP-CS (conversation subjects), NIP-34 (Git announcements).
|
||||
|
||||
## Kinds
|
||||
|
||||
| Kind | Name | Location | Claim |
|
||||
|------|------|----------|-------|
|
||||
| `45011` | Repository | channel | exclusive |
|
||||
| `45014` | Branch | thread | attachment |
|
||||
|
||||
These are regular stored events. NIP-CS defines identity, revision chaining,
|
||||
controller authority, and common tags. A repository subject maps a Buzz identity
|
||||
to a NIP-34 repository; it does not create another Git repository. A branch
|
||||
subject identifies a collaboration lifecycle, not a ref-state snapshot.
|
||||
|
||||
## Repository event
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"kind": 45011,
|
||||
"tags": [
|
||||
["cs", "1"],
|
||||
["d", "<repository-uuid>"],
|
||||
["h", "<repository-channel-uuid>"],
|
||||
["location", "channel"],
|
||||
["claim", "exclusive"],
|
||||
["a", "30617:<owner-public-key>:checkout"]
|
||||
],
|
||||
"content": "{\"title\":\"Checkout\",\"archived\":false}"
|
||||
}
|
||||
```
|
||||
|
||||
Standard signed-event fields are omitted. Updates include `prev`.
|
||||
|
||||
| Additional tag | Cardinality | Meaning |
|
||||
|----------------|-------------|---------|
|
||||
| `a` | 1 | NIP-34 repository coordinate; exactly two tag elements |
|
||||
|
||||
The coordinate MUST start with `30617:` followed by a 64-character lowercase
|
||||
hex public key, a colon, and the repository identifier. Split on the first two
|
||||
colons only; the remaining identifier is literal. The relay MUST resolve the
|
||||
coordinate to an existing repository announcement in the same community.
|
||||
The coordinate is immutable and unique among repository subjects in that
|
||||
community, including archived subjects.
|
||||
|
||||
| Content field | JSON type | Constraints |
|
||||
|---------------|-----------|-------------|
|
||||
| `title` | string | Required; non-whitespace; at most 512 UTF-8 bytes |
|
||||
| `archived` | boolean | Required |
|
||||
|
||||
Duplicate and unknown fields MUST be rejected. Archiving prevents new branch
|
||||
subjects but does not delete code, hide discussions, or release the home claim.
|
||||
Clone URLs, commit tips, and ref snapshots remain NIP-34 state, not independently
|
||||
writable fields on this subject.
|
||||
|
||||
## Branch event
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"kind": 45014,
|
||||
"tags": [
|
||||
["cs", "1"],
|
||||
["d", "<branch-uuid>"],
|
||||
["h", "<discussion-channel-uuid>"],
|
||||
["location", "thread"],
|
||||
["root", "<discussion-anchor-event-id>"],
|
||||
["claim", "attachment"],
|
||||
["repository", "<repository-uuid>"],
|
||||
["ref", "refs/heads/fix-timeout"]
|
||||
],
|
||||
"content": "{\"retired\":false}"
|
||||
}
|
||||
```
|
||||
|
||||
| Additional tag | Cardinality | Meaning |
|
||||
|----------------|-------------|---------|
|
||||
| `repository` | 1 | Buzz repository UUID using NIP-CS encoding |
|
||||
| `ref` | 1 | Full Git branch ref |
|
||||
|
||||
Tags have exactly two elements. Both values are immutable. The repository MUST
|
||||
resolve to kind 45011 in the same community and MUST NOT be archived on branch
|
||||
creation. The branch resolves its NIP-34 target through that repository; it MUST
|
||||
NOT repeat an independently authoritative coordinate.
|
||||
|
||||
`ref` MUST start with `refs/heads/` and satisfy Git ref-name rules: no empty or
|
||||
dot-prefixed path component, `.lock` component suffix, trailing dot, `..`, `@{`,
|
||||
ASCII control/space, DEL, or any of `~ ^ : ? * [ \\`. A branch subject may precede
|
||||
the first push; ref existence is not required. At most one non-retired subject
|
||||
may occupy `(community, repository, ref)`.
|
||||
|
||||
Content contains exactly the required boolean `retired`. Retirement is terminal:
|
||||
a retired branch cannot become active again. A new incarnation uses a new UUID
|
||||
and may claim the released ref slot, but the retired subject retains its
|
||||
conversation association. Ref deletion alone does not retire a subject; rename
|
||||
is retirement followed by creation, not a silent identity rewrite.
|
||||
|
||||
## Authorization and disclosure
|
||||
|
||||
Repository creation requires control of the NIP-34 announcement and channel
|
||||
administration. Its Git access binding MUST match the subject's channel; a
|
||||
conflicting binding MUST be rejected rather than silently overwritten. Later
|
||||
Git metadata writes MUST NOT contradict an adopted binding.
|
||||
|
||||
Branch creation and updates require the NIP-CS controller rules, destination
|
||||
channel write permission, and repository administration authority. This is
|
||||
stronger than code read or ordinary push permission: placing a ref name in
|
||||
another channel constitutes disclosure to that audience.
|
||||
|
||||
A branch's discussion channel MAY differ from its repository channel. Repository
|
||||
readers do not thereby gain access to the discussion, and discussion readers do
|
||||
not gain code access. Rendering a branch MUST NOT implicitly fetch or expose Git
|
||||
content without a separate repository access check. Git-originated notifications
|
||||
and agent output are subject to the same disclosure boundary.
|
||||
|
||||
## Queries and compatibility
|
||||
|
||||
Query repositories by kind 45011 and `h`; query branches by kind 45014 and
|
||||
`repository`, optionally `ref`. Query discussion attachments by `h` and `root`.
|
||||
Relationship filtering MUST precede pagination and use NIP-CS authorization and
|
||||
revision-history semantics.
|
||||
|
||||
NIP-34 announcement 30617 and ref state 30618 retain their identities. A 30618
|
||||
event may describe many refs and MUST NOT serve as a branch subject identity.
|
||||
Pull requests, patches, and their statuses remain separate Git protocol objects.
|
||||
Adopting a repository does not retrospectively restrict copies of previously
|
||||
published metadata or reinterpret legacy comments as private channel replies.
|
||||
Reference in New Issue
Block a user