From 5a4e1446be58f55af60272f8c9fb253b2b5274b2 Mon Sep 17 00:00:00 2001 From: Tiberius Brastaviceanu Date: Thu, 29 Oct 2026 16:22:29 -0400 Subject: [PATCH 01/14] feat(shared): add Measure type and migrate quantity/unit to Measure Introduces `vf:Measure { numerical_value, has_unit }` as a shared type in crates/shared, mirrored at the integrity + coordinator layers of both zome_resource and zome_gouvernance. - EconomicResource.{quantity, unit} -> quantity: Measure - EconomicEvent.resource_quantity -> Measure (was f64) - Commitment gains resource_quantity: Option and effort_quantity: Option to track with ValueFlows flow fields - propose_commitment accepts explicit receiver (was hardcoded to caller), so third parties can author commitments on behalf of two other agents - EconomicResourceView and TransitionContext.quantity_change kept but switched to Measure / documented as magnitude-only - Measure::validate() enforces non-NaN, finite, non-negative numerical_value and non-empty unit; zero permitted (post-depletion observations) - Integrity validation plumbed through on create for EconomicResource, EconomicEvent, and Commitment - Sweettest mirror structs updated accordingly (resource/mod.rs, governance/mod.rs) Preparing for commitment-pool-experiment: - Grassroots-side tooling (and hREA) can read the same Measure shape - Commitments on the pool can now carry resource/effort quantities without squeezing them into free-text notes cargo build + bun run build:zomes both clean. Co-authored-by: Cursor (cherry picked from commit dcbc019c3d41dc6979d88c133019d2a414a77fc4) --- crates/shared/src/io/governance.rs | 12 +++-- crates/shared/src/types.rs | 45 +++++++++++++++++++ dnas/nondominium/tests/src/governance/mod.rs | 24 ++++++---- dnas/nondominium/tests/src/resource/mod.rs | 11 ++--- .../zome_gouvernance/src/commitment.rs | 35 ++++++++++++++- .../zome_gouvernance/src/economic_event.rs | 15 ++++++- .../zome_resource/src/economic_resource.rs | 38 ++++++++-------- .../integrity/zome_gouvernance/src/lib.rs | 34 +++++++++++++- .../zomes/integrity/zome_resource/src/lib.rs | 27 ++++++----- 9 files changed, 186 insertions(+), 55 deletions(-) diff --git a/crates/shared/src/io/governance.rs b/crates/shared/src/io/governance.rs index 9cfdb625..ed3b12ec 100644 --- a/crates/shared/src/io/governance.rs +++ b/crates/shared/src/io/governance.rs @@ -1,7 +1,7 @@ use crate::constraints::ConstraintViolation; use crate::types::{ - BenefitClause, LifecycleStage, NdoLinkType, OperationalState, PropertyRegime, ResourceNature, - Rivalry, VfAction, + BenefitClause, LifecycleStage, Measure, NdoLinkType, OperationalState, PropertyRegime, + ResourceNature, Rivalry, VfAction, }; use hdi::prelude::*; use serde::{Deserialize, Serialize}; @@ -67,8 +67,9 @@ pub struct NdoClassificationView { /// Wire-compatible view of `EconomicResource` for cross-zome transition requests. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EconomicResourceView { - pub quantity: f64, - pub unit: String, + /// ValueFlows `resourceQuantity` as a `Measure`; replaces the previous + /// `quantity: f64 + unit: String` pair. + pub quantity: Measure, pub custodian: AgentPubKey, pub current_location: Option, pub operational_state: OperationalState, @@ -77,6 +78,9 @@ pub struct EconomicResourceView { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TransitionContext { pub target_location: Option, + /// Signed delta applied to the resource's `Measure.numerical_value` during + /// the transition. The unit is inherited from the source resource — this is + /// a magnitude, not a unit redefinition. pub quantity_change: Option, pub target_custodian: Option, pub process_notes: Option, diff --git a/crates/shared/src/types.rs b/crates/shared/src/types.rs index 40a61d91..bd3467e5 100644 --- a/crates/shared/src/types.rs +++ b/crates/shared/src/types.rs @@ -218,6 +218,51 @@ pub struct NdoDnaProperties { pub created_at: Timestamp, } +// ─── ValueFlows Measure ────────────────────────────────────────────────────── +// +// `vf:Measure` is the shared value-with-unit type ValueFlows attaches to flow +// quantities (`resourceQuantity`, `effortQuantity`, `availableQuantity`, +// `minimumQuantity`). Carrying it as its own type — rather than a loose +// `f64 + String` pair — keeps Intent / Commitment / EconomicEvent / Claim +// aligned with VF 1.0 and lets hREA / Grassroots-side tooling consume the same +// shape without reshaping on the wire. +// +// `unit` is kept open as a String for the MVP: VF spec recommends om2 unit URIs +// (e.g. `om2:kilogram`), but a typed unit vocabulary is a cross-cutting +// standardisation task that is out of scope for the commitment-pool experiment. +// Downstream code should treat `unit` as case-sensitive and compare it as-is. +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct Measure { + pub numerical_value: f64, + pub has_unit: String, +} + +impl Measure { + pub fn new(numerical_value: f64, has_unit: impl Into) -> Self { + Self { + numerical_value, + has_unit: has_unit.into(), + } + } + + /// Validation used across all VF entries carrying a Measure. + /// A Measure must have a non-NaN, non-infinite, non-negative numerical value + /// and a non-empty unit. Zero is permitted — some flows legitimately record + /// nil-quantity observations. + pub fn validate(&self) -> Result<(), String> { + if self.numerical_value.is_nan() || !self.numerical_value.is_finite() { + return Err("Measure numerical_value must be a finite number".to_string()); + } + if self.numerical_value < 0.0 { + return Err("Measure numerical_value cannot be negative".to_string()); + } + if self.has_unit.trim().is_empty() { + return Err("Measure has_unit cannot be empty".to_string()); + } + Ok(()) + } +} + // ─── ValueFlows action enum ─────────────────────────────────────────────────── // Shared here so ValidateContributionInput (io/governance.rs) can reference it // without needing to import from the governance integrity zome (a WASM crate). diff --git a/dnas/nondominium/tests/src/governance/mod.rs b/dnas/nondominium/tests/src/governance/mod.rs index d9086a97..24ed8929 100644 --- a/dnas/nondominium/tests/src/governance/mod.rs +++ b/dnas/nondominium/tests/src/governance/mod.rs @@ -20,7 +20,9 @@ use nondominium_shared::io::governance::{ CreateAgreementInput, CreateNdoHardLinkInput, GetNdoHardLinksByTypeInput, UpdateAgreementInput, ValidateContributionInput, }; -use nondominium_shared::types::{BeneficiaryRef, BenefitClause, BenefitType, NdoLinkType, VfAction}; +use nondominium_shared::types::{ + BeneficiaryRef, BenefitClause, BenefitType, Measure, NdoLinkType, VfAction, +}; // ─── Local output types (partial views for assertion — reference entry types // from integrity zomes which cannot be imported in native test crates) ────── @@ -74,7 +76,7 @@ struct LogEconomicEventInput { pub provider: AgentPubKey, pub receiver: AgentPubKey, pub resource_inventoried_as: ActionHash, - pub resource_quantity: f64, + pub resource_quantity: Measure, pub note: Option, pub commitment_hash: Option, pub generate_pprs: Option, @@ -271,7 +273,7 @@ async fn create_and_get_ndo_hard_link() { provider: alice_key.clone(), receiver: alice_key.clone(), resource_inventoried_as: stub_resource, - resource_quantity: 1.0, + resource_quantity: Measure::new(1.0, "each"), note: Some("link fulfillment event".to_string()), commitment_hash: None, generate_pprs: Some(false), @@ -371,7 +373,7 @@ async fn nondominium_transfer_event_is_hard_rejected() { provider: alice_key.clone(), receiver: alice_key.clone(), resource_inventoried_as: stub_resource, - resource_quantity: 1.0, + resource_quantity: Measure::new(1.0, "each"), note: Some("should fail".to_string()), commitment_hash: None, generate_pprs: Some(false), @@ -466,8 +468,7 @@ async fn evaluate_state_transition_hard_and_soft_paths() { #[derive(Debug, Clone, Serialize, Deserialize)] struct EconomicResourceView { - pub quantity: f64, - pub unit: String, + pub quantity: Measure, pub custodian: AgentPubKey, pub current_location: Option, pub operational_state: String, @@ -499,8 +500,7 @@ async fn evaluate_state_transition_hard_and_soft_paths() { } let resource = EconomicResourceView { - quantity: 1.0, - unit: "each".to_string(), + quantity: Measure::new(1.0, "each"), custodian: alice_key.clone(), current_location: None, operational_state: "Available".to_string(), @@ -579,6 +579,9 @@ struct ProposeCommitmentInput { pub resource_hash: Option, pub resource_spec_hash: Option, pub provider: AgentPubKey, + pub receiver: AgentPubKey, + pub resource_quantity: Option, + pub effort_quantity: Option, pub due_date: Timestamp, pub note: Option, pub ndo_identity_hash: ActionHash, @@ -637,6 +640,9 @@ async fn commitment_is_discoverable_by_its_author() { resource_hash: None, resource_spec_hash: None, provider: alice_key.clone(), + receiver: alice_key.clone(), + resource_quantity: None, + effort_quantity: None, due_date: Timestamp::now(), note: Some("discoverable commitment".to_string()), ndo_identity_hash: ndo.action_hash.clone(), @@ -698,7 +704,7 @@ async fn economic_event_is_discoverable_by_its_author() { provider: alice_key.clone(), receiver: alice_key.clone(), resource_inventoried_as: stub_resource, - resource_quantity: 1.0, + resource_quantity: Measure::new(1.0, "each"), note: Some("discoverable event".to_string()), commitment_hash: None, generate_pprs: Some(false), diff --git a/dnas/nondominium/tests/src/resource/mod.rs b/dnas/nondominium/tests/src/resource/mod.rs index 2b4afc51..0cf3865b 100644 --- a/dnas/nondominium/tests/src/resource/mod.rs +++ b/dnas/nondominium/tests/src/resource/mod.rs @@ -20,7 +20,7 @@ use holochain::prelude::*; use holochain::sweettest::{await_consistency_s, SweetCell, SweetConductorBatch}; -use nondominium_shared::types::OperationalState; +use nondominium_shared::types::{Measure, OperationalState}; use serde::{Deserialize, Serialize}; use nondominium_sweettest::common::*; @@ -118,15 +118,13 @@ struct UpdateResourceSpecificationInput { #[derive(Debug, Serialize, Deserialize)] struct EconomicResourceInput { pub spec_hash: ActionHash, - pub quantity: f64, - pub unit: String, + pub quantity: Measure, pub current_location: Option, } #[derive(Debug, Serialize, Deserialize)] struct EconomicResource { - pub quantity: f64, - pub unit: String, + pub quantity: Measure, pub custodian: AgentPubKey, pub current_location: Option, pub operational_state: OperationalState, @@ -327,8 +325,7 @@ async fn economic_resource_operational_state_lifecycle() { "create_economic_resource", EconomicResourceInput { spec_hash: spec_out.spec_hash, - quantity: 1.0, - unit: "each".to_string(), + quantity: Measure::new(1.0, "each"), current_location: Some("workshop".to_string()), }, ) diff --git a/dnas/nondominium/zomes/coordinator/zome_gouvernance/src/commitment.rs b/dnas/nondominium/zomes/coordinator/zome_gouvernance/src/commitment.rs index ce5a1cf5..7de4f093 100644 --- a/dnas/nondominium/zomes/coordinator/zome_gouvernance/src/commitment.rs +++ b/dnas/nondominium/zomes/coordinator/zome_gouvernance/src/commitment.rs @@ -1,5 +1,6 @@ use crate::GovernanceError; use hdk::prelude::*; +use nondominium_shared::types::Measure; use zome_gouvernance_integrity::*; // ============================================================================ @@ -12,6 +13,18 @@ pub struct ProposeCommitmentInput { pub resource_hash: Option, pub resource_spec_hash: Option, pub provider: AgentPubKey, + /// Explicit receiver of the flow. Previously the receiver was hardcoded to + /// the caller, which forced the provider-is-caller convention and prevented + /// third parties (e.g. a Group member proposing a commitment on behalf of + /// two other agents) from authoring commitments. Making this explicit also + /// keeps us closer to the ValueFlows flow pair (provider ↔ receiver). + pub receiver: AgentPubKey, + /// ValueFlows `resourceQuantity` for countable commitments. Optional for + /// commitments whose quantity is implied by context (e.g. "repair this + /// machine"). + pub resource_quantity: Option, + /// ValueFlows `effortQuantity` for labour/time components (e.g. "4 h"). + pub effort_quantity: Option, pub due_date: Timestamp, pub note: Option, /// Layer 0 identity for action-constraint evaluation. @@ -26,19 +39,37 @@ pub struct ProposeCommitmentOutput { #[hdk_extern] pub fn propose_commitment(input: ProposeCommitmentInput) -> ExternResult { - let agent_info = agent_info()?; + let _agent_info = agent_info()?; let now = sys_time()?; + // Shape-check any Measures before integrity sees them. + if let Some(ref m) = input.resource_quantity { + m.validate().map_err(|e| { + wasm_error!(WasmErrorInner::Guest(format!( + "Invalid resource_quantity: {e}" + ))) + })?; + } + if let Some(ref m) = input.effort_quantity { + m.validate().map_err(|e| { + wasm_error!(WasmErrorInner::Guest(format!( + "Invalid effort_quantity: {e}" + ))) + })?; + } + // Hard action constraints are enforced by integrity validation. // Soft warnings: use evaluate_state_transition / check_action_constraints (parallel path). let commitment = Commitment { action: input.action, provider: input.provider, - receiver: agent_info.agent_initial_pubkey, + receiver: input.receiver, resource_inventoried_as: input.resource_hash, resource_conforms_to: input.resource_spec_hash, input_of: None, // TODO: Add process support in Phase 4 + resource_quantity: input.resource_quantity, + effort_quantity: input.effort_quantity, due_date: input.due_date, note: input.note, committed_at: now, diff --git a/dnas/nondominium/zomes/coordinator/zome_gouvernance/src/economic_event.rs b/dnas/nondominium/zomes/coordinator/zome_gouvernance/src/economic_event.rs index a1059e0d..699b7667 100644 --- a/dnas/nondominium/zomes/coordinator/zome_gouvernance/src/economic_event.rs +++ b/dnas/nondominium/zomes/coordinator/zome_gouvernance/src/economic_event.rs @@ -4,6 +4,7 @@ use nondominium_shared::constraints::{ check_action_permitted, ConstraintViolation, ResourceClassification, }; use nondominium_shared::io::governance::CheckActionConstraintsInput; +use nondominium_shared::types::Measure; use zome_gouvernance_integrity::*; // ============================================================================ @@ -16,7 +17,8 @@ pub struct LogEconomicEventInput { pub provider: AgentPubKey, pub receiver: AgentPubKey, pub resource_inventoried_as: ActionHash, - pub resource_quantity: f64, + /// ValueFlows `resourceQuantity` on the observed flow. + pub resource_quantity: Measure, pub note: Option, pub commitment_hash: Option, // Optional link to commitment being fulfilled pub generate_pprs: Option, // Whether to auto-generate PPR claims @@ -35,6 +37,15 @@ pub struct LogEconomicEventOutput { pub fn log_economic_event(input: LogEconomicEventInput) -> ExternResult { let now = sys_time()?; + // Shape-check the Measure before integrity sees it so callers get a clearer + // error on malformed input (integrity will also reject it, but with a less + // specific message). + input.resource_quantity.validate().map_err(|e| { + wasm_error!(WasmErrorInner::Guest(format!( + "Invalid resource_quantity: {e}" + ))) + })?; + // Hard action constraints are enforced by integrity validation. // Soft warnings are available via evaluate_state_transition / check_action_constraints // (parallel path — see complete-resource-specification.md §5 item 2). @@ -106,7 +117,7 @@ pub fn log_economic_event(input: LogEconomicEventInput) -> ExternResult, } @@ -83,15 +86,16 @@ pub fn create_economic_resource( ) -> ExternResult { let agent_info = agent_info()?; - // Validate input - if input.quantity <= 0.0 { + // Validate input (shape + positivity). Measure::validate() checks the pair; + // the > 0 check captures the creation-time positive-balance requirement. + input + .quantity + .validate() + .map_err(|e| ResourceError::InvalidInput(format!("Invalid quantity: {e}")))?; + if input.quantity.numerical_value <= 0.0 { return Err(ResourceError::InvalidInput("Quantity must be positive".to_string()).into()); } - if input.unit.trim().is_empty() { - return Err(ResourceError::InvalidInput("Unit cannot be empty".to_string()).into()); - } - // Validate that the specification exists let _spec_record = get(input.spec_hash.clone(), GetOptions::default())?.ok_or( ResourceError::ResourceSpecNotFound("ResourceSpecification not found".to_string()), @@ -99,7 +103,6 @@ pub fn create_economic_resource( let resource = EconomicResource { quantity: input.quantity, - unit: input.unit, custodian: agent_info.agent_initial_pubkey.clone(), current_location: input.current_location, operational_state: OperationalState::PendingValidation, @@ -242,18 +245,17 @@ pub fn update_economic_resource(input: UpdateEconomicResourceInput) -> ExternRes return Err(ResourceError::NotCustodian.into()); } - // Validate input - if input.updated_resource.quantity <= 0.0 { - return Err(ResourceError::InvalidInput("Quantity must be positive".to_string()).into()); - } - - if input.updated_resource.unit.trim().is_empty() { - return Err(ResourceError::InvalidInput("Unit cannot be empty".to_string()).into()); - } + // Validate input. Updates may legitimately drop quantity to 0 (post-depletion + // bookkeeping), so we validate shape only here; the create-time > 0 check does + // not apply to updates. + input + .updated_resource + .quantity + .validate() + .map_err(|e| ResourceError::InvalidInput(format!("Invalid quantity: {e}")))?; let updated_resource = EconomicResource { quantity: input.updated_resource.quantity, - unit: input.updated_resource.unit, custodian: original_resource.custodian, // Keep the same custodian current_location: input.updated_resource.current_location, operational_state: original_resource.operational_state, diff --git a/dnas/nondominium/zomes/integrity/zome_gouvernance/src/lib.rs b/dnas/nondominium/zomes/integrity/zome_gouvernance/src/lib.rs index 0836ef2d..3944c38f 100644 --- a/dnas/nondominium/zomes/integrity/zome_gouvernance/src/lib.rs +++ b/dnas/nondominium/zomes/integrity/zome_gouvernance/src/lib.rs @@ -5,7 +5,7 @@ use hdi::prelude::*; // nondominium_shared::types so coordinator zomes and Sweettest test crates can import // them directly without WASM-crate dependency constraints. pub use nondominium_shared::types::{ - BeneficiaryRef, BenefitClause, BenefitType, LifecycleStage, NdoLinkType, PropertyRegime, + BeneficiaryRef, BenefitClause, BenefitType, LifecycleStage, Measure, NdoLinkType, PropertyRegime, ResourceNature, Rivalry, VfAction, }; @@ -31,7 +31,10 @@ pub struct EconomicEvent { pub receiver: AgentPubKey, pub resource_inventoried_as: ActionHash, // Link to the EconomicResource pub affects: ActionHash, // Link to the EconomicResource that is affected - pub resource_quantity: f64, + /// ValueFlows `resourceQuantity` as a shared `Measure`. Replaces the previous + /// flattened `resource_quantity: f64` so Intent / Commitment / Event / Claim + /// all carry the same `{ numerical_value, has_unit }` shape. + pub resource_quantity: Measure, pub event_time: Timestamp, pub note: Option, /// Direct pointer to Layer 0 for action-constraint evaluation. @@ -47,6 +50,14 @@ pub struct Commitment { pub resource_inventoried_as: Option, // Link to specific resource if applicable pub resource_conforms_to: Option, // Link to ResourceSpecification if general pub input_of: Option, // Optional link to a Process + /// ValueFlows `resourceQuantity` — how much of the resource the commitment + /// promises to flow. Optional for commitments whose quantity is implied by + /// context (e.g. "repair this machine"), present when the flow is countable. + pub resource_quantity: Option, + /// ValueFlows `effortQuantity` — the labour or time component of the + /// commitment (e.g. 4 h of mechanic time). Optional and independent of + /// `resource_quantity`. + pub effort_quantity: Option, pub due_date: Timestamp, pub note: Option, pub committed_at: Timestamp, @@ -480,6 +491,11 @@ fn validate_create_economic_event( event: &EconomicEvent, _author: &AgentPubKey, ) -> ExternResult { + if let Err(e) = event.resource_quantity.validate() { + return Ok(ValidateCallbackResult::Invalid(format!( + "EconomicEvent resource_quantity invalid: {e}" + ))); + } validate_action_against_ndo(&event.ndo_identity_hash, &event.action) } @@ -487,5 +503,19 @@ fn validate_create_commitment( commitment: &Commitment, _author: &AgentPubKey, ) -> ExternResult { + if let Some(ref m) = commitment.resource_quantity { + if let Err(e) = m.validate() { + return Ok(ValidateCallbackResult::Invalid(format!( + "Commitment resource_quantity invalid: {e}" + ))); + } + } + if let Some(ref m) = commitment.effort_quantity { + if let Err(e) = m.validate() { + return Ok(ValidateCallbackResult::Invalid(format!( + "Commitment effort_quantity invalid: {e}" + ))); + } + } validate_action_against_ndo(&commitment.ndo_identity_hash, &commitment.action) } diff --git a/dnas/nondominium/zomes/integrity/zome_resource/src/lib.rs b/dnas/nondominium/zomes/integrity/zome_resource/src/lib.rs index b1e42d64..fde94299 100644 --- a/dnas/nondominium/zomes/integrity/zome_resource/src/lib.rs +++ b/dnas/nondominium/zomes/integrity/zome_resource/src/lib.rs @@ -1,7 +1,7 @@ use hdi::prelude::*; pub use nondominium_shared::types::{ - LifecycleStage, NdoDnaProperties, OperationalState, PropertyRegime, ResourceNature, ResourceScope, - Rivalry, + LifecycleStage, Measure, NdoDnaProperties, OperationalState, PropertyRegime, ResourceNature, + ResourceScope, Rivalry, }; pub use nondominium_shared::rule_data::RuleData; @@ -54,8 +54,11 @@ pub struct GovernanceRule { #[hdk_entry_helper] #[derive(Clone, PartialEq)] pub struct EconomicResource { - pub quantity: f64, - pub unit: String, + /// Current inventoried quantity. ValueFlows models this as a `Measure` + /// (numericalValue + hasUnit); keeping it as its own type lets Commitments, + /// Events, and Resources share the same shape and keeps the wire format + /// aligned with hREA / Grassroots-side tooling. + pub quantity: Measure, pub custodian: AgentPubKey, // The Primary Accountable Agent holding the resource pub current_location: Option, // Physical or virtual location TODO: use an enum pub operational_state: OperationalState, @@ -478,15 +481,17 @@ fn validate_create_economic_resource( resource: &EconomicResource, _author: &AgentPubKey, ) -> ExternResult { - if resource.quantity <= 0.0 { - return Ok(ValidateCallbackResult::Invalid( - "Resource quantity must be positive".to_string(), - )); + if let Err(e) = resource.quantity.validate() { + return Ok(ValidateCallbackResult::Invalid(format!( + "EconomicResource quantity invalid: {e}" + ))); } - - if resource.unit.trim().is_empty() { + // Creation requires a strictly positive quantity (zero is reserved for + // post-depletion observations captured through EconomicEvents, not for + // the initial inventoried state). + if resource.quantity.numerical_value <= 0.0 { return Ok(ValidateCallbackResult::Invalid( - "Resource unit cannot be empty".to_string(), + "Resource quantity must be positive at creation".to_string(), )); } From 2163d653eeec6119fcecc16658324d4d81e7579f Mon Sep 17 00:00:00 2001 From: Tiberius Brastaviceanu Date: Thu, 29 Oct 2026 16:31:14 -0400 Subject: [PATCH 02/14] feat(ui): migrate UI to Measure and explicit commitment receiver Mirrors the Rust-side Measure migration across: - packages/shared-types: adds Measure interface, migrates EconomicResource.{quantity,unit} -> Measure, VfEconomicEvent / VfCommitment / LogEconomicEventInput / ProposeCommitmentInput to Measure, adds explicit receiver + resource_quantity + effort_quantity on ProposeCommitmentInput - ui/schemas: adds MeasureSchema, swaps number+string pair for Measure in EconomicResourceInput, UIEconomicResource, UIEconomicEvent, CommitmentInput, UICommitment - UI components: * EconomicEventCreateForm now takes quantity + unit (defaults 'each') and submits a Measure * CommitmentCreateForm now takes explicit receiver (defaults to self) and optional resource/effort quantity+unit pairs * ResourcesTab + ActivityTab render Measure.{numerical_value,has_unit} - holochain-records: isEconomicResource guards against the Measure shape - resource.store: updateResourceQuantity patches numerical_value only (unit unchanged); documents that the backend fn is still not wired - ndo.store: event dedup checks both numerical_value and has_unit svelte-check: no new regressions. Error count 228 -> 224 (noise). Co-authored-by: Cursor (cherry picked from commit 0fc0ab30fb0b121513f7448cdb5db45f1d6f252f) --- packages/shared-types/src/governance.types.ts | 29 +++++- packages/shared-types/src/resource.types.ts | 19 +++- ui/src/lib/components/ndo/ActivityTab.svelte | 3 +- .../ndo/CommitmentCreateForm.svelte | 99 +++++++++++++++++-- .../ndo/EconomicEventCreateForm.svelte | 21 +++- ui/src/lib/components/ndo/ResourcesTab.svelte | 5 +- ui/src/lib/schemas/governance.schemas.ts | 7 +- ui/src/lib/schemas/resource.schemas.ts | 15 ++- ui/src/lib/stores/ndo.store.svelte.ts | 3 +- ui/src/lib/stores/resource.store.svelte.ts | 8 +- ui/src/lib/utils/holochain-records.ts | 6 +- 11 files changed, 180 insertions(+), 35 deletions(-) diff --git a/packages/shared-types/src/governance.types.ts b/packages/shared-types/src/governance.types.ts index ac7ed898..7f6cca39 100644 --- a/packages/shared-types/src/governance.types.ts +++ b/packages/shared-types/src/governance.types.ts @@ -1,19 +1,25 @@ import type { ActionHash, AgentPubKey, EntryHash, Timestamp } from '@holochain/client'; import type { ConstraintViolation, + Measure, PropertyRegime, ResourceNature, Rivalry } from './resource.types.js'; -export type { ConstraintViolation }; +export type { ConstraintViolation, Measure }; // Core Governance Types +// +// Note: the `Commitment` / `EconomicEvent` interfaces below are the pre-NDO +// legacy shapes kept for backward compatibility with a handful of UI views. +// The canonical zome entries are `VfCommitment` / `VfEconomicEvent` further +// down this file — new code should prefer those. export interface Commitment { resource_specification: EntryHash; provider: AgentPubKey; receiver: AgentPubKey; - quantity?: number; + quantity?: Measure; due_date?: Timestamp; fulfilled: boolean; created_at: Timestamp; @@ -23,7 +29,7 @@ export interface EconomicEvent { resource_specification: EntryHash; provider: AgentPubKey; receiver: AgentPubKey; - quantity?: number; + quantity?: Measure; event_type: 'transfer' | 'produce' | 'consume' | 'use'; occurred_at: Timestamp; } @@ -54,7 +60,8 @@ export interface VfEconomicEvent { receiver: AgentPubKey; resource_inventoried_as: ActionHash; affects: ActionHash; - resource_quantity: number; + /** ValueFlows `resourceQuantity`. */ + resource_quantity: Measure; event_time: Timestamp; note?: string | null; ndo_identity_hash: ActionHash; @@ -66,6 +73,16 @@ export interface ProposeCommitmentInput { resource_hash?: ActionHash | null; resource_spec_hash?: ActionHash | null; provider: AgentPubKey; + /** + * Explicit receiver. Previously the zome hardcoded the caller as receiver; + * the UI now has to pass it. For "I propose this to myself" flows pass the + * caller's own key. + */ + receiver: AgentPubKey; + /** ValueFlows `resourceQuantity` on the commitment (optional). */ + resource_quantity?: Measure | null; + /** ValueFlows `effortQuantity` on the commitment (optional). */ + effort_quantity?: Measure | null; due_date: Timestamp; note?: string | null; ndo_identity_hash: ActionHash; @@ -84,6 +101,8 @@ export interface VfCommitment { resource_inventoried_as?: ActionHash | null; resource_conforms_to?: ActionHash | null; input_of?: ActionHash | null; + resource_quantity?: Measure | null; + effort_quantity?: Measure | null; due_date: Timestamp; note?: string | null; committed_at: Timestamp; @@ -96,7 +115,7 @@ export interface LogEconomicEventInput { provider: AgentPubKey; receiver: AgentPubKey; resource_inventoried_as: ActionHash; - resource_quantity: number; + resource_quantity: Measure; note?: string | null; commitment_hash?: ActionHash | null; generate_pprs?: boolean | null; diff --git a/packages/shared-types/src/resource.types.ts b/packages/shared-types/src/resource.types.ts index a521e672..f276e8a0 100644 --- a/packages/shared-types/src/resource.types.ts +++ b/packages/shared-types/src/resource.types.ts @@ -1,5 +1,18 @@ import type { ActionHash, AgentPubKey, DnaHash, EntryHash, Record, Timestamp } from '@holochain/client'; +/** + * ValueFlows `Measure` — a numerical value with a unit label. Mirrors + * `crates/shared/src/types.rs::Measure` 1:1. Used anywhere ValueFlows carries + * a quantity: `resourceQuantity`, `effortQuantity`, `availableQuantity`. + * + * `has_unit` is a free-form string in the MVP; the VF spec recommends om2 + * unit URIs (e.g. `om2:kilogram`), but typed units are deferred. + */ +export interface Measure { + numerical_value: number; + has_unit: string; +} + // Resource operational state types (Layer 2 instance condition) export type OperationalState = | "Available" @@ -25,8 +38,7 @@ export interface ResourceSpecification { } export interface EconomicResource { - quantity: number; - unit: string; + quantity: Measure; custodian: AgentPubKey; current_location?: string; operational_state: OperationalState; @@ -79,8 +91,7 @@ export interface NestedGovernanceRuleInput { export interface EconomicResourceInput { spec_hash: ActionHash; - quantity: number; - unit: string; + quantity: Measure; current_location?: string; } diff --git a/ui/src/lib/components/ndo/ActivityTab.svelte b/ui/src/lib/components/ndo/ActivityTab.svelte index 2ac71181..99489078 100644 --- a/ui/src/lib/components/ndo/ActivityTab.svelte +++ b/ui/src/lib/components/ndo/ActivityTab.svelte @@ -172,7 +172,8 @@
  • {ev.action}
    - Qty {ev.resource_quantity} · {new Date(Number(ev.event_time) / 1000).toLocaleString()} + Qty {ev.resource_quantity.numerical_value} + {ev.resource_quantity.has_unit} · {new Date(Number(ev.event_time) / 1000).toLocaleString()}
    {#if ev.note}
    {ev.note}
    diff --git a/ui/src/lib/components/ndo/CommitmentCreateForm.svelte b/ui/src/lib/components/ndo/CommitmentCreateForm.svelte index d582159a..e1c8db9a 100644 --- a/ui/src/lib/components/ndo/CommitmentCreateForm.svelte +++ b/ui/src/lib/components/ndo/CommitmentCreateForm.svelte @@ -53,8 +53,17 @@ let action = $state('Use'); let providerB64 = $state(''); + // Explicit receiver (was previously hardcoded on the backend to the caller; + // the zome now requires an explicit field). Defaults to self for the common + // "I propose this to myself" flow. + let receiverB64 = $state(''); let dueDateLocal = $state(''); let note = $state(''); + // Optional ValueFlows Measures. Empty = omit from the commitment. + let resourceQuantity = $state(''); + let resourceUnit = $state('each'); + let effortQuantity = $state(''); + let effortUnit = $state('hour'); let isSubmitting = $state(false); let errorMessage = $state(''); let violations = $state([]); @@ -64,13 +73,13 @@ const softViolations = $derived(violations.filter((v) => v.severity === 'Soft')); async function ensureProvider() { - if (!providerB64) { - try { - const me = await connectionStore.getMyAgentPubKey(); - providerB64 = encodeHashToBase64(me); - } catch { - /* leave empty */ - } + try { + const me = await connectionStore.getMyAgentPubKey(); + const b64 = encodeHashToBase64(me); + if (!providerB64) providerB64 = b64; + if (!receiverB64) receiverB64 = b64; + } catch { + /* leave empty */ } } @@ -103,24 +112,41 @@ errorMessage = 'Resolve Hard action constraints before submitting.'; return; } - if (!providerB64 || !dueDateLocal) { - errorMessage = 'Provider and due date are required.'; + if (!providerB64 || !receiverB64 || !dueDateLocal) { + errorMessage = 'Provider, receiver, and due date are required.'; return; } let provider: AgentPubKey; + let receiver: AgentPubKey; try { provider = decodeHashFromBase64(providerB64) as AgentPubKey; + receiver = decodeHashFromBase64(receiverB64) as AgentPubKey; } catch { - errorMessage = 'Invalid provider pubkey (base64).'; + errorMessage = 'Invalid base64 pubkey for provider or receiver.'; return; } isSubmitting = true; errorMessage = ''; const dueMs = new Date(dueDateLocal).getTime(); + const resQty = resourceQuantity.trim() + ? { + numerical_value: Number(resourceQuantity), + has_unit: resourceUnit.trim() || 'each' + } + : null; + const effQty = effortQuantity.trim() + ? { + numerical_value: Number(effortQuantity), + has_unit: effortUnit.trim() || 'hour' + } + : null; const out = await governanceStore.proposeCommitment( { action, provider, + receiver, + resource_quantity: resQty, + effort_quantity: effQty, due_date: dueMs * 1000, note: note.trim() || null, ndo_identity_hash: ndoActionHash @@ -177,6 +203,17 @@ class="w-full rounded border border-gray-300 px-3 py-2 font-mono text-xs" /> +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + +
    +
    + + +
    +