Skip to content

Commit 19dd154

Browse files
authored
feat(dna): Group DNA per-group coordination DHT with Sweettest suite (#107)
* feat(dna/group): Group DNA integrity and coordinator zomes Introduces the Group DNA as the per-group coordination layer of the Lobby → Group → NDO hierarchy. Each group runs in its own cloned cell (separate DHT, same DNA template) provisioned via clone_cell. Entry types (integrity zome): - GroupProfile — name, description; identity from action header - GroupMembership — group_hash, optional role; member from action header - WorkLog — group_hash, description, hours (> 0); author from action header - SoftLink — group_hash, target_ndo_hash, description; planning-only (ADR-GROUP-04) Coordinator externs (16): - create_group, get_group, get_my_group, update_group (NotAuthor guard) - join_group (AlreadyMember guard), leave_group, get_group_members, is_member - log_work, get_work_logs, get_my_work_logs, delete_work_log (NotAuthor guard) - create_soft_link, get_soft_links, delete_soft_link (NotAuthor guard), init happ.yaml: group role added with deferred: true and clone_limit: 64 Shared crate: GroupError domain error enum added * feat(dna/lobby): replace NdoAnnouncement with GroupAnnouncement The Lobby DHT is now the registry for group cells, not NDOs directly. NDOs are discovered through group cells (Lobby → Groups → NDOs). - NdoAnnouncement entry type and announce_ndo, get_all_ndo_announcements, get_ndo_announcement, update_ndo_announcement replaced by GroupAnnouncement and announce_group, get_all_group_announcements, get_group_announcement_by_dna_hash - get_my_groups now returns real GroupDescriptorStub from DHT instead of stub solo workspace - AnnounceGroupInput replaces AnnounceNdoInput and UpdateNdoAnnouncementInput in shared crate * test(group): Sweettest suite — 13 tests for Group DNA and Lobby update Group DNA Sweettest (dnas/group/tests/): - create_group_returns_profile, get_group_by_hash - join_group_creates_membership, leave_group_removes_membership - is_member_reflects_membership_state, duplicate_join_returns_already_member_error - log_work_and_get_work_logs, get_my_work_logs_returns_own_logs - create_soft_link_and_get_soft_links, get_my_group_returns_created_group - update_group_changes_name - empty_group_name_rejected, work_log_zero_hours_rejected Thread limit: 13 tests × 2 conductors; --test-threads 6 required. Lobby Sweettest updated for GroupAnnouncement API (announce_group, get_my_group_announcements, get_group_announcement_by_dna_hash). * feat(ui): TypeScript service layer for Group DNA GroupService stub replaced with real callZome implementation. Interface takes groupCellId: CellId (DnaHash + AgentPubKey) — CellId is the correct addressing for cloned group cells (ADR-GROUP-03). - group.types.ts: GroupProfile, GroupMembership, WorkLog, SoftLink types and input types; GroupGovernanceRule removed - group.service.ts: getMembers, getWorkLogs, getSoftLinks implemented via callGroupZome helper; GroupServiceLive built as Effect-TS Layer - cell.manager.ts: getGroupCellHandle added to resolve DnaHash to group CellId - lobby.service.ts: GroupAnnouncement API wired; get_my_groups updated - lobby.types.ts: NdoAnnouncement → GroupAnnouncement; shared-types updated * docs(group): API reference, architecture overview, test commands - group_zome.md: full API reference for Group DNA (entry types, link types, 16 coordinator functions including delete_work_log and delete_soft_link, validation rules, error types, ADRs, test commands) - architecture_overview.md: updated to multi-DNA hApp structure with Nondominium DNA / Lobby DNA / Group DNA sections - lobby_zome.md: updated for GroupAnnouncement API - API_REFERENCE.md, IMPLEMENTATION_STATUS.md, TEST_COMMANDS.md, DOCUMENTATION_INDEX.md updated for Group DNA
1 parent 1b4dfc9 commit 19dd154

42 files changed

Lines changed: 2252 additions & 463 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ members = [
1414
"dnas/nondominium/zomes/integrity/zome_resource",
1515
"dnas/nondominium/zomes/coordinator/zome_gouvernance",
1616
"dnas/nondominium/zomes/integrity/zome_gouvernance",
17+
"dnas/group/zomes/coordinator/zome_group",
18+
"dnas/group/zomes/integrity/zome_group_integrity",
1719
"crates/shared",
1820
"dnas/nondominium/tests",
21+
"dnas/group/tests",
1922
"dnas/lobby/tests",
2023
]
2124
# default-members excludes Sweettest crates so that
@@ -32,6 +35,8 @@ default-members = [
3235
"dnas/nondominium/zomes/coordinator/zome_gouvernance",
3336
"dnas/nondominium/zomes/integrity/zome_gouvernance",
3437
"dnas/nondominium/zomes/coordinator/misc",
38+
"dnas/group/zomes/coordinator/zome_group",
39+
"dnas/group/zomes/integrity/zome_group_integrity",
3540
"crates/shared",
3641
"dnas/lobby/zomes/coordinator/zome_lobby_coordinator",
3742
"dnas/lobby/zomes/integrity/zome_lobby_integrity",

crates/shared/src/errors.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,33 @@ impl From<GovernanceError> for WasmError {
165165
wasm_error!(WasmErrorInner::Guest(err.to_string()))
166166
}
167167
}
168+
169+
// =============================================================================
170+
// GroupError — domain errors for zome_group
171+
// =============================================================================
172+
173+
#[derive(Debug, thiserror::Error)]
174+
pub enum GroupError {
175+
#[error("Group not found: {0}")]
176+
GroupNotFound(String),
177+
#[error("Already a member of this group")]
178+
AlreadyMember,
179+
#[error("Not a member of this group")]
180+
NotMember,
181+
#[error("Not the author of this entry")]
182+
NotAuthor,
183+
#[error("Serialization error: {0}")]
184+
SerializationError(String),
185+
#[error("Entry operation failed: {0}")]
186+
EntryOperationFailed(String),
187+
#[error("Link operation failed: {0}")]
188+
LinkOperationFailed(String),
189+
#[error("Invalid input: {0}")]
190+
InvalidInput(String),
191+
}
192+
193+
impl From<GroupError> for WasmError {
194+
fn from(err: GroupError) -> Self {
195+
wasm_error!(WasmErrorInner::Guest(err.to_string()))
196+
}
197+
}

crates/shared/src/io/lobby.rs

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,6 @@
1-
use crate::types::{LifecycleStage, PropertyRegime, ResourceNature};
21
use hdi::prelude::*;
32
use serde::{Deserialize, Serialize};
43

5-
/// Input to `announce_ndo` in `zome_lobby_coordinator`.
6-
#[derive(Debug, Serialize, Deserialize)]
7-
pub struct AnnounceNdoInput {
8-
pub ndo_name: String,
9-
pub ndo_dna_hash: DnaHash,
10-
pub network_seed: String,
11-
pub ndo_identity_hash: ActionHash,
12-
pub lifecycle_stage: LifecycleStage,
13-
pub property_regime: PropertyRegime,
14-
pub resource_nature: ResourceNature,
15-
pub description: Option<String>,
16-
}
17-
184
/// Input to `upsert_lobby_agent_profile` in `zome_lobby_coordinator`.
195
#[derive(Debug, Serialize, Deserialize)]
206
pub struct LobbyAgentProfileInput {
@@ -23,15 +9,19 @@ pub struct LobbyAgentProfileInput {
239
pub bio: Option<String>,
2410
}
2511

26-
/// Input to `update_ndo_announcement` in `zome_lobby_coordinator`.
12+
/// Input to `announce_group` in `zome_lobby_coordinator`.
13+
///
14+
/// Follows the Lobby → Groups → NDOs hierarchy: the Lobby is the registry for groups,
15+
/// groups host NDOs. NDOs are discovered through group cells, not the Lobby DHT.
2716
#[derive(Debug, Serialize, Deserialize)]
28-
pub struct UpdateNdoAnnouncementInput {
29-
pub original_action_hash: ActionHash,
30-
pub new_lifecycle_stage: LifecycleStage,
17+
pub struct AnnounceGroupInput {
18+
pub group_name: String,
19+
pub group_dna_hash: DnaHash,
20+
pub network_seed: String,
21+
pub description: Option<String>,
3122
}
3223

33-
/// Minimal group descriptor stub until Group DNA ships in issue #101.
34-
/// Used by `get_my_groups` in `zome_lobby_coordinator`.
24+
/// Minimal group descriptor stub returned by `get_my_groups`.
3525
#[derive(Debug, Serialize, Deserialize)]
3626
pub struct GroupDescriptorStub {
3727
pub id: String,

crates/shared/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pub mod errors;
1414
#[cfg(feature = "coordinator")]
1515
pub mod paths;
1616
#[cfg(feature = "coordinator")]
17-
pub use errors::{CommonError, GovernanceError, PersonError, ResourceError};
17+
pub use errors::{CommonError, GovernanceError, GroupError, PersonError, ResourceError};
1818

1919
#[cfg(feature = "coordinator")]
2020
use hdk::prelude::*;
@@ -139,3 +139,4 @@ pub mod validation {
139139
Ok(())
140140
}
141141
}
142+

dnas/group/tests/Cargo.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "group_sweettest"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# This crate is NOT compiled to WASM. It uses the holochain test_utils feature
7+
# to run Sweettest in-process against the compiled .dna bundles.
8+
#
9+
# IMPORTANT: Run with CARGO_TARGET_DIR=target/native-tests to avoid
10+
# artifact conflicts with the wasm32-unknown-unknown build directory.
11+
#
12+
# Prerequisites: `bun run build:happ` must be run before `cargo test`
13+
14+
[dependencies]
15+
holochain = { workspace = true }
16+
tokio = { workspace = true }
17+
serde = { workspace = true }
18+
holochain_serialized_bytes = { workspace = true }
19+
20+
[[test]]
21+
name = "group"
22+
path = "src/group/mod.rs"
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use holochain::prelude::*;
2+
use holochain::sweettest::*;
3+
use std::sync::atomic::{AtomicU64, Ordering};
4+
5+
/// Path to the compiled group DNA bundle, resolved relative to this crate's Cargo.toml.
6+
pub const GROUP_DNA_PATH: &str = concat!(
7+
env!("CARGO_MANIFEST_DIR"),
8+
"/../workdir/group.dna"
9+
);
10+
11+
// Each test invocation gets a unique monotonic ID. Combined with the process PID this
12+
// guarantees distinct network seeds even when multiple test processes run in parallel.
13+
static TEST_INSTANCE: AtomicU64 = AtomicU64::new(0);
14+
15+
pub fn unique_seed() -> NetworkSeed {
16+
let id = TEST_INSTANCE.fetch_add(1, Ordering::SeqCst);
17+
format!("test-{}-{}", std::process::id(), id).into()
18+
}
19+
20+
/// Spin up two conductors, each with the group DNA installed.
21+
///
22+
/// Returns `(conductors, cell_alice, cell_bob)`.
23+
pub async fn setup_two_agents() -> (SweetConductorBatch, SweetCell, SweetCell) {
24+
let mut conductors =
25+
SweetConductorBatch::from_config_rendezvous(2, SweetConductorConfig::standard()).await;
26+
27+
let dna = SweetDnaFile::from_bundle(std::path::Path::new(GROUP_DNA_PATH))
28+
.await
29+
.expect("Failed to load group DNA bundle. Did you run `bun run build:happ`?")
30+
.with_network_seed(unique_seed())
31+
.await;
32+
33+
let apps = conductors
34+
.setup_app("group", &[dna])
35+
.await
36+
.expect("Failed to install group app on conductors");
37+
38+
conductors.exchange_peer_info().await;
39+
40+
let ((cell_alice,), (cell_bob,)) = apps.into_tuples();
41+
(conductors, cell_alice, cell_bob)
42+
}

dnas/group/tests/src/common/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod conductors;
2+
pub use conductors::*;

0 commit comments

Comments
 (0)