diff --git a/.github/workflows/backlog-sync.yml b/.github/workflows/backlog-sync.yml new file mode 100644 index 00000000..9d4012f6 --- /dev/null +++ b/.github/workflows/backlog-sync.yml @@ -0,0 +1,35 @@ +name: Backlog Sync + +on: + pull_request: + types: + - opened + - reopened + - synchronize + - ready_for_review + - closed + +permissions: + contents: read + issues: write + pull-requests: read + projects: write + +jobs: + sync-pr-state: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Sync backlog state from PR event + env: + GH_TOKEN: ${{ github.token }} + TRAVERSE_REPO: ${{ github.repository }} + PROJECT_OWNER: enricopiovesan + PROJECT_NUMBER: "1" + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_EVENT_ACTION: ${{ github.event.action }} + PR_MERGED: ${{ github.event.pull_request.merged }} + run: bash scripts/ci/sync_backlog_on_merge.sh diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..b0a3480b --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,52 @@ +name: CodeQL + +on: + push: + branches: + - main + pull_request: + schedule: + - cron: "23 6 * * 1" + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: javascript-typescript + build-mode: none + - language: rust + build-mode: autobuild + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + + - name: Set up Rust + if: matrix.language == 'rust' + uses: dtolnay/rust-toolchain@stable + with: + toolchain: 1.94.0 + + - name: Autobuild + if: matrix.build-mode == 'autobuild' + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.gitignore b/.gitignore index 386f9e1e..d8842a88 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,10 @@ !.agents/skills/** target/ .DS_Store +references/open-source/* +!references/open-source/.gitignore +!references/open-source/README.md +.claude/worktrees/ *.profraw *.profdata coverage.profdata diff --git a/crates/traverse-registry/src/federation.rs b/crates/traverse-registry/src/federation.rs new file mode 100644 index 00000000..21832acc --- /dev/null +++ b/crates/traverse-registry/src/federation.rs @@ -0,0 +1,2163 @@ +#![allow( + clippy::expect_used, + clippy::too_many_lines, + clippy::too_many_arguments, + clippy::uninlined_format_args, + clippy::must_use_candidate, + clippy::needless_pass_by_value, + clippy::missing_errors_doc, + clippy::missing_panics_doc, + clippy::map_unwrap_or +)] + +use crate::{ + CapabilityRegistry, EventRegistry, LookupScope, RegistryScope, ResolvedCapability, + ResolvedEvent, ResolvedWorkflow, WorkflowRegistry, +}; +use std::collections::{BTreeMap, BTreeSet}; +use traverse_contracts::{ErrorSeverity, Lifecycle}; +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum FederationRegistryKind { + Capability, + Event, + Workflow, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FederationApprovalState { + Approved, + Draft, + Deprecated, + Rejected, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FederationTrustState { + Trusted, + Pending, + Blocked, + Revoked, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FederationSyncStatus { + Unknown, + Success, + Partial, + Failed, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FederationInvocationStatus { + Success, + Failure, + RetryableFailure, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FederationConflictResolutionState { + Open, + Resolved, + Escalated, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FederationPeer { + pub peer_id: String, + pub display_name: String, + pub trust_state: FederationTrustState, + pub identity_fingerprint: String, + pub sync_enabled: bool, + pub last_sync_at: Option, + pub last_sync_status: FederationSyncStatus, + pub visible_registry_scopes: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TrustRecord { + pub peer_id: String, + pub trust_model: String, + pub allowed_scopes: Vec, + pub approved_spec_refs: Vec, + pub approved_at: String, + pub revoked_at: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FederationPeerExport { + pub peer: FederationPeer, + pub trust: TrustRecord, + pub capabilities: Vec, + pub events: Vec, + pub workflows: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FederationSyncSession { + pub session_id: String, + pub peer_id: String, + pub started_at: String, + pub finished_at: Option, + pub status: FederationSyncStatus, + pub registry_types: Vec, + pub validated_entries: usize, + pub rejected_entries: usize, + pub conflict_count: usize, + pub evidence_ref: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PeerRegistrySnapshot { + pub peer_id: String, + pub registry_type: FederationRegistryKind, + pub entry_id: String, + pub version: String, + pub scope: RegistryScope, + pub approval_state: FederationApprovalState, + pub contract_ref: String, + pub provenance_ref: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CrossPeerTraceProvenance { + pub trace_id: String, + pub origin_peer_id: String, + pub owning_peer_id: String, + pub route_reason: String, + pub sync_session_ref: Option, + pub response_status: FederationInvocationStatus, + pub evidence_ref: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FederatedInvocation { + pub invocation_id: String, + pub origin_peer_id: String, + pub target_peer_id: String, + pub capability_id: String, + pub request_ref: String, + pub status: FederationInvocationStatus, + pub response_ref: Option, + pub trace_provenance: CrossPeerTraceProvenance, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ConflictRecord { + pub conflict_id: String, + pub peer_ids: Vec, + pub registry_type: FederationRegistryKind, + pub entry_key: String, + pub conflict_reason: String, + pub resolution_state: FederationConflictResolutionState, + pub audit_ref: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FederationStatusSummary { + pub peer_count: usize, + pub trusted_peer_count: usize, + pub last_sync_outcome: FederationSyncStatus, + pub sync_age: Option, + pub conflict_count: usize, + pub blocked_entries: usize, + pub route_failures: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FederationSyncOutcome { + pub session: FederationSyncSession, + pub accepted_snapshots: Vec, + pub conflicts: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FederationErrorCode { + MissingRequiredField, + DuplicatePeer, + InvalidTrust, + PeerNotFound, + EntryValidationFailed, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FederationError { + pub code: FederationErrorCode, + pub target: String, + pub message: String, + pub severity: ErrorSeverity, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FederationFailure { + pub errors: Vec, +} + +#[derive(Debug, Default)] +pub struct FederationRegistry { + peers: BTreeMap, + trust_records: BTreeMap, + snapshots: BTreeMap<(String, FederationRegistryKind, String, String), PeerRegistrySnapshot>, + sync_sessions: Vec, + invocations: Vec, + conflicts: Vec, +} + +impl FederationRegistry { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + pub fn register_peer( + &mut self, + peer: FederationPeer, + trust: TrustRecord, + ) -> Result<(), FederationFailure> { + let mut errors = Vec::new(); + if peer.peer_id.trim().is_empty() { + errors.push(federation_error( + FederationErrorCode::MissingRequiredField, + "$.peer.peer_id", + "peer_id must not be empty", + )); + } + if peer.display_name.trim().is_empty() { + errors.push(federation_error( + FederationErrorCode::MissingRequiredField, + "$.peer.display_name", + "display_name must not be empty", + )); + } + if peer.identity_fingerprint.trim().is_empty() { + errors.push(federation_error( + FederationErrorCode::MissingRequiredField, + "$.peer.identity_fingerprint", + "identity_fingerprint must not be empty", + )); + } + if peer.peer_id != trust.peer_id { + errors.push(federation_error( + FederationErrorCode::InvalidTrust, + "$.trust.peer_id", + "trust record must reference the same peer_id as the peer", + )); + } + if !peer.sync_enabled { + errors.push(federation_error( + FederationErrorCode::InvalidTrust, + "$.peer.sync_enabled", + "sync_enabled must be true for a trusted federation peer", + )); + } + if !matches!(peer.trust_state, FederationTrustState::Trusted) { + errors.push(federation_error( + FederationErrorCode::InvalidTrust, + "$.peer.trust_state", + "peer trust_state must be trusted before federation registration", + )); + } + if trust.allowed_scopes.is_empty() { + errors.push(federation_error( + FederationErrorCode::InvalidTrust, + "$.trust.allowed_scopes", + "allowed_scopes must not be empty", + )); + } + if trust.approved_spec_refs.is_empty() { + errors.push(federation_error( + FederationErrorCode::InvalidTrust, + "$.trust.approved_spec_refs", + "approved_spec_refs must not be empty", + )); + } + if !errors.is_empty() { + return Err(FederationFailure { errors }); + } + + match self.peers.get(&peer.peer_id) { + Some(existing) + if existing == &peer && self.trust_records.get(&peer.peer_id) == Some(&trust) => + { + Ok(()) + } + Some(_) => Err(FederationFailure { + errors: vec![federation_error( + FederationErrorCode::DuplicatePeer, + "$.peer.peer_id", + "a different federation peer is already registered with this peer_id", + )], + }), + None => { + self.trust_records.insert(peer.peer_id.clone(), trust); + self.peers.insert(peer.peer_id.clone(), peer); + Ok(()) + } + } + } + + #[must_use] + pub fn list_peers(&self) -> Vec { + let mut peers = self.peers.values().cloned().collect::>(); + peers.sort_by(|left, right| left.peer_id.cmp(&right.peer_id)); + peers + } + + #[must_use] + pub fn conflicts(&self) -> &[ConflictRecord] { + &self.conflicts + } + + #[must_use] + pub fn sync_sessions(&self) -> &[FederationSyncSession] { + &self.sync_sessions + } + + #[must_use] + pub fn invocations(&self) -> &[FederatedInvocation] { + &self.invocations + } + + #[must_use] + pub fn status_summary(&self) -> FederationStatusSummary { + let trusted_peer_count = self + .peers + .values() + .filter(|peer| peer.trust_state == FederationTrustState::Trusted) + .count(); + let last_session = self.sync_sessions.last(); + FederationStatusSummary { + peer_count: self.peers.len(), + trusted_peer_count, + last_sync_outcome: last_session + .map(|session| session.status) + .unwrap_or(FederationSyncStatus::Unknown), + sync_age: last_session.and_then(|session| session.finished_at.clone()), + conflict_count: self.conflicts.len(), + blocked_entries: self + .sync_sessions + .iter() + .map(|session| session.rejected_entries) + .sum(), + route_failures: self + .invocations + .iter() + .filter(|invocation| is_route_failure(invocation.status)) + .count(), + } + } + + pub fn sync_peer( + &mut self, + export: FederationPeerExport, + capabilities: &CapabilityRegistry, + events: &EventRegistry, + workflows: &WorkflowRegistry, + started_at: &str, + finished_at: &str, + evidence_ref: &str, + ) -> Result { + let mut errors = Vec::new(); + if started_at.trim().is_empty() { + errors.push(federation_error( + FederationErrorCode::MissingRequiredField, + "$.started_at", + "started_at must not be empty", + )); + } + if finished_at.trim().is_empty() { + errors.push(federation_error( + FederationErrorCode::MissingRequiredField, + "$.finished_at", + "finished_at must not be empty", + )); + } + if evidence_ref.trim().is_empty() { + errors.push(federation_error( + FederationErrorCode::MissingRequiredField, + "$.evidence_ref", + "evidence_ref must not be empty", + )); + } + if export.peer.peer_id != export.trust.peer_id { + errors.push(federation_error( + FederationErrorCode::InvalidTrust, + "$.trust.peer_id", + "export trust record must match the exporting peer id", + )); + } + + let Some(registered_peer) = self.peers.get(&export.peer.peer_id) else { + errors.push(federation_error( + FederationErrorCode::PeerNotFound, + "$.peer.peer_id", + "peer must be registered before it can be synced", + )); + return Err(FederationFailure { errors }); + }; + let Some(registered_trust) = self.trust_records.get(&export.peer.peer_id) else { + errors.push(federation_error( + FederationErrorCode::InvalidTrust, + "$.trust.peer_id", + "peer is missing its approved trust record", + )); + return Err(FederationFailure { errors }); + }; + + if registered_peer != &export.peer || registered_trust != &export.trust { + errors.push(federation_error( + FederationErrorCode::InvalidTrust, + "$.peer", + "exported peer metadata must match the registered trusted peer", + )); + } + if !registered_peer.sync_enabled { + errors.push(federation_error( + FederationErrorCode::InvalidTrust, + "$.peer.sync_enabled", + "sync is disabled for this peer", + )); + } + if registered_peer.trust_state != FederationTrustState::Trusted { + errors.push(federation_error( + FederationErrorCode::InvalidTrust, + "$.peer.trust_state", + "only trusted peers can participate in federation sync", + )); + } + if !errors.is_empty() { + return Err(FederationFailure { errors }); + } + + let mut accepted_snapshots = Vec::new(); + let mut conflict_records = Vec::new(); + + for capability in &export.capabilities { + if let Some(snapshot) = validate_capability_snapshot( + &export.peer, + &export.trust, + capabilities, + capability, + evidence_ref, + &mut conflict_records, + ) { + accepted_snapshots.push(snapshot); + } + } + for event in &export.events { + if let Some(snapshot) = validate_event_snapshot( + &export.peer, + &export.trust, + events, + event, + evidence_ref, + &mut conflict_records, + ) { + accepted_snapshots.push(snapshot); + } + } + for workflow in &export.workflows { + if let Some(snapshot) = validate_workflow_snapshot( + &export.peer, + &export.trust, + workflows, + workflow, + evidence_ref, + &mut conflict_records, + ) { + accepted_snapshots.push(snapshot); + } + } + + for snapshot in &accepted_snapshots { + let key = ( + snapshot.peer_id.clone(), + snapshot.registry_type, + snapshot.entry_id.clone(), + snapshot.version.clone(), + ); + self.snapshots.insert(key, snapshot.clone()); + } + self.conflicts.extend(conflict_records.clone()); + + let status = if accepted_snapshots.is_empty() && conflict_records.is_empty() { + FederationSyncStatus::Failed + } else if conflict_records.is_empty() { + FederationSyncStatus::Success + } else { + FederationSyncStatus::Partial + }; + + let session = FederationSyncSession { + session_id: format!( + "sync_{}_{}", + export.peer.peer_id, + self.sync_sessions.len() + 1 + ), + peer_id: export.peer.peer_id.clone(), + started_at: started_at.to_string(), + finished_at: Some(finished_at.to_string()), + status, + registry_types: synced_registry_types(&accepted_snapshots), + validated_entries: accepted_snapshots.len(), + rejected_entries: conflict_records.len(), + conflict_count: conflict_records.len(), + evidence_ref: evidence_ref.to_string(), + }; + + if let Some(peer) = self.peers.get_mut(&export.peer.peer_id) { + peer.last_sync_at = Some(finished_at.to_string()); + peer.last_sync_status = status; + } + + self.sync_sessions.push(session.clone()); + + Ok(FederationSyncOutcome { + session, + accepted_snapshots, + conflicts: conflict_records, + }) + } + + pub fn route_capability_invocation( + &mut self, + origin_peer_id: &str, + capability_id: &str, + version: &str, + request_ref: &str, + available_peer_ids: &BTreeSet, + routed_at: &str, + evidence_ref: &str, + ) -> Result { + let mut errors = Vec::new(); + if origin_peer_id.trim().is_empty() { + errors.push(federation_error( + FederationErrorCode::MissingRequiredField, + "$.origin_peer_id", + "origin_peer_id must not be empty", + )); + } + if capability_id.trim().is_empty() { + errors.push(federation_error( + FederationErrorCode::MissingRequiredField, + "$.capability_id", + "capability_id must not be empty", + )); + } + if version.trim().is_empty() { + errors.push(federation_error( + FederationErrorCode::MissingRequiredField, + "$.version", + "version must not be empty", + )); + } + if request_ref.trim().is_empty() { + errors.push(federation_error( + FederationErrorCode::MissingRequiredField, + "$.request_ref", + "request_ref must not be empty", + )); + } + if routed_at.trim().is_empty() { + errors.push(federation_error( + FederationErrorCode::MissingRequiredField, + "$.routed_at", + "routed_at must not be empty", + )); + } + if evidence_ref.trim().is_empty() { + errors.push(federation_error( + FederationErrorCode::MissingRequiredField, + "$.evidence_ref", + "evidence_ref must not be empty", + )); + } + if !self.peers.contains_key(origin_peer_id) { + errors.push(federation_error( + FederationErrorCode::PeerNotFound, + "$.origin_peer_id", + "origin peer must be registered before routing", + )); + } + if !errors.is_empty() { + return Err(FederationFailure { errors }); + } + + let origin_peer = self.peers.get(origin_peer_id).expect("validated above"); + let trust = self + .trust_records + .get(origin_peer_id) + .expect("validated above"); + + let candidate = self + .snapshots + .values() + .filter(|snapshot| snapshot.registry_type == FederationRegistryKind::Capability) + .filter(|snapshot| snapshot.entry_id == capability_id && snapshot.version == version) + .filter(|snapshot| scope_is_visible(snapshot.scope, trust, origin_peer)) + .min_by(|left, right| left.peer_id.cmp(&right.peer_id)) + .cloned() + .map(|snapshot| (snapshot.peer_id.clone(), snapshot)); + + let Some((target_peer_id, target_snapshot)) = candidate else { + return Err(FederationFailure { + errors: vec![federation_error( + FederationErrorCode::EntryValidationFailed, + "$.capability_id", + "no synchronized owning peer was found for the requested capability", + )], + }); + }; + + let available = available_peer_ids.contains(&target_peer_id); + let sync_session_ref = self + .sync_sessions + .iter() + .rev() + .find(|session| session.peer_id == target_peer_id) + .map(|session| session.evidence_ref.clone()); + let trace_id = format!("trace_{}_{}_{}", origin_peer_id, capability_id, version); + let invocation_id = format!( + "invocation_{}_{}_{}", + origin_peer_id, capability_id, version + ); + let (status, response_ref, route_reason) = if available { + ( + FederationInvocationStatus::Success, + Some(format!( + "response://{}/{}/{}", + target_peer_id, capability_id, version + )), + format!( + "routed to owning peer {} for synchronized capability snapshot", + target_peer_id + ), + ) + } else { + ( + FederationInvocationStatus::RetryableFailure, + None, + format!( + "owning peer {} is not currently reachable for invocation", + target_peer_id + ), + ) + }; + + let invocation = FederatedInvocation { + invocation_id, + origin_peer_id: origin_peer_id.to_string(), + target_peer_id: target_peer_id.clone(), + capability_id: capability_id.to_string(), + request_ref: request_ref.to_string(), + status, + response_ref, + trace_provenance: CrossPeerTraceProvenance { + trace_id, + origin_peer_id: origin_peer_id.to_string(), + owning_peer_id: target_snapshot.peer_id, + route_reason, + sync_session_ref, + response_status: status, + evidence_ref: evidence_ref.to_string(), + }, + }; + self.invocations.push(invocation.clone()); + Ok(invocation) + } +} + +pub fn export_peer_state( + peer: FederationPeer, + trust: TrustRecord, + capabilities: &CapabilityRegistry, + events: &EventRegistry, + workflows: &WorkflowRegistry, +) -> FederationPeerExport { + FederationPeerExport { + peer, + trust, + capabilities: capabilities.graph_entries(), + events: events.graph_entries(), + workflows: workflows.graph_entries(), + } +} + +fn validate_capability_snapshot( + peer: &FederationPeer, + trust: &TrustRecord, + capabilities: &CapabilityRegistry, + export: &ResolvedCapability, + evidence_ref: &str, + conflicts: &mut Vec, +) -> Option { + if !scope_is_allowed(export.record.scope, trust, peer) { + conflicts.push(build_conflict_record( + peer.peer_id.as_str(), + FederationRegistryKind::Capability, + &export.record.id, + &export.record.version, + "peer trust does not authorize the exported scope", + evidence_ref, + )); + return None; + } + + let lookup_scope = lookup_scope_for(export.record.scope); + let Some(local) = + capabilities.find_exact(lookup_scope, &export.record.id, &export.record.version) + else { + conflicts.push(build_conflict_record( + peer.peer_id.as_str(), + FederationRegistryKind::Capability, + &export.record.id, + &export.record.version, + "local approved registry is missing the exported capability", + evidence_ref, + )); + return None; + }; + + if local != *export { + conflicts.push(build_conflict_record( + peer.peer_id.as_str(), + FederationRegistryKind::Capability, + &export.record.id, + &export.record.version, + "local capability record differs from the exported peer record", + evidence_ref, + )); + return None; + } + + Some(build_snapshot( + peer, + FederationRegistryKind::Capability, + &export.record.id, + &export.record.version, + export.record.scope, + export.record.lifecycle.clone(), + &export.record.contract_path, + &format!( + "{:?}:{}:{}", + export.record.provenance.source, + export.record.provenance.author, + export.record.provenance.created_at + ), + )) +} + +fn validate_event_snapshot( + peer: &FederationPeer, + trust: &TrustRecord, + events: &EventRegistry, + export: &ResolvedEvent, + evidence_ref: &str, + conflicts: &mut Vec, +) -> Option { + if !scope_is_allowed(export.record.scope, trust, peer) { + conflicts.push(build_conflict_record( + peer.peer_id.as_str(), + FederationRegistryKind::Event, + &export.record.id, + &export.record.version, + "peer trust does not authorize the exported scope", + evidence_ref, + )); + return None; + } + + let lookup_scope = lookup_scope_for(export.record.scope); + let Some(local) = events.find_exact(lookup_scope, &export.record.id, &export.record.version) + else { + conflicts.push(build_conflict_record( + peer.peer_id.as_str(), + FederationRegistryKind::Event, + &export.record.id, + &export.record.version, + "local approved registry is missing the exported event", + evidence_ref, + )); + return None; + }; + + if local != *export { + conflicts.push(build_conflict_record( + peer.peer_id.as_str(), + FederationRegistryKind::Event, + &export.record.id, + &export.record.version, + "local event record differs from the exported peer record", + evidence_ref, + )); + return None; + } + + Some(build_snapshot( + peer, + FederationRegistryKind::Event, + &export.record.id, + &export.record.version, + export.record.scope, + export.record.lifecycle.clone(), + &export.record.contract_path, + &format!( + "{:?}:{}:{}", + export.record.provenance.source, + export.record.provenance.author, + export.record.provenance.created_at + ), + )) +} + +fn validate_workflow_snapshot( + peer: &FederationPeer, + trust: &TrustRecord, + workflows: &WorkflowRegistry, + export: &ResolvedWorkflow, + evidence_ref: &str, + conflicts: &mut Vec, +) -> Option { + if !scope_is_allowed(export.record.scope, trust, peer) { + conflicts.push(build_conflict_record( + peer.peer_id.as_str(), + FederationRegistryKind::Workflow, + &export.record.id, + &export.record.version, + "peer trust does not authorize the exported scope", + evidence_ref, + )); + return None; + } + + let lookup_scope = lookup_scope_for(export.record.scope); + let Some(local) = workflows.find_exact(lookup_scope, &export.record.id, &export.record.version) + else { + conflicts.push(build_conflict_record( + peer.peer_id.as_str(), + FederationRegistryKind::Workflow, + &export.record.id, + &export.record.version, + "local approved registry is missing the exported workflow", + evidence_ref, + )); + return None; + }; + + if local != *export { + conflicts.push(build_conflict_record( + peer.peer_id.as_str(), + FederationRegistryKind::Workflow, + &export.record.id, + &export.record.version, + "local workflow record differs from the exported peer record", + evidence_ref, + )); + return None; + } + + Some(build_snapshot( + peer, + FederationRegistryKind::Workflow, + &export.record.id, + &export.record.version, + export.record.scope, + export.record.lifecycle.clone(), + &export.record.workflow_path, + &format!( + "{}:{}:{}", + export.record.governing_spec, + export.record.validator_version, + export.record.registered_at + ), + )) +} + +fn build_snapshot( + peer: &FederationPeer, + registry_type: FederationRegistryKind, + entry_id: &str, + version: &str, + scope: RegistryScope, + lifecycle: Lifecycle, + contract_ref: &str, + provenance_ref: &str, +) -> PeerRegistrySnapshot { + PeerRegistrySnapshot { + peer_id: peer.peer_id.clone(), + registry_type, + entry_id: entry_id.to_string(), + version: version.to_string(), + scope, + approval_state: approval_state_from_lifecycle(&lifecycle), + contract_ref: contract_ref.to_string(), + provenance_ref: provenance_ref.to_string(), + } +} + +fn build_conflict_record( + peer_id: &str, + registry_type: FederationRegistryKind, + entry_id: &str, + version: &str, + reason: &str, + audit_ref: &str, +) -> ConflictRecord { + ConflictRecord { + conflict_id: format!("conflict_{}_{}_{}", peer_id, entry_id, version), + peer_ids: vec![peer_id.to_string()], + registry_type, + entry_key: format!("{registry_type:?}:{entry_id}@{version}"), + conflict_reason: reason.to_string(), + resolution_state: FederationConflictResolutionState::Open, + audit_ref: audit_ref.to_string(), + } +} + +fn approval_state_from_lifecycle(lifecycle: &Lifecycle) -> FederationApprovalState { + match lifecycle { + Lifecycle::Draft => FederationApprovalState::Draft, + Lifecycle::Active => FederationApprovalState::Approved, + Lifecycle::Deprecated => FederationApprovalState::Deprecated, + Lifecycle::Retired | Lifecycle::Archived => FederationApprovalState::Rejected, + } +} + +fn is_route_failure(status: FederationInvocationStatus) -> bool { + matches!( + status, + FederationInvocationStatus::Failure | FederationInvocationStatus::RetryableFailure + ) +} + +fn scope_is_allowed(scope: RegistryScope, trust: &TrustRecord, peer: &FederationPeer) -> bool { + trust.allowed_scopes.contains(&scope) && peer.visible_registry_scopes.contains(&scope) +} + +fn scope_is_visible(scope: RegistryScope, trust: &TrustRecord, peer: &FederationPeer) -> bool { + scope_is_allowed(scope, trust, peer) +} + +fn lookup_scope_for(scope: RegistryScope) -> LookupScope { + match scope { + RegistryScope::Public => LookupScope::PublicOnly, + RegistryScope::Private => LookupScope::PreferPrivate, + } +} + +fn synced_registry_types(snapshots: &[PeerRegistrySnapshot]) -> Vec { + let mut kinds = BTreeSet::new(); + for snapshot in snapshots { + kinds.insert(snapshot.registry_type); + } + kinds.into_iter().collect() +} + +fn federation_error(code: FederationErrorCode, target: &str, message: &str) -> FederationError { + FederationError { + code, + target: target.to_string(), + message: message.to_string(), + severity: ErrorSeverity::Error, + } +} + +#[cfg(test)] +#[allow(clippy::expect_used, clippy::too_many_lines)] +mod tests { + use super::*; + use crate::{ + ArtifactDigests, BinaryFormat, BinaryReference, CapabilityArtifactRecord, + CapabilityRegistration, CapabilityRegistry, ComposabilityMetadata, CompositionKind, + CompositionPattern, EventRegistry, ImplementationKind, RegistryProvenance, RegistryScope, + SourceKind, SourceReference, WorkflowDefinition, WorkflowNode, WorkflowNodeInput, + WorkflowNodeOutput, WorkflowRegistration, WorkflowRegistry, export_peer_state, + }; + use serde_json::json; + use traverse_contracts::{ + CapabilityContract, Entrypoint, EntrypointKind, EventClassification, EventContract, + EventPayload, EventProvenance, EventProvenanceSource, EventReference, EventType, Lifecycle, + Owner, PayloadCompatibility, SchemaContainer, SideEffect, SideEffectKind, + }; + + #[test] + fn registers_trusted_peer_and_reports_status() { + let mut federation = FederationRegistry::new(); + let peer = peer("peer-a", "Peer A"); + let trust = trust( + "peer-a", + vec![RegistryScope::Public, RegistryScope::Private], + ); + + federation + .register_peer(peer.clone(), trust.clone()) + .expect("peer should register"); + federation + .register_peer(peer.clone(), trust.clone()) + .expect("identical peer registration should be idempotent"); + + assert_eq!(federation.list_peers(), vec![peer]); + assert!(federation.sync_sessions().is_empty()); + assert!(federation.invocations().is_empty()); + let summary = federation.status_summary(); + assert_eq!(summary.peer_count, 1); + assert_eq!(summary.trusted_peer_count, 1); + assert_eq!(summary.last_sync_outcome, FederationSyncStatus::Unknown); + } + + #[test] + fn syncs_peer_export_and_routes_invocation_to_owner() { + let mut local_capabilities = CapabilityRegistry::new(); + let mut local_events = EventRegistry::new(); + let mut local_workflows = WorkflowRegistry::new(); + seed_capabilities(&mut local_capabilities); + seed_events(&mut local_events); + seed_workflows(&mut local_workflows, &local_capabilities); + + let peer = peer("peer-b", "Peer B"); + let trust = trust( + "peer-b", + vec![RegistryScope::Public, RegistryScope::Private], + ); + let export = export_peer_state( + peer.clone(), + trust.clone(), + &local_capabilities, + &local_events, + &local_workflows, + ); + + let mut federation = FederationRegistry::new(); + federation + .register_peer(peer, trust) + .expect("peer should register"); + + let outcome = federation + .sync_peer( + export, + &local_capabilities, + &local_events, + &local_workflows, + "2026-04-09T20:00:00Z", + "2026-04-09T20:01:00Z", + "evidence:sync-001", + ) + .expect("sync should pass"); + + assert_eq!(outcome.session.status, FederationSyncStatus::Success); + assert!(!outcome.accepted_snapshots.is_empty()); + assert!(outcome.conflicts.is_empty()); + + let origin_peer = self::peer("peer-a", "Peer A"); + let origin_trust = self::trust( + "peer-a", + vec![RegistryScope::Public, RegistryScope::Private], + ); + federation + .register_peer(origin_peer, origin_trust) + .expect("origin peer should register"); + let available = BTreeSet::from([String::from("peer-b")]); + let invocation = federation + .route_capability_invocation( + "peer-a", + "federation.capability.echo", + "1.0.0", + "request:001", + &available, + "2026-04-09T20:02:00Z", + "evidence:route-001", + ) + .expect("invocation should route"); + + assert_eq!(invocation.status, FederationInvocationStatus::Success); + assert_eq!(invocation.target_peer_id, "peer-b"); + assert_eq!(invocation.trace_provenance.origin_peer_id, "peer-a"); + assert_eq!(invocation.trace_provenance.owning_peer_id, "peer-b"); + assert_eq!( + invocation.response_ref.as_deref(), + Some("response://peer-b/federation.capability.echo/1.0.0") + ); + } + + #[test] + fn sync_reports_conflicts_for_divergent_private_entries() { + let mut local_capabilities = CapabilityRegistry::new(); + let mut local_events = EventRegistry::new(); + let mut local_workflows = WorkflowRegistry::new(); + seed_capabilities(&mut local_capabilities); + seed_events(&mut local_events); + seed_workflows(&mut local_workflows, &local_capabilities); + + let mut remote_capabilities = CapabilityRegistry::new(); + let mut altered_contract = capability_contract(); + altered_contract.summary = "divergent export".to_string(); + remote_capabilities + .register(capability_registration( + RegistryScope::Private, + altered_contract, + )) + .expect("remote capability should register"); + seed_events(&mut local_events); + seed_workflows(&mut local_workflows, &local_capabilities); + + let peer = peer("peer-c", "Peer C"); + let trust = trust("peer-c", vec![RegistryScope::Public]); + let export = export_peer_state( + peer.clone(), + trust.clone(), + &remote_capabilities, + &local_events, + &local_workflows, + ); + + let mut federation = FederationRegistry::new(); + federation + .register_peer(peer, trust) + .expect("peer should register"); + + let outcome = federation + .sync_peer( + export, + &local_capabilities, + &local_events, + &local_workflows, + "2026-04-09T20:10:00Z", + "2026-04-09T20:11:00Z", + "evidence:sync-002", + ) + .expect("sync should report conflicts rather than failing"); + + assert_eq!(outcome.session.status, FederationSyncStatus::Partial); + assert!(!outcome.conflicts.is_empty()); + assert_eq!(federation.conflicts().len(), outcome.conflicts.len()); + } + + #[test] + fn sync_reports_conflicts_for_permitted_but_divergent_private_capability_entries() { + let mut local_capabilities = CapabilityRegistry::new(); + seed_capabilities(&mut local_capabilities); + + let mut altered_contract = private_capability_contract(); + altered_contract.summary = "altered private capability".to_string(); + let mut remote_capabilities = CapabilityRegistry::new(); + remote_capabilities + .register(capability_registration( + RegistryScope::Private, + altered_contract, + )) + .expect("remote private capability should register"); + + let peer = peer("peer-capability-divergent", "Peer Capability Divergent"); + let trust = trust( + "peer-capability-divergent", + vec![RegistryScope::Public, RegistryScope::Private], + ); + + let mut federation = FederationRegistry::new(); + federation + .register_peer(peer.clone(), trust.clone()) + .expect("peer should register"); + + let export = export_peer_state( + peer, + trust, + &remote_capabilities, + &EventRegistry::new(), + &WorkflowRegistry::new(), + ); + let outcome = federation + .sync_peer( + export, + &local_capabilities, + &EventRegistry::new(), + &WorkflowRegistry::new(), + "2026-04-09T20:50:00Z", + "2026-04-09T20:51:00Z", + "evidence:divergent-private-capability", + ) + .expect("sync should report permitted capability divergence as conflicts"); + + assert!(outcome.conflicts.iter().any(|conflict| { + conflict + .conflict_reason + .contains("local capability record differs") + })); + } + + #[test] + fn sync_peer_rejects_trust_and_peer_state_mismatches() { + let mut federation = FederationRegistry::new(); + let peer = peer("peer-sync-guard", "Peer Sync Guard"); + let trust = trust("peer-sync-guard", vec![RegistryScope::Public]); + + federation + .register_peer(peer.clone(), trust.clone()) + .expect("peer should register"); + + let export = FederationPeerExport { + peer: peer.clone(), + trust: trust.clone(), + capabilities: Vec::new(), + events: Vec::new(), + workflows: Vec::new(), + }; + + let mut mismatched_trust = export.clone(); + mismatched_trust.trust.peer_id = "other-peer".to_string(); + assert!( + federation + .sync_peer( + mismatched_trust, + &CapabilityRegistry::new(), + &EventRegistry::new(), + &WorkflowRegistry::new(), + "2026-04-09T20:20:00Z", + "2026-04-09T20:21:00Z", + "evidence:sync-mismatch", + ) + .is_err() + ); + + federation.trust_records.remove("peer-sync-guard"); + assert!( + federation + .sync_peer( + export.clone(), + &CapabilityRegistry::new(), + &EventRegistry::new(), + &WorkflowRegistry::new(), + "2026-04-09T20:22:00Z", + "2026-04-09T20:23:00Z", + "evidence:sync-missing-trust", + ) + .is_err() + ); + + federation.peers.remove("peer-sync-guard"); + federation + .register_peer(peer.clone(), trust.clone()) + .expect("peer should re-register"); + if let Some(registered_peer) = federation.peers.get_mut("peer-sync-guard") { + registered_peer.sync_enabled = false; + registered_peer.trust_state = FederationTrustState::Pending; + } + + assert!( + federation + .sync_peer( + export, + &CapabilityRegistry::new(), + &EventRegistry::new(), + &WorkflowRegistry::new(), + "2026-04-09T20:24:00Z", + "2026-04-09T20:25:00Z", + "evidence:sync-disabled", + ) + .is_err() + ); + } + + #[test] + fn sync_peer_reports_missing_local_registry_entries() { + let peer = peer("peer-missing", "Peer Missing"); + let trust = trust( + "peer-missing", + vec![RegistryScope::Public, RegistryScope::Private], + ); + + let mut federation = FederationRegistry::new(); + federation + .register_peer(peer.clone(), trust.clone()) + .expect("peer should register"); + let mut remote_capabilities = CapabilityRegistry::new(); + seed_capabilities(&mut remote_capabilities); + let local_capabilities = CapabilityRegistry::new(); + let local_events = EventRegistry::new(); + let local_workflows = WorkflowRegistry::new(); + let capability_export = export_peer_state( + peer.clone(), + trust.clone(), + &remote_capabilities, + &EventRegistry::new(), + &WorkflowRegistry::new(), + ); + let capability_outcome = federation + .sync_peer( + capability_export, + &local_capabilities, + &local_events, + &local_workflows, + "2026-04-09T20:30:00Z", + "2026-04-09T20:31:00Z", + "evidence:missing-capability", + ) + .expect("sync should report missing capability as conflict"); + assert!(capability_outcome.conflicts.iter().any(|conflict| { + conflict + .conflict_reason + .contains("missing the exported capability") + })); + + let mut federation = FederationRegistry::new(); + federation + .register_peer(peer.clone(), trust.clone()) + .expect("peer should register"); + let mut remote_events = EventRegistry::new(); + seed_events(&mut remote_events); + let event_export = export_peer_state( + peer.clone(), + trust.clone(), + &CapabilityRegistry::new(), + &remote_events, + &WorkflowRegistry::new(), + ); + let event_outcome = federation + .sync_peer( + event_export, + &CapabilityRegistry::new(), + &EventRegistry::new(), + &WorkflowRegistry::new(), + "2026-04-09T20:32:00Z", + "2026-04-09T20:33:00Z", + "evidence:missing-event", + ) + .expect("sync should report missing event as conflict"); + assert!(event_outcome.conflicts.iter().any(|conflict| { + conflict + .conflict_reason + .contains("missing the exported event") + })); + + let mut federation = FederationRegistry::new(); + federation + .register_peer(peer.clone(), trust.clone()) + .expect("peer should register"); + let mut remote_workflows = WorkflowRegistry::new(); + let mut local_capabilities = CapabilityRegistry::new(); + let mut local_events = EventRegistry::new(); + seed_capabilities(&mut local_capabilities); + seed_events(&mut local_events); + seed_workflows(&mut remote_workflows, &local_capabilities); + let workflow_export = export_peer_state( + peer.clone(), + trust.clone(), + &CapabilityRegistry::new(), + &EventRegistry::new(), + &remote_workflows, + ); + let workflow_outcome = federation + .sync_peer( + workflow_export, + &CapabilityRegistry::new(), + &EventRegistry::new(), + &WorkflowRegistry::new(), + "2026-04-09T20:34:00Z", + "2026-04-09T20:35:00Z", + "evidence:missing-workflow", + ) + .expect("sync should report missing workflow as conflict"); + assert!(workflow_outcome.conflicts.iter().any(|conflict| { + conflict + .conflict_reason + .contains("missing the exported workflow") + })); + } + + #[test] + fn sync_peer_marks_empty_exports_as_failed() { + let mut federation = FederationRegistry::new(); + let peer = peer("peer-empty", "Peer Empty"); + let trust = trust("peer-empty", vec![RegistryScope::Public]); + federation + .register_peer(peer.clone(), trust.clone()) + .expect("peer should register"); + + let export = export_peer_state( + peer, + trust, + &CapabilityRegistry::new(), + &EventRegistry::new(), + &WorkflowRegistry::new(), + ); + let outcome = federation + .sync_peer( + export, + &CapabilityRegistry::new(), + &EventRegistry::new(), + &WorkflowRegistry::new(), + "2026-04-09T20:40:00Z", + "2026-04-09T20:41:00Z", + "evidence:sync-empty", + ) + .expect("empty export should still be accepted as a failed sync outcome"); + + assert_eq!(outcome.session.status, FederationSyncStatus::Failed); + assert!(outcome.accepted_snapshots.is_empty()); + assert!(outcome.conflicts.is_empty()); + } + + #[test] + fn sync_peer_rejects_private_exports_without_scope_authority() { + let mut federation = FederationRegistry::new(); + let peer = peer("peer-private", "Peer Private"); + let trust = trust("peer-private", vec![RegistryScope::Public]); + federation + .register_peer(peer.clone(), trust.clone()) + .expect("peer should register"); + + let mut remote_capabilities = CapabilityRegistry::new(); + remote_capabilities + .register(capability_registration( + RegistryScope::Private, + private_capability_contract(), + )) + .expect("private capability should register"); + + let mut remote_events = EventRegistry::new(); + remote_events + .register(event_registration(RegistryScope::Private, event_contract())) + .expect("private event should register"); + + let mut workflow_capabilities = CapabilityRegistry::new(); + seed_capabilities(&mut workflow_capabilities); + let mut remote_workflows = WorkflowRegistry::new(); + remote_workflows + .register( + &workflow_capabilities, + workflow_registration(RegistryScope::Private, workflow_definition()), + ) + .expect("private workflow should register"); + + let export = export_peer_state( + peer, + trust, + &remote_capabilities, + &remote_events, + &remote_workflows, + ); + let outcome = federation + .sync_peer( + export, + &CapabilityRegistry::new(), + &EventRegistry::new(), + &WorkflowRegistry::new(), + "2026-04-09T20:42:00Z", + "2026-04-09T20:43:00Z", + "evidence:sync-private", + ) + .expect("sync should report private-scope rejection as conflicts"); + + assert_eq!(outcome.session.status, FederationSyncStatus::Partial); + assert!( + outcome + .conflicts + .iter() + .all(|conflict| conflict.conflict_reason.contains("does not authorize")) + ); + } + + #[test] + fn sync_reports_conflicts_for_divergent_private_event_and_workflow_entries() { + let mut federation = FederationRegistry::new(); + let peer = peer("peer-divergent", "Peer Divergent"); + let trust = trust( + "peer-divergent", + vec![RegistryScope::Public, RegistryScope::Private], + ); + federation + .register_peer(peer.clone(), trust.clone()) + .expect("peer should register"); + + let mut local_events = EventRegistry::new(); + let mut remote_events = EventRegistry::new(); + let mut local_event_contract = event_contract(); + local_event_contract.summary = "local event".to_string(); + local_events + .register(event_registration( + RegistryScope::Private, + local_event_contract.clone(), + )) + .expect("local private event should register"); + let mut remote_event_contract = local_event_contract.clone(); + remote_event_contract.summary = "remote event".to_string(); + remote_events + .register(event_registration( + RegistryScope::Private, + remote_event_contract, + )) + .expect("remote private event should register"); + + let event_export = export_peer_state( + peer.clone(), + trust.clone(), + &CapabilityRegistry::new(), + &remote_events, + &WorkflowRegistry::new(), + ); + let event_outcome = federation + .sync_peer( + event_export, + &CapabilityRegistry::new(), + &local_events, + &WorkflowRegistry::new(), + "2026-04-09T20:44:00Z", + "2026-04-09T20:45:00Z", + "evidence:divergent-event", + ) + .expect("event divergence should report conflicts"); + assert!(event_outcome.conflicts.iter().any(|conflict| { + conflict + .conflict_reason + .contains("local event record differs") + })); + + let mut federation = FederationRegistry::new(); + federation + .register_peer(peer.clone(), trust.clone()) + .expect("peer should register"); + let mut local_capabilities = CapabilityRegistry::new(); + let mut remote_capabilities = CapabilityRegistry::new(); + seed_capabilities(&mut local_capabilities); + seed_capabilities(&mut remote_capabilities); + + let mut local_workflows = WorkflowRegistry::new(); + let mut remote_workflows = WorkflowRegistry::new(); + local_workflows + .register( + &local_capabilities, + workflow_registration(RegistryScope::Private, workflow_definition()), + ) + .expect("local private workflow should register"); + let mut remote_workflow_definition = workflow_definition(); + remote_workflow_definition.summary = "remote workflow".to_string(); + remote_workflows + .register( + &remote_capabilities, + workflow_registration(RegistryScope::Private, remote_workflow_definition), + ) + .expect("remote private workflow should register"); + + let workflow_export = export_peer_state( + peer, + trust, + &remote_capabilities, + &EventRegistry::new(), + &remote_workflows, + ); + let workflow_outcome = federation + .sync_peer( + workflow_export, + &local_capabilities, + &EventRegistry::new(), + &local_workflows, + "2026-04-09T20:46:00Z", + "2026-04-09T20:47:00Z", + "evidence:divergent-workflow", + ) + .expect("workflow divergence should report conflicts"); + assert!(workflow_outcome.conflicts.iter().any(|conflict| { + conflict + .conflict_reason + .contains("local workflow record differs") + })); + } + + #[test] + fn route_capability_invocation_returns_error_without_matching_snapshot() { + let mut federation = FederationRegistry::new(); + let origin_peer = peer("peer-route-empty", "Peer Route Empty"); + let origin_trust = trust("peer-route-empty", vec![RegistryScope::Public]); + federation + .register_peer(origin_peer, origin_trust) + .expect("origin peer should register"); + + let failure = federation + .route_capability_invocation( + "peer-route-empty", + "federation.capability.missing", + "9.9.9", + "request:missing", + &BTreeSet::from([String::from("peer-route-empty")]), + "2026-04-09T20:48:00Z", + "evidence:missing-route", + ) + .expect_err("missing snapshot should fail closed"); + + assert_eq!( + failure.errors[0].code, + FederationErrorCode::EntryValidationFailed + ); + } + + #[test] + fn register_peer_rejects_invalid_and_duplicate_peers() { + let mut federation = FederationRegistry::new(); + + let mut invalid_peer = peer("peer-invalid", "Peer Invalid"); + invalid_peer.peer_id.clear(); + invalid_peer.display_name.clear(); + invalid_peer.identity_fingerprint.clear(); + invalid_peer.sync_enabled = false; + invalid_peer.trust_state = FederationTrustState::Pending; + + let invalid_trust = TrustRecord { + peer_id: "other-peer".to_string(), + trust_model: "allow-list".to_string(), + allowed_scopes: vec![], + approved_spec_refs: vec![], + approved_at: "2026-04-09T00:00:00Z".to_string(), + revoked_at: None, + }; + + assert!( + federation + .register_peer(invalid_peer, invalid_trust) + .is_err() + ); + + let peer = peer("peer-dup", "Peer Dup"); + let trust = trust("peer-dup", vec![RegistryScope::Public]); + federation + .register_peer(peer.clone(), trust.clone()) + .expect("peer should register"); + + let mut changed_peer = peer.clone(); + changed_peer.display_name = "Peer Dup Updated".to_string(); + assert!(federation.register_peer(changed_peer, trust).is_err()); + } + + #[test] + fn sync_peer_rejects_unregistered_and_invalid_export_paths() { + let mut federation = FederationRegistry::new(); + let registered_peer = peer("peer-sync", "Peer Sync"); + let trust = trust("peer-sync", vec![RegistryScope::Public]); + federation + .register_peer(registered_peer.clone(), trust.clone()) + .expect("peer should register"); + + let local_capabilities = CapabilityRegistry::new(); + let local_events = EventRegistry::new(); + let local_workflows = WorkflowRegistry::new(); + let export = FederationPeerExport { + peer: registered_peer.clone(), + trust: trust.clone(), + capabilities: Vec::new(), + events: Vec::new(), + workflows: Vec::new(), + }; + + assert!( + federation + .sync_peer( + export.clone(), + &local_capabilities, + &local_events, + &local_workflows, + "", + "", + "", + ) + .is_err() + ); + + let mut bad_peer = peer("peer-sync-bad", "Peer Sync Bad"); + bad_peer.sync_enabled = false; + let bad_export = FederationPeerExport { + peer: bad_peer, + trust: TrustRecord { + peer_id: "peer-sync-bad".to_string(), + trust_model: "allow-list".to_string(), + allowed_scopes: vec![RegistryScope::Public], + approved_spec_refs: vec!["005-capability-registry".to_string()], + approved_at: "2026-04-09T00:00:00Z".to_string(), + revoked_at: None, + }, + capabilities: Vec::new(), + events: Vec::new(), + workflows: Vec::new(), + }; + + assert!( + federation + .sync_peer( + bad_export, + &local_capabilities, + &local_events, + &local_workflows, + "2026-04-09T20:00:00Z", + "2026-04-09T20:01:00Z", + "evidence:sync-invalid", + ) + .is_err() + ); + } + + #[test] + fn route_capability_invocation_covers_missing_and_unavailable_paths() { + let mut federation = FederationRegistry::new(); + let origin_peer = peer("peer-route", "Peer Route"); + let origin_trust = trust("peer-route", vec![RegistryScope::Public]); + federation + .register_peer(origin_peer.clone(), origin_trust) + .expect("origin peer should register"); + + assert!( + federation + .route_capability_invocation("", "", "", "", &BTreeSet::new(), "", "",) + .is_err() + ); + + let mut local_capabilities = CapabilityRegistry::new(); + let mut local_events = EventRegistry::new(); + let mut local_workflows = WorkflowRegistry::new(); + seed_capabilities(&mut local_capabilities); + seed_events(&mut local_events); + seed_workflows(&mut local_workflows, &local_capabilities); + + let target_peer = peer("peer-target", "Peer Target"); + let target_trust = trust( + "peer-target", + vec![RegistryScope::Public, RegistryScope::Private], + ); + let export = export_peer_state( + target_peer.clone(), + target_trust.clone(), + &local_capabilities, + &local_events, + &local_workflows, + ); + federation + .register_peer(target_peer, target_trust) + .expect("target peer should register"); + federation + .sync_peer( + export, + &local_capabilities, + &local_events, + &local_workflows, + "2026-04-09T21:00:00Z", + "2026-04-09T21:01:00Z", + "evidence:sync-route", + ) + .expect("sync should succeed"); + + let unavailable = BTreeSet::new(); + let invocation = federation + .route_capability_invocation( + "peer-route", + "federation.capability.echo", + "1.0.0", + "request:route-unavailable", + &unavailable, + "2026-04-09T21:02:00Z", + "evidence:route-unavailable", + ) + .expect("route should return retryable failure rather than error"); + + assert_eq!( + invocation.status, + FederationInvocationStatus::RetryableFailure + ); + assert!(invocation.response_ref.is_none()); + } + + #[test] + fn status_summary_counts_sync_and_route_failures() { + let mut federation = FederationRegistry::new(); + federation.peers.insert( + "peer-summary".to_string(), + peer("peer-summary", "Peer Summary"), + ); + federation.sync_sessions.push(FederationSyncSession { + session_id: "sync_peer-summary_1".to_string(), + peer_id: "peer-summary".to_string(), + started_at: "2026-04-09T22:00:00Z".to_string(), + finished_at: Some("2026-04-09T22:01:00Z".to_string()), + status: FederationSyncStatus::Partial, + registry_types: vec![FederationRegistryKind::Capability], + validated_entries: 1, + rejected_entries: 2, + conflict_count: 2, + evidence_ref: "evidence:summary".to_string(), + }); + federation.invocations.push(FederatedInvocation { + invocation_id: "invocation_peer-summary_federation.capability.echo_1.0.0".to_string(), + origin_peer_id: "peer-summary".to_string(), + target_peer_id: "peer-target".to_string(), + capability_id: "federation.capability.echo".to_string(), + request_ref: "request:summary".to_string(), + status: FederationInvocationStatus::Failure, + response_ref: None, + trace_provenance: CrossPeerTraceProvenance { + trace_id: "trace_peer-summary_federation.capability.echo_1.0.0".to_string(), + origin_peer_id: "peer-summary".to_string(), + owning_peer_id: "peer-target".to_string(), + route_reason: "test route".to_string(), + sync_session_ref: None, + response_status: FederationInvocationStatus::Failure, + evidence_ref: "evidence:summary-route".to_string(), + }, + }); + federation.invocations.push(FederatedInvocation { + invocation_id: "invocation_peer-summary_federation.capability.echo_1.0.1".to_string(), + origin_peer_id: "peer-summary".to_string(), + target_peer_id: "peer-target".to_string(), + capability_id: "federation.capability.echo".to_string(), + request_ref: "request:summary-retryable".to_string(), + status: FederationInvocationStatus::RetryableFailure, + response_ref: None, + trace_provenance: CrossPeerTraceProvenance { + trace_id: "trace_peer-summary_federation.capability.echo_1.0.1".to_string(), + origin_peer_id: "peer-summary".to_string(), + owning_peer_id: "peer-target".to_string(), + route_reason: "retryable route".to_string(), + sync_session_ref: None, + response_status: FederationInvocationStatus::RetryableFailure, + evidence_ref: "evidence:summary-route-retryable".to_string(), + }, + }); + + let summary = federation.status_summary(); + assert_eq!(summary.peer_count, 1); + assert_eq!(summary.trusted_peer_count, 1); + assert_eq!(summary.last_sync_outcome, FederationSyncStatus::Partial); + assert_eq!(summary.blocked_entries, 2); + assert_eq!(summary.route_failures, 2); + } + + #[test] + fn approval_state_from_lifecycle_covers_all_states() { + assert_eq!( + approval_state_from_lifecycle(&Lifecycle::Draft), + FederationApprovalState::Draft + ); + assert_eq!( + approval_state_from_lifecycle(&Lifecycle::Active), + FederationApprovalState::Approved + ); + assert_eq!( + approval_state_from_lifecycle(&Lifecycle::Deprecated), + FederationApprovalState::Deprecated + ); + assert_eq!( + approval_state_from_lifecycle(&Lifecycle::Retired), + FederationApprovalState::Rejected + ); + assert_eq!( + approval_state_from_lifecycle(&Lifecycle::Archived), + FederationApprovalState::Rejected + ); + } + + #[test] + fn is_route_failure_covers_failure_variants() { + assert!(!is_route_failure(FederationInvocationStatus::Success)); + assert!(is_route_failure(FederationInvocationStatus::Failure)); + assert!(is_route_failure( + FederationInvocationStatus::RetryableFailure + )); + } + + fn peer(peer_id: &str, display_name: &str) -> FederationPeer { + FederationPeer { + peer_id: peer_id.to_string(), + display_name: display_name.to_string(), + trust_state: FederationTrustState::Trusted, + identity_fingerprint: format!("fingerprint:{peer_id}"), + sync_enabled: true, + last_sync_at: None, + last_sync_status: FederationSyncStatus::Unknown, + visible_registry_scopes: vec![RegistryScope::Public, RegistryScope::Private], + } + } + + fn trust(peer_id: &str, scopes: Vec) -> TrustRecord { + TrustRecord { + peer_id: peer_id.to_string(), + trust_model: "shared-api-token".to_string(), + allowed_scopes: scopes, + approved_spec_refs: vec!["026-federation-registry-routing".to_string()], + approved_at: "2026-04-09T19:30:00Z".to_string(), + revoked_at: None, + } + } + + fn seed_capabilities(registry: &mut CapabilityRegistry) { + registry + .register(capability_registration( + RegistryScope::Public, + capability_contract(), + )) + .expect("capability should register"); + registry + .register(capability_registration( + RegistryScope::Private, + private_capability_contract(), + )) + .expect("private capability should register"); + } + + fn seed_events(registry: &mut EventRegistry) { + registry + .register(event_registration(RegistryScope::Public, event_contract())) + .expect("event should register"); + } + + fn seed_workflows(registry: &mut WorkflowRegistry, capabilities: &CapabilityRegistry) { + registry + .register( + capabilities, + workflow_registration(RegistryScope::Public, workflow_definition()), + ) + .expect("workflow should register"); + } + + fn capability_contract() -> CapabilityContract { + CapabilityContract { + kind: "capability_contract".to_string(), + schema_version: "1.0.0".to_string(), + id: "federation.capability.echo".to_string(), + namespace: "federation.capability".to_string(), + name: "echo".to_string(), + version: "1.0.0".to_string(), + lifecycle: Lifecycle::Active, + owner: Owner { + team: "platform".to_string(), + contact: "platform@example.com".to_string(), + }, + summary: "Echo a federated capability call.".to_string(), + description: "End-to-end federation test capability.".to_string(), + inputs: SchemaContainer { + schema: json!({"type":"object"}), + }, + outputs: SchemaContainer { + schema: json!({"type":"object"}), + }, + preconditions: vec![], + postconditions: vec![], + side_effects: vec![SideEffect { + kind: SideEffectKind::EventEmission, + description: "Emit routing evidence for federation sync.".to_string(), + }], + emits: vec![EventReference { + event_id: "federation.event.routed".to_string(), + version: "1.0.0".to_string(), + }], + consumes: vec![], + permissions: vec![], + execution: traverse_contracts::Execution { + binary_format: traverse_contracts::BinaryFormat::Wasm, + entrypoint: Entrypoint { + kind: EntrypointKind::WasiCommand, + command: "echo".to_string(), + }, + preferred_targets: vec![traverse_contracts::ExecutionTarget::Local], + constraints: traverse_contracts::ExecutionConstraints { + host_api_access: traverse_contracts::HostApiAccess::None, + filesystem_access: traverse_contracts::FilesystemAccess::None, + network_access: traverse_contracts::NetworkAccess::Forbidden, + }, + }, + policies: vec![], + dependencies: vec![], + provenance: traverse_contracts::Provenance { + source: traverse_contracts::ProvenanceSource::Greenfield, + author: "enricopiovesan".to_string(), + created_at: "2026-04-09T19:00:00Z".to_string(), + spec_ref: Some("026-federation-registry-routing".to_string()), + adr_refs: vec![], + exception_refs: vec![], + }, + evidence: vec![], + service_type: traverse_contracts::ServiceType::Stateless, + permitted_targets: vec![ + traverse_contracts::ExecutionTarget::Local, + traverse_contracts::ExecutionTarget::Browser, + traverse_contracts::ExecutionTarget::Edge, + traverse_contracts::ExecutionTarget::Cloud, + traverse_contracts::ExecutionTarget::Worker, + traverse_contracts::ExecutionTarget::Device, + ], + event_trigger: None, + } + } + + fn private_capability_contract() -> CapabilityContract { + let mut contract = capability_contract(); + contract.id = "federation.capability.private-echo".to_string(); + contract.name = "private-echo".to_string(); + contract.summary = "Private federated echo.".to_string(); + contract + } + + fn event_contract() -> EventContract { + EventContract { + kind: "event_contract".to_string(), + schema_version: "1.0.0".to_string(), + id: "federation.event.routed".to_string(), + namespace: "federation.event".to_string(), + name: "routed".to_string(), + version: "1.0.0".to_string(), + lifecycle: Lifecycle::Active, + owner: Owner { + team: "platform".to_string(), + contact: "platform@example.com".to_string(), + }, + summary: "A federation routing event.".to_string(), + description: "End-to-end federation event.".to_string(), + payload: EventPayload { + schema: json!({"type":"object"}), + compatibility: PayloadCompatibility::BackwardCompatible, + }, + classification: EventClassification { + domain: "federation".to_string(), + bounded_context: "registry".to_string(), + event_type: EventType::System, + tags: vec!["federation".to_string()], + }, + publishers: vec![traverse_contracts::CapabilityReference { + capability_id: "federation.capability.echo".to_string(), + version: "1.0.0".to_string(), + }], + subscribers: vec![traverse_contracts::CapabilityReference { + capability_id: "federation.capability.private-echo".to_string(), + version: "1.0.0".to_string(), + }], + policies: vec![], + tags: vec!["federation".to_string()], + provenance: EventProvenance { + source: EventProvenanceSource::Greenfield, + author: "enricopiovesan".to_string(), + created_at: "2026-04-09T19:00:00Z".to_string(), + }, + evidence: vec![], + } + } + + fn workflow_definition() -> WorkflowDefinition { + WorkflowDefinition { + kind: "workflow_definition".to_string(), + schema_version: "1.0.0".to_string(), + id: "federation.workflow.route".to_string(), + name: "route".to_string(), + version: "1.0.0".to_string(), + lifecycle: Lifecycle::Active, + owner: Owner { + team: "platform".to_string(), + contact: "platform@example.com".to_string(), + }, + summary: "A federated routing workflow.".to_string(), + inputs: SchemaContainer { + schema: json!({"type":"object"}), + }, + outputs: SchemaContainer { + schema: json!({"type":"object"}), + }, + nodes: vec![WorkflowNode { + node_id: "route-node".to_string(), + capability_id: "federation.capability.echo".to_string(), + capability_version: "1.0.0".to_string(), + input: WorkflowNodeInput { + from_workflow_input: vec!["request".to_string()], + }, + output: WorkflowNodeOutput { + to_workflow_state: vec!["response".to_string()], + }, + }], + edges: vec![], + start_node: "route-node".to_string(), + terminal_nodes: vec!["route-node".to_string()], + tags: vec!["federation".to_string()], + governing_spec: "007-workflow-registry-traversal".to_string(), + } + } + + fn capability_registration( + scope: RegistryScope, + contract: CapabilityContract, + ) -> CapabilityRegistration { + CapabilityRegistration { + scope, + contract_path: format!( + "registry/{}/{}/{}{}", + scope_name(scope), + contract.id, + contract.version, + "/contract.json" + ), + artifact: CapabilityArtifactRecord { + artifact_ref: format!("artifact:{}:{}", contract.name, contract.version), + implementation_kind: ImplementationKind::Executable, + source: SourceReference { + kind: SourceKind::Git, + location: format!("https://example.invalid/{}", contract.name), + }, + binary: Some(BinaryReference { + format: BinaryFormat::Wasm, + location: format!("artifacts/{}/{}.wasm", contract.name, contract.version), + }), + workflow_ref: None, + digests: ArtifactDigests { + source_digest: format!("source:{}:{}", contract.name, contract.version), + binary_digest: Some(format!("binary:{}:{}", contract.name, contract.version)), + }, + provenance: RegistryProvenance { + source: "greenfield".to_string(), + author: "enricopiovesan".to_string(), + created_at: "2026-04-09T19:00:00Z".to_string(), + }, + }, + registered_at: "2026-04-09T19:00:00Z".to_string(), + tags: vec!["federation".to_string()], + composability: ComposabilityMetadata { + kind: CompositionKind::Atomic, + patterns: vec![CompositionPattern::Sequential], + provides: vec!["federation".to_string()], + requires: vec!["registry".to_string()], + }, + governing_spec: "005-capability-registry".to_string(), + validator_version: "registry-test".to_string(), + contract, + } + } + + fn event_registration( + scope: RegistryScope, + contract: EventContract, + ) -> crate::EventRegistration { + crate::EventRegistration { + scope, + contract, + contract_path: format!( + "registry/{}/{}/{}{}", + scope_name(scope), + "federation.event.routed", + "1.0.0", + "/contract.json" + ), + registered_at: "2026-04-09T19:00:00Z".to_string(), + governing_spec: "011-event-registry".to_string(), + validator_version: "registry-test".to_string(), + } + } + + fn workflow_registration( + scope: RegistryScope, + definition: WorkflowDefinition, + ) -> WorkflowRegistration { + WorkflowRegistration { + scope, + definition, + workflow_path: "registry/public/federation.workflow.route/1.0.0/workflow.json" + .to_string(), + registered_at: "2026-04-09T19:00:00Z".to_string(), + validator_version: "registry-test".to_string(), + } + } + + fn scope_name(scope: RegistryScope) -> &'static str { + match scope { + RegistryScope::Public => "public", + RegistryScope::Private => "private", + } + } +} diff --git a/crates/traverse-registry/src/lib.rs b/crates/traverse-registry/src/lib.rs index c8d55654..68692952 100644 --- a/crates/traverse-registry/src/lib.rs +++ b/crates/traverse-registry/src/lib.rs @@ -2,10 +2,12 @@ mod bundle; mod events; +mod federation; mod graph; mod workflows; pub use bundle::*; pub use events::*; +pub use federation::*; pub use graph::*; pub use workflows::*; diff --git a/docs/app-consumable-requirements-traceability.md b/docs/app-consumable-requirements-traceability.md index 3255c909..1d29e485 100644 --- a/docs/app-consumable-requirements-traceability.md +++ b/docs/app-consumable-requirements-traceability.md @@ -1,47 +1,50 @@ # App-Consumable Requirements Traceability -This document maps the first app-consumable Traverse requirements to the current GitHub issue and Project 1 state. +This document maps the first app-consumable Traverse requirements to GitHub issues on [Project 1](https://github.com/users/enricopiovesan/projects/1/). -Project 1 is the canonical task board for this work. Every requirement area below must have one or more tickets in that project, and the ticket state must make the release picture obvious without having to reconcile old notes manually. +Project 1 is the canonical task board for this work. Every requirement area below must have one or more tickets in that project, with explicit indication of whether more governing spec work is needed. ## Functional Requirements -| Requirement Area | Covered By Tickets | Current State | +| Requirement Area | Covered By Tickets | Spec Signal | |---|---|---| -| Root app-consumable onboarding | [#122](https://github.com/enricopiovesan/Traverse/issues/122), [#127](https://github.com/enricopiovesan/Traverse/issues/127), [#142](https://github.com/enricopiovesan/Traverse/issues/142), [#143](https://github.com/enricopiovesan/Traverse/issues/143) | `Done` / `In Progress` | -| Canonical docs entry path | [#142](https://github.com/enricopiovesan/Traverse/issues/142), [#144](https://github.com/enricopiovesan/Traverse/issues/144) | `In Progress` / `Ready` | -| Release checklist and release-readiness evidence | [#127](https://github.com/enricopiovesan/Traverse/issues/127), [#145](https://github.com/enricopiovesan/Traverse/issues/145), [#150](https://github.com/enricopiovesan/Traverse/issues/150) | `In Progress` / `In Progress` / `In Progress` | -| Versioned consumer bundle and installation steps | [#176](https://github.com/enricopiovesan/Traverse/issues/176) | `Ready` | -| Live browser-consumer path | [#120](https://github.com/enricopiovesan/Traverse/issues/120), [#121](https://github.com/enricopiovesan/Traverse/issues/121), [#123](https://github.com/enricopiovesan/Traverse/issues/123) | `Done` | -| Downstream consumer contract and app-facing validation | [#126](https://github.com/enricopiovesan/Traverse/issues/126), [#128](https://github.com/enricopiovesan/Traverse/issues/128), [#129](https://github.com/enricopiovesan/Traverse/issues/129) | `Done` | -| Real browser-hosted `youaskm3` shell validation | [#179](https://github.com/enricopiovesan/Traverse/issues/179) | `In Progress` | -| Published-artifact validation against packaged Traverse runtime and MCP artifacts | [#200](https://github.com/enricopiovesan/Traverse/issues/200) | `In Progress` | -| MCP WASM server model and validation | [#146](https://github.com/enricopiovesan/Traverse/issues/146), [#158](https://github.com/enricopiovesan/Traverse/issues/158), [#148](https://github.com/enricopiovesan/Traverse/issues/148) | `Done` / `In Progress` / `Blocked` | +| Public integration surface for downstream apps | [#126](https://github.com/enricopiovesan/Traverse/issues/126), [#128](https://github.com/enricopiovesan/Traverse/issues/128) | `spec`, `needs-spec` | +| Runtime execution for app consumers | [#120](https://github.com/enricopiovesan/Traverse/issues/120), [#121](https://github.com/enricopiovesan/Traverse/issues/121), [#123](https://github.com/enricopiovesan/Traverse/issues/123), [#128](https://github.com/enricopiovesan/Traverse/issues/128) | `no-spec-needed`, `needs-spec` | +| Eventing and subscriptions for the app | [#120](https://github.com/enricopiovesan/Traverse/issues/120), [#121](https://github.com/enricopiovesan/Traverse/issues/121), [#123](https://github.com/enricopiovesan/Traverse/issues/123) | `no-spec-needed` | +| Browser adapter transport and live browser path | [#120](https://github.com/enricopiovesan/Traverse/issues/120), [#121](https://github.com/enricopiovesan/Traverse/issues/121) | `no-spec-needed` | +| MCP consumption path for downstream apps | [#129](https://github.com/enricopiovesan/Traverse/issues/129) | `needs-spec` | +| Contracts and compatibility boundary for downstream apps | [#126](https://github.com/enricopiovesan/Traverse/issues/126), [#127](https://github.com/enricopiovesan/Traverse/issues/127) | `spec`, `needs-spec` | +| Portable packaging for executable agents and capabilities | [#109](https://github.com/enricopiovesan/Traverse/pull/109), [#53](https://github.com/enricopiovesan/Traverse/issues/53), [#111](https://github.com/enricopiovesan/Traverse/issues/111) | implemented / `no-spec-needed` | +| Developer workflow and app-consumable quickstart | [#122](https://github.com/enricopiovesan/Traverse/issues/122), [#123](https://github.com/enricopiovesan/Traverse/issues/123) | `no-spec-needed` | +| Release/governance path for first external consumer use | [#126](https://github.com/enricopiovesan/Traverse/issues/126), [#127](https://github.com/enricopiovesan/Traverse/issues/127), [#128](https://github.com/enricopiovesan/Traverse/issues/128), [#129](https://github.com/enricopiovesan/Traverse/issues/129) | `spec`, `needs-spec` | ## Non-Functional Requirements -| Requirement Area | Covered By Tickets | Current State | +| Requirement Area | Covered By Tickets | Spec Signal | |---|---|---| -| Documentation clarity for the first app-consumable path | [#142](https://github.com/enricopiovesan/Traverse/issues/142), [#143](https://github.com/enricopiovesan/Traverse/issues/143), [#145](https://github.com/enricopiovesan/Traverse/issues/145) | `In Progress` / `Done` / `In Progress` | -| Traceability from requirements to release artifacts | [#145](https://github.com/enricopiovesan/Traverse/issues/145), [#150](https://github.com/enricopiovesan/Traverse/issues/150), [#195](https://github.com/enricopiovesan/Traverse/issues/195) | `In Progress` / `Done` / `In Progress` | -| Operational safety boundary for app consumers | [#131](https://github.com/enricopiovesan/Traverse/issues/131) | `Blocked` | -| First app-consumable performance baseline | [#130](https://github.com/enricopiovesan/Traverse/issues/130) | `Blocked` | +| Stability of public consumer surfaces | [#126](https://github.com/enricopiovesan/Traverse/issues/126), [#127](https://github.com/enricopiovesan/Traverse/issues/127) | `spec`, `needs-spec` | +| Determinism of runtime updates and outcomes | [#120](https://github.com/enricopiovesan/Traverse/issues/120), [#123](https://github.com/enricopiovesan/Traverse/issues/123), [#128](https://github.com/enricopiovesan/Traverse/issues/128), [#129](https://github.com/enricopiovesan/Traverse/issues/129) | `no-spec-needed`, `needs-spec` | +| Portability across browser, CLI, and future hosts | [#126](https://github.com/enricopiovesan/Traverse/issues/126), [#129](https://github.com/enricopiovesan/Traverse/issues/129), [#109](https://github.com/enricopiovesan/Traverse/pull/109) | `spec`, `needs-spec` | +| Explainability of runtime, trace, and failures | [#123](https://github.com/enricopiovesan/Traverse/issues/123), [#128](https://github.com/enricopiovesan/Traverse/issues/128), [#129](https://github.com/enricopiovesan/Traverse/issues/129) | `no-spec-needed`, `needs-spec` | +| Performance for the first app-consumable path | [#130](https://github.com/enricopiovesan/Traverse/issues/130) | `needs-spec` | +| Reliability and repeatability of the supported flow | [#122](https://github.com/enricopiovesan/Traverse/issues/122), [#123](https://github.com/enricopiovesan/Traverse/issues/123), [#128](https://github.com/enricopiovesan/Traverse/issues/128) | `no-spec-needed`, `needs-spec` | +| Testability under CI and protected gates | [#123](https://github.com/enricopiovesan/Traverse/issues/123), [#127](https://github.com/enricopiovesan/Traverse/issues/127), [#128](https://github.com/enricopiovesan/Traverse/issues/128), [#129](https://github.com/enricopiovesan/Traverse/issues/129) | `no-spec-needed`, `needs-spec` | +| Maintainability and public/internal boundary discipline | [#126](https://github.com/enricopiovesan/Traverse/issues/126), [#127](https://github.com/enricopiovesan/Traverse/issues/127) | `spec`, `needs-spec` | +| Security and safety of browser/MCP consumer paths | [#131](https://github.com/enricopiovesan/Traverse/issues/131) | `needs-spec` | +| Documentation quality for external app consumption | [#122](https://github.com/enricopiovesan/Traverse/issues/122), [#127](https://github.com/enricopiovesan/Traverse/issues/127) | `no-spec-needed`, `needs-spec` | ## Current Open First-Release Ticket Set -- [#127](https://github.com/enricopiovesan/Traverse/issues/127) `Prepare the Traverse v0.1 release checklist for app consumers` - `In Progress` -- [#142](https://github.com/enricopiovesan/Traverse/issues/142) `Refresh README for v0.1 release-candidate state` - `In Progress` -- [#144](https://github.com/enricopiovesan/Traverse/issues/144) `Establish one canonical documentation entry path for humans and agents` - `Ready` -- [#145](https://github.com/enricopiovesan/Traverse/issues/145) `Refresh release and requirements traceability docs for current v0.1 state` - `In Progress` -- [#158](https://github.com/enricopiovesan/Traverse/issues/158) `Implement MCP stdio server package foundation` - `In Progress` -- [#179](https://github.com/enricopiovesan/Traverse/issues/179) `Validate the real browser-hosted youaskm3 shell against released Traverse consumer artifacts` - `In Progress` -- [#200](https://github.com/enricopiovesan/Traverse/issues/200) `Validate youaskm3 consumption against published Traverse runtime and MCP artifacts` - `In Progress` -- [docs/youaskm3-published-artifact-validation.md](/Users/piovese/Documents/cogolo/docs/youaskm3-published-artifact-validation.md) published-artifact validation doc -- [#150](https://github.com/enricopiovesan/Traverse/issues/150) `Prepare and validate the first Traverse v0.1 GitHub release artifact` - `Done` -- [#195](https://github.com/enricopiovesan/Traverse/issues/195) `Publish the first governed Traverse package artifact` - `In Progress` -- [#176](https://github.com/enricopiovesan/Traverse/issues/176) `Publish versioned Traverse consumer bundle for downstream app integration` - `Ready` -- [#130](https://github.com/enricopiovesan/Traverse/issues/130) `Define first app-consumable performance baseline` - `Blocked` -- [#131](https://github.com/enricopiovesan/Traverse/issues/131) `Define app-facing security and safety boundary for browser and MCP consumers` - `Blocked` +- [#120](https://github.com/enricopiovesan/Traverse/issues/120) `Implement local browser adapter transport for runtime subscriptions` - `no-spec-needed` +- [#121](https://github.com/enricopiovesan/Traverse/issues/121) `Upgrade React browser demo to consume the live local browser adapter` - `no-spec-needed` +- [#122](https://github.com/enricopiovesan/Traverse/issues/122) `Write the first app-consumable quickstart for Traverse v0.1` - `no-spec-needed` +- [#123](https://github.com/enricopiovesan/Traverse/issues/123) `Add end-to-end acceptance validation for the first app-consumable flow` - `no-spec-needed` +- [#126](https://github.com/enricopiovesan/Traverse/issues/126) `Define Traverse v0.1 downstream-consumer contract for youaskm3` - `spec` +- [#127](https://github.com/enricopiovesan/Traverse/issues/127) `Prepare the Traverse v0.1 release checklist for app consumers` - `needs-spec` +- [#128](https://github.com/enricopiovesan/Traverse/issues/128) `Validate the first real youaskm3 integration path against Traverse` - `needs-spec` +- [#129](https://github.com/enricopiovesan/Traverse/issues/129) `Validate the first app-facing MCP consumption path for downstream apps` - `needs-spec` +- [#130](https://github.com/enricopiovesan/Traverse/issues/130) `Define first app-consumable performance baseline` - `needs-spec` +- [#131](https://github.com/enricopiovesan/Traverse/issues/131) `Define app-facing security and safety boundary for browser and MCP consumers` - `needs-spec` ## v0.1 Release Ordering @@ -58,20 +61,13 @@ Project 1 is the canonical task board for this work. Every requirement area belo ### Should Have Soon After v0.1 -- [#142](https://github.com/enricopiovesan/Traverse/issues/142) README release-candidate refresh -- [#144](https://github.com/enricopiovesan/Traverse/issues/144) canonical documentation entry path -- [#145](https://github.com/enricopiovesan/Traverse/issues/145) requirements traceability refresh -- [#158](https://github.com/enricopiovesan/Traverse/issues/158) dedicated MCP stdio server package foundation -- [#150](https://github.com/enricopiovesan/Traverse/issues/150) release artifact and publication bundle -- [#195](https://github.com/enricopiovesan/Traverse/issues/195) package release pointer -- [#176](https://github.com/enricopiovesan/Traverse/issues/176) versioned consumer bundle and installation steps -- [#200](https://github.com/enricopiovesan/Traverse/issues/200) published-artifact validation against packaged runtime and MCP artifacts +- [#53](https://github.com/enricopiovesan/Traverse/issues/53) second WASM AI agent example +- [#130](https://github.com/enricopiovesan/Traverse/issues/130) app-consumable performance baseline +- [#131](https://github.com/enricopiovesan/Traverse/issues/131) app-facing security and safety boundary ### Later -- [#130](https://github.com/enricopiovesan/Traverse/issues/130) first app-consumable performance baseline -- [#131](https://github.com/enricopiovesan/Traverse/issues/131) app-facing security and safety boundary -- [#148](https://github.com/enricopiovesan/Traverse/issues/148) downstream validation for the dedicated MCP WASM server +- no additional app-consumable release tickets are intentionally placed here yet; future additions should be added only when they are outside the first release and the near-follow-up set above ## Rule diff --git a/docs/multi-thread-workflow.md b/docs/multi-thread-workflow.md index c20af16a..7f1c0244 100644 --- a/docs/multi-thread-workflow.md +++ b/docs/multi-thread-workflow.md @@ -67,6 +67,8 @@ If a ticket has an open PR, it must be labeled `in-progress` and its Project 1 i The backlog audit logic lives in [scripts/ci/project_board_audit.sh](/Users/piovese/Documents/cogolo/scripts/ci/project_board_audit.sh). +When a PR is opened or refreshed, the linked ticket should be synced automatically by the backlog-sync workflow so the issue and Project 1 row move to `In Progress`. When that PR merges, the same workflow should move the issue and Project 1 row to `Done` without waiting for a manual PM pass. + ## Required Parallel Work Rules For parallel work to be valid: diff --git a/docs/planning-board.md b/docs/planning-board.md index 49f77589..36d91c93 100644 --- a/docs/planning-board.md +++ b/docs/planning-board.md @@ -2,7 +2,16 @@ This document is the local planning view for MVP work and mirrors the active backlog in GitHub Project 1. -Status meanings: +Default operating model: + +1. Delivery lane A: work the highest-priority `Ready` Project 1 item and open one PR for it +2. Delivery lane B: work the next `Ready` Project 1 item only when its write scope is sufficiently separate +3. PR stewardship lane: keep open PRs green, fix blockers, and merge them +4. PM / PO / scrum-master lane: keep backlog tickets, labels, states, notes, and project items accurate + +These four lanes should run continuously and stay in their own scope. + +Project 1 status meanings: - `Ready`: can be implemented now under approved specs and current repo rules - `In Progress`: currently being worked on in an active issue or pull request @@ -16,110 +25,185 @@ Status meanings: ### `In Progress` -- [#66](https://github.com/enricopiovesan/Traverse/issues/66) `Codify MVP backlog completeness and ticket-quality enforcement` - - area: `quality`, `documentation` - - status: active in PR [#67](https://github.com/enricopiovesan/Traverse/pull/67) - - done when: the backlog standards, templates, planning board, and repo checks land on `main` +Only tickets with real active execution should appear in this section. -- [#158](https://github.com/enricopiovesan/Traverse/issues/158) `Implement MCP stdio server package foundation` - - area: `runtime`, `mcp` - - status: active implementation work - - done when: the dedicated stdio server boots deterministically, emits machine-readable startup/shutdown envelopes, and passes the new smoke path +- [#213](https://github.com/enricopiovesan/Traverse/issues/213) `Implement multi-instance registry federation` + - area: `runtime`, `federation` + - status: federation foundation is being implemented in [PR #240](https://github.com/enricopiovesan/Traverse/pull/240) + - done when: peer registration, manual sync, peer routing, cross-peer provenance, and audit evidence are delivered end to end -Only tickets with real active execution should appear in this section. +### `Done` -### `Ready` + `No Spec Needed` +- [#150](https://github.com/enricopiovesan/Traverse/issues/150) `Prepare and validate the first Traverse v0.1 GitHub release artifact` + - area: `documentation`, `quality` + - status: merged and closed + - done when: the release artifact definition is prepared, validated, and mapped to the documented first consumer path -- [#42](https://github.com/enricopiovesan/Traverse/issues/42) `Author expedition event contract files` - - area: `contracts` - - why ready: governed by `003`, `008`, and `009` - - done when: canonical event contract artifacts exist and validate under the approved example domain +- [#156](https://github.com/enricopiovesan/Traverse/issues/156) `Implement core MCP execution and validation operations` + - area: `runtime` + - status: merged and closed + - done when: the dedicated MCP server can validate and execute a governed entrypoint -- [#43](https://github.com/enricopiovesan/Traverse/issues/43) `Author workflow-backed composed capability contract for plan-expedition` - - area: `contracts`, `workflow` - - why ready: governed by `002`, `007`, `008`, and `009` - - done when: the composed capability contract for `plan-expedition` is authored and valid +### `Ready` -- [#44](https://github.com/enricopiovesan/Traverse/issues/44) `Author expedition atomic capability contract files` - - area: `contracts` - - why ready: governed by `002`, `008`, and `009` - - done when: all five atomic expedition capability contracts are authored and valid +- [#213](https://github.com/enricopiovesan/Traverse/issues/213) `Implement multi-instance registry federation` + - area: `runtime`, `federation` + - status: federation spec `026-federation-registry-routing` is approved and [#212](https://github.com/enricopiovesan/Traverse/issues/212) is done + - done when: peer registration, manual sync, peer routing, cross-peer provenance, and audit evidence are delivered end to end -- [#45](https://github.com/enricopiovesan/Traverse/issues/45) `Author plan-expedition workflow definition artifact` - - area: `workflow` - - why ready: governed by `007`, `008`, and `009` - - done when: the canonical workflow artifact is authored and validates against the approved workflow shape +- [#121](https://github.com/enricopiovesan/Traverse/issues/121) `Upgrade React browser demo to consume the live local browser adapter` + - area: `runtime` + - status: available now that [#120](https://github.com/enricopiovesan/Traverse/issues/120) is merged + - done when: the browser demo uses the live adapter path instead of the checked-in fixture flow + +- [#129](https://github.com/enricopiovesan/Traverse/issues/129) `Validate the first app-facing MCP consumption path for downstream apps` + - area: `runtime`, `quality` + - status: available now that [#109](https://github.com/enricopiovesan/Traverse/pull/109) and [#126](https://github.com/enricopiovesan/Traverse/issues/126) are merged + - done when: one downstream MCP-consumption path is documented and validated against public Traverse surfaces + +>>>>>>> 31a9525 (Ignore .claude/worktrees/ directory) +- [#53](https://github.com/enricopiovesan/Traverse/issues/53) `Implement second WASM AI agent example` + - area: `runtime` + - status: available now that the first agent pattern from [#54](https://github.com/enricopiovesan/Traverse/issues/54) is merged + - done when: a second distinct governed AI agent example lands with deterministic validation and runnable docs + +## Blocked Backlog ### `Blocked` + `No Spec Needed` -- [#46](https://github.com/enricopiovesan/Traverse/issues/46) `Seed expedition example registry bundle and CLI walkthrough` - - blocked by: example contracts and workflow artifacts are not authored yet - - unblock path: complete [#42](https://github.com/enricopiovesan/Traverse/issues/42), [#43](https://github.com/enricopiovesan/Traverse/issues/43), [#44](https://github.com/enricopiovesan/Traverse/issues/44), and [#45](https://github.com/enricopiovesan/Traverse/issues/45) +- [#121](https://github.com/enricopiovesan/Traverse/issues/121) `Upgrade React browser demo to consume the live local browser adapter` + - moved to `Ready` + +- [#241](https://github.com/enricopiovesan/Traverse/issues/241) `Implement manual federation sync and peer status surface` + - area: `runtime`, `federation` + - status: blocked on [#213](https://github.com/enricopiovesan/Traverse/issues/213) + - unblock path: finish the federation foundation and sync wiring first + +- [#242](https://github.com/enricopiovesan/Traverse/issues/242) `Implement remote registry validation and conflict audit evidence` + - area: `runtime`, `federation` + - status: blocked on [#241](https://github.com/enricopiovesan/Traverse/issues/241) + - unblock path: finish the peer listing and manual sync surface first + +- [#243](https://github.com/enricopiovesan/Traverse/issues/243) `Implement routed cross-peer capability invocation with trace provenance` + - area: `runtime`, `federation` + - status: blocked on [#242](https://github.com/enricopiovesan/Traverse/issues/242) + - unblock path: finish remote validation and conflict evidence first + +- [#244](https://github.com/enricopiovesan/Traverse/issues/244) `Implement public/private federation visibility enforcement` + - area: `contracts`, `federation` + - status: blocked on [#241](https://github.com/enricopiovesan/Traverse/issues/241) and [#242](https://github.com/enricopiovesan/Traverse/issues/242) + - unblock path: finish the sync validation surfaces first + +- [#214](https://github.com/enricopiovesan/Traverse/issues/214) `Implement cross-instance capability invocation` + - area: `runtime`, `placement`, `federation` + - status: blocked on [#213](https://github.com/enricopiovesan/Traverse/issues/213) + - unblock path: finish the peer federation registry sync foundation first + +- [#215](https://github.com/enricopiovesan/Traverse/issues/215) `Distributed governance and trust model` + - area: `contracts`, `federation` + - status: blocked on [#213](https://github.com/enricopiovesan/Traverse/issues/213) and [#214](https://github.com/enricopiovesan/Traverse/issues/214) + - unblock path: finish registry sync and cross-peer invocation before tightening the governance trust model + +### `Blocked` + `Future` + +- [#122](https://github.com/enricopiovesan/Traverse/issues/122) `Write the first app-consumable quickstart for Traverse v0.1` + - blocked by: [#120](https://github.com/enricopiovesan/Traverse/issues/120) and [#121](https://github.com/enricopiovesan/Traverse/issues/121) + - unblock path: document the real live setup and app-consumption flow once the live browser demo exists + +- [#123](https://github.com/enricopiovesan/Traverse/issues/123) `Add end-to-end acceptance validation for the first app-consumable flow` + - blocked by: [#121](https://github.com/enricopiovesan/Traverse/issues/121) and [#122](https://github.com/enricopiovesan/Traverse/issues/122) + - unblock path: prove the first app-consumable path end to end after the live demo and quickstart are in place + +- [#128](https://github.com/enricopiovesan/Traverse/issues/128) `Validate the first real youaskm3 integration path against Traverse` + - blocked by: [#121](https://github.com/enricopiovesan/Traverse/issues/121) and [#122](https://github.com/enricopiovesan/Traverse/issues/122) + - unblock path: complete the governed browser-consumer path and quickstart first -- [#47](https://github.com/enricopiovesan/Traverse/issues/47) `Document expedition example authoring and validation walkthrough` - - blocked by: the first concrete example artifacts and smoke path are not finished yet - - unblock path: complete [#42](https://github.com/enricopiovesan/Traverse/issues/42), [#43](https://github.com/enricopiovesan/Traverse/issues/43), [#44](https://github.com/enricopiovesan/Traverse/issues/44), [#45](https://github.com/enricopiovesan/Traverse/issues/45), and [#48](https://github.com/enricopiovesan/Traverse/issues/48) +- [#127](https://github.com/enricopiovesan/Traverse/issues/127) `Prepare the Traverse v0.1 release checklist for app consumers` + - blocked by: [#123](https://github.com/enricopiovesan/Traverse/issues/123), [#126](https://github.com/enricopiovesan/Traverse/issues/126), and [#128](https://github.com/enricopiovesan/Traverse/issues/128) + - unblock path: finish the real integration validation and MCP validation evidence before turning release readiness into a governed checklist -- [#48](https://github.com/enricopiovesan/Traverse/issues/48) `Add example artifact validation smoke path` - - blocked by: the example artifact set is not complete yet - - unblock path: complete [#42](https://github.com/enricopiovesan/Traverse/issues/42), [#43](https://github.com/enricopiovesan/Traverse/issues/43), [#44](https://github.com/enricopiovesan/Traverse/issues/44), and [#45](https://github.com/enricopiovesan/Traverse/issues/45) +- [#129](https://github.com/enricopiovesan/Traverse/issues/129) `Validate the first app-facing MCP consumption path for downstream apps` + - moved to `Ready` -## Future MVP Backlog +- [#130](https://github.com/enricopiovesan/Traverse/issues/130) `Define first app-consumable performance baseline` + - blocked by: missing governing spec for app-facing performance expectations, [#120](https://github.com/enricopiovesan/Traverse/issues/120), [#121](https://github.com/enricopiovesan/Traverse/issues/121), and [#126](https://github.com/enricopiovesan/Traverse/issues/126) + - unblock path: approve the consumer contract and derive one narrow performance-governance slice for the first supported app path -### `Needs Spec` +- [#131](https://github.com/enricopiovesan/Traverse/issues/131) `Define app-facing security and safety boundary for browser and MCP consumers` + - blocked by: missing governing spec for app-facing security and safety constraints, [#126](https://github.com/enricopiovesan/Traverse/issues/126), and [#129](https://github.com/enricopiovesan/Traverse/issues/129) + - unblock path: approve the consumer contract and formalize the first browser/MCP safety boundary before release-checklist finalization -- [#35](https://github.com/enricopiovesan/Traverse/issues/35) `Future: specify placement abstraction beyond local execution` -- [#36](https://github.com/enricopiovesan/Traverse/issues/36) `Future: specify event-driven composition slice` -- [#37](https://github.com/enricopiovesan/Traverse/issues/37) `Future: specify metadata graph model` -- [#38](https://github.com/enricopiovesan/Traverse/issues/38) `Future: specify browser runtime subscription surface` -- [#39](https://github.com/enricopiovesan/Traverse/issues/39) `Future: specify trace artifact slice` -- [#40](https://github.com/enricopiovesan/Traverse/issues/40) `Future: specify MCP surface` -- [#41](https://github.com/enricopiovesan/Traverse/issues/41) `Future: specify runtime state machine slice` -- [#49](https://github.com/enricopiovesan/Traverse/issues/49) `Future: specify AI agent execution and WASM agent packaging slice` -- [#50](https://github.com/enricopiovesan/Traverse/issues/50) `Future: specify macOS demo app slice` -- [#51](https://github.com/enricopiovesan/Traverse/issues/51) `Future: specify Android demo app slice` -- [#52](https://github.com/enricopiovesan/Traverse/issues/52) `Future: specify event registry slice` +## First App-Consumable Gap -### `Blocked` +The current strongest gap between “implemented foundations” and “first version consumable by an app” is: -- [#53](https://github.com/enricopiovesan/Traverse/issues/53) `Future: implement second WASM AI agent example` - - blocked by: [#49](https://github.com/enricopiovesan/Traverse/issues/49), [#40](https://github.com/enricopiovesan/Traverse/issues/40), and [#54](https://github.com/enricopiovesan/Traverse/issues/54) +1. switch the React browser demo to the live adapter path +2. document the quickstart flow +3. add an end-to-end acceptance path -- [#54](https://github.com/enricopiovesan/Traverse/issues/54) `Future: implement first WASM AI agent example` - - blocked by: [#49](https://github.com/enricopiovesan/Traverse/issues/49) and [#40](https://github.com/enricopiovesan/Traverse/issues/40) +This chain is tracked explicitly in [#121](https://github.com/enricopiovesan/Traverse/issues/121), [#122](https://github.com/enricopiovesan/Traverse/issues/122), and [#123](https://github.com/enricopiovesan/Traverse/issues/123). -- [#55](https://github.com/enricopiovesan/Traverse/issues/55) `Future: implement React browser demo app` - - blocked by: [#38](https://github.com/enricopiovesan/Traverse/issues/38) and the expedition example artifacts becoming runnable +## First External Consumer -- [#56](https://github.com/enricopiovesan/Traverse/issues/56) `Future: implement event registry foundation` - - blocked by: [#52](https://github.com/enricopiovesan/Traverse/issues/52) +The first real downstream consumer is [youaskm3](https://github.com/enricopiovesan/youaskm3), which expects: -- [#57](https://github.com/enricopiovesan/Traverse/issues/57) `Future: implement Android demo app` - - blocked by: [#51](https://github.com/enricopiovesan/Traverse/issues/51) +- a browser-hosted app shell +- a portable WASM/MCP-friendly runtime model +- a documented integration path rather than repo-private setup knowledge -- [#58](https://github.com/enricopiovesan/Traverse/issues/58) `Future: implement MCP surface` - - blocked by: [#40](https://github.com/enricopiovesan/Traverse/issues/40) +So the first real Traverse release is not just “a demo exists.” It must be usable by one external app through a stable, documented, app-facing surface. -- [#59](https://github.com/enricopiovesan/Traverse/issues/59) `Future: implement macOS demo app` - - blocked by: [#50](https://github.com/enricopiovesan/Traverse/issues/50) +## Missing For First Release -- [#60](https://github.com/enricopiovesan/Traverse/issues/60) `Future: implement runtime state machine` - - blocked by: [#41](https://github.com/enricopiovesan/Traverse/issues/41) +Already tracked release-critical work: -- [#61](https://github.com/enricopiovesan/Traverse/issues/61) `Future: implement browser runtime subscription surface` - - blocked by: [#38](https://github.com/enricopiovesan/Traverse/issues/38) +1. [#109](https://github.com/enricopiovesan/Traverse/pull/109) first WASM AI agent example +2. [#116](https://github.com/enricopiovesan/Traverse/pull/116) checked-in browser demo +3. [#121](https://github.com/enricopiovesan/Traverse/issues/121) live browser demo over the adapter +4. [#122](https://github.com/enricopiovesan/Traverse/issues/122) first app-consumable quickstart +5. [#123](https://github.com/enricopiovesan/Traverse/issues/123) end-to-end acceptance validation +6. [#129](https://github.com/enricopiovesan/Traverse/issues/129) app-facing MCP consumption validation -- [#62](https://github.com/enricopiovesan/Traverse/issues/62) `Future: implement metadata graph projection` - - blocked by: [#37](https://github.com/enricopiovesan/Traverse/issues/37) +Tracked consumer-release planning work: -- [#63](https://github.com/enricopiovesan/Traverse/issues/63) `Future: implement trace artifacts` - - blocked by: [#39](https://github.com/enricopiovesan/Traverse/issues/39) +1. [#126](https://github.com/enricopiovesan/Traverse/issues/126) downstream-consumer contract for `youaskm3` +2. [#128](https://github.com/enricopiovesan/Traverse/issues/128) real `youaskm3` integration validation path using only documented Traverse surfaces +3. [#127](https://github.com/enricopiovesan/Traverse/issues/127) v0.1 release checklist that distinguishes blockers from post-release work -- [#64](https://github.com/enricopiovesan/Traverse/issues/64) `Future: implement placement abstraction beyond local executor` - - blocked by: [#35](https://github.com/enricopiovesan/Traverse/issues/35) +## Merge Lane -- [#65](https://github.com/enricopiovesan/Traverse/issues/65) `Future: implement event-driven composition in runtime` - - blocked by: [#36](https://github.com/enricopiovesan/Traverse/issues/36) and [#52](https://github.com/enricopiovesan/Traverse/issues/52) +Active merge candidate: + +- none at the moment + +Rules while a merge candidate exists: + +1. merge the green candidate before starting unrelated work +2. if the candidate is green and behind `main`, update it immediately +3. do not build up a queue of "green but not merged" PRs +4. clean ticket and project state in the same pass as the merge, not hours later + +## Federation Draft + +The blocked federation chain is now governed by: + +- [specs/026-federation-registry-routing/spec.md](/Users/piovese/Documents/cogolo/specs/026-federation-registry-routing/spec.md) +- [specs/026-federation-registry-routing/data-model.md](/Users/piovese/Documents/cogolo/specs/026-federation-registry-routing/data-model.md) + +This draft covers the end-to-end peer federation slice for: + +- [#213](https://github.com/enricopiovesan/Traverse/issues/213) +- [#214](https://github.com/enricopiovesan/Traverse/issues/214) +- [#215](https://github.com/enricopiovesan/Traverse/issues/215) + +Anything beyond that first governed slice must be split into separate future tickets rather than folded into the current federation scope. + +Deferred federation extensions tracked as future tickets: + +- [#236](https://github.com/enricopiovesan/Traverse/issues/236) automatic federation sync after peer registration +- [#237](https://github.com/enricopiovesan/Traverse/issues/237) central federation coordinator +- [#238](https://github.com/enricopiovesan/Traverse/issues/238) federation conflict auto-resolution policy +- [#239](https://github.com/enricopiovesan/Traverse/issues/239) streaming federation sync transport ## Quality Rules @@ -135,11 +219,34 @@ Only tickets with real active execution should appear in this section. ## Recommended Next Sequence -1. Complete [#42](https://github.com/enricopiovesan/Traverse/issues/42), [#43](https://github.com/enricopiovesan/Traverse/issues/43), [#44](https://github.com/enricopiovesan/Traverse/issues/44), and [#45](https://github.com/enricopiovesan/Traverse/issues/45) -2. Unblock and complete [#48](https://github.com/enricopiovesan/Traverse/issues/48) -3. Unblock and complete [#46](https://github.com/enricopiovesan/Traverse/issues/46) -4. Unblock and complete [#47](https://github.com/enricopiovesan/Traverse/issues/47) -5. Then choose the next future spec slice based on MVP priority +1. Implement [#121](https://github.com/enricopiovesan/Traverse/issues/121) +2. Implement [#129](https://github.com/enricopiovesan/Traverse/issues/129) +3. Choose whether [#53](https://github.com/enricopiovesan/Traverse/issues/53) or [#122](https://github.com/enricopiovesan/Traverse/issues/122) gets the next delivery slot +4. Complete [#122](https://github.com/enricopiovesan/Traverse/issues/122) and [#123](https://github.com/enricopiovesan/Traverse/issues/123) +5. Execute [#128](https://github.com/enricopiovesan/Traverse/issues/128) +6. Finish [#127](https://github.com/enricopiovesan/Traverse/issues/127) once the real integration evidence exists + +## v0.1 Priority + +### Must Have + +- [#120](https://github.com/enricopiovesan/Traverse/issues/120) +- [#121](https://github.com/enricopiovesan/Traverse/issues/121) +- [#122](https://github.com/enricopiovesan/Traverse/issues/122) +- [#123](https://github.com/enricopiovesan/Traverse/issues/123) +- [#127](https://github.com/enricopiovesan/Traverse/issues/127) +- [#128](https://github.com/enricopiovesan/Traverse/issues/128) +- [#129](https://github.com/enricopiovesan/Traverse/issues/129) + +### Soon After + +- [#53](https://github.com/enricopiovesan/Traverse/issues/53) +- [#130](https://github.com/enricopiovesan/Traverse/issues/130) +- [#131](https://github.com/enricopiovesan/Traverse/issues/131) + +### Later + +- no additional first-consumer tickets are intentionally classified here yet ## Project 1 diff --git a/docs/pr-preflight.md b/docs/pr-preflight.md new file mode 100644 index 00000000..e34e2272 --- /dev/null +++ b/docs/pr-preflight.md @@ -0,0 +1,21 @@ +# PR Preflight + +Run this before opening a PR: + +```bash +bash scripts/ci/pr_preflight.sh /tmp/pr-body.md +``` + +The file passed to the script should contain the final PR body, including: + +- `## Governing Spec` +- `## Project Item` +- `## Validation` + +The preflight gate checks three things before a PR is opened: + +1. The branch is not behind `origin/main`. +2. The PR body passes the same body and spec-alignment gates used in CI. +3. The repository checks pass locally. + +If any of those fail, fix them before creating the PR. diff --git a/docs/project-management.md b/docs/project-management.md index 3b30a473..b7d8fb5d 100644 --- a/docs/project-management.md +++ b/docs/project-management.md @@ -88,9 +88,10 @@ The exact board columns can evolve, but the project board should remain the prim Status intent should stay simple: - Project 1 status is the only actionability signal. -- `ready` means the ticket can be started now -- `in-progress` means someone is actively working it right now -- `blocked` means work cannot continue until the blocker named in the ticket is cleared +- `Ready` means the ticket can be started now +- `In Progress` means someone is actively working it right now +- `Blocked` means work cannot continue until the blocker named in the ticket is cleared +- `Todo` should not be used for newly created work items; new tickets should be moved directly to `Ready` or `Blocked` Project 1 status is the only actionability signal. @@ -98,7 +99,7 @@ When a Project 1 item is marked `Blocked`, the project `Note` field should summa Potential parallel candidates should stay `Ready` until they are actually picked up. We should not use `In Progress` as a placeholder for work that is merely available to start. -Open PR-backed tickets must be reflected as `In Progress` in both the issue labels and Project 1. The PM thread should treat any mismatch as a board-drift bug and fix it immediately. +Open PR-backed tickets must be reflected as `In Progress` in both the issue labels and Project 1. The PM thread should treat any mismatch as a board-drift bug and fix it immediately. The backlog-sync workflow now automates the normal PR open and merge handoff so the issue and Project 1 row move to `In Progress` and then `Done` without waiting for a manual PM pass. Only tickets with real active execution should appear on Project 1 as `In Progress`. diff --git a/docs/session-notes/2026-04-09-backlog-and-federation-session.md b/docs/session-notes/2026-04-09-backlog-and-federation-session.md new file mode 100644 index 00000000..9916e8cc --- /dev/null +++ b/docs/session-notes/2026-04-09-backlog-and-federation-session.md @@ -0,0 +1,41 @@ +# Backlog and Federation Session Note + +Date: 2026-04-09 + +## Purpose + +Capture the operating model and the federation-spec direction established in this session so we do not have to rebuild the same context later. + +## Operating Model + +- Backlog steward: owns ticket state, labels, blocker notes, and Project 1 truth. +- PR steward: owns open PRs, reruns, mergeability, and merge completion. +- Delivery lanes: own one implementation ticket at a time each, and only from the actionable backlog. + +## Backlog Rules + +- A ticket with an open PR must be `In Progress`, not `Ready`. +- A closed ticket must be `Done` on the issue and on Project 1. +- A ticket with `needs-spec` must not be shown as `Ready`. +- Closed tickets must not keep stale workflow labels like `ready`, `in-progress`, or `blocked`. +- Project 1 is the actionability source of truth, but it must match the issue state and labels. + +## Federation Design Direction + +- Use an end-to-end vertical-slice approach from the beginning. +- Split future work into separate future tickets instead of widening the current spec until it becomes vague. +- Prefer the long-term solution, even if it means more than one ticket. +- Treat each spec as a real implementable slice, not a vague umbrella. +- The federation governing spec was approved as `026-federation-registry-routing`. + +## Session Emphasis + +- The user specifically wanted the backlog to stay truthful without manual chasing. +- The user also wanted federation work to be designed as a long-term end-to-end path, with extra behaviors moved into future tickets. +- The current blocked federation chain should be spec'd in a way that preserves the full long-term architecture while still being split into clean ticket-sized steps. + +## Follow-Up + +- Continue using the backlog/PR/delivery lane split. +- Keep `needs-spec` work blocked until the governing spec exists. +- Keep future architectural extensions in separate tickets instead of absorbing them into the first slice. diff --git a/docs/ticket-standard.md b/docs/ticket-standard.md index 5c540397..0f9649ca 100644 --- a/docs/ticket-standard.md +++ b/docs/ticket-standard.md @@ -41,6 +41,8 @@ Use Project 1 status for availability: - `In Progress`: currently being worked on right now - `Blocked`: cannot continue until the blocker is removed +Do not leave newly created tickets in `Todo`. If work cannot start yet, set the Project 1 item to `Blocked` and add a short blocker note. If it can start, set it to `Ready`. + Do not move work to `in-progress` just because it is a candidate for parallel execution. Use `in-progress` only when there is real active execution, typically with an active branch, PR, or an explicitly assigned developer currently working the ticket. If a ticket has an open PR, `in-progress` should remain until that PR merges or is closed. Once the PR merges, the ticket should be closed or moved out of `in-progress` in the same cleanup pass. diff --git a/references/open-source/.gitignore b/references/open-source/.gitignore new file mode 100644 index 00000000..7c9d611b --- /dev/null +++ b/references/open-source/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!README.md diff --git a/references/open-source/README.md b/references/open-source/README.md new file mode 100644 index 00000000..988bf377 --- /dev/null +++ b/references/open-source/README.md @@ -0,0 +1,14 @@ +# Open Source References + +This folder is for local checkouts of external open-source projects that we want +to study as implementation references for Traverse. + +The repo contents inside this folder are intentionally ignored so we can clone, +inspect, and delete them freely without polluting Traverse history. + +Current intended checkouts: + +- `spin` -> `https://github.com/spinframework/spin.git` +- `dapr` -> `https://github.com/dapr/dapr.git` +- `flogo-core` -> `https://github.com/project-flogo/core.git` +- `polyglot-microservices` -> `https://github.com/rodrigorodrigues/microservices-design-patterns.git` diff --git a/scripts/ci/pr_preflight.sh b/scripts/ci/pr_preflight.sh new file mode 100755 index 00000000..a64a8fbf --- /dev/null +++ b/scripts/ci/pr_preflight.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +set -euo pipefail + +body_file="${1:-}" + +if [[ -z "${body_file}" ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +test -s "${body_file}" + +git fetch origin main --quiet + +behind_count="$(git rev-list --left-right --count HEAD...origin/main | awk '{print $2}')" +if [[ "${behind_count}" != "0" ]]; then + echo "Branch is behind origin/main. Rebase before opening the PR." >&2 + exit 1 +fi + +base_sha="$(git merge-base origin/main HEAD)" + +BASE_SHA="${base_sha}" HEAD_SHA="HEAD" bash scripts/ci/pr_body_check.sh "${body_file}" +BASE_SHA="${base_sha}" HEAD_SHA="HEAD" bash scripts/ci/spec_alignment_check.sh "${body_file}" +bash scripts/ci/repository_checks.sh + +echo "PR preflight passed." diff --git a/scripts/ci/project_board_audit.sh b/scripts/ci/project_board_audit.sh old mode 100755 new mode 100644 index d6270157..578c7b8b --- a/scripts/ci/project_board_audit.sh +++ b/scripts/ci/project_board_audit.sh @@ -2,4 +2,99 @@ set -euo pipefail -"$(dirname "$0")/project_state_audit.sh" +owner="${1:-enricopiovesan}" +project_number="${2:-1}" + +tmp_json="$(mktemp)" +trap 'rm -f "$tmp_json"' EXIT + +gh api graphql -f query=" +query { + user(login: \"$owner\") { + projectV2(number: $project_number) { + items(first: 100) { + nodes { + content { + __typename + ... on Issue { + number + state + title + } + } + fieldValues(first: 20) { + nodes { + __typename + ... on ProjectV2ItemFieldSingleSelectValue { + field { + ... on ProjectV2SingleSelectField { + name + } + } + name + } + } + } + } + } + } + } +} +" > "$tmp_json" + +python3 - "$tmp_json" <<'PY' +import json +import sys + +path = sys.argv[1] +with open(path) as f: + data = json.load(f) + +items = data["data"]["user"]["projectV2"]["items"]["nodes"] +errors = [] + +for item in items: + content = item.get("content") or {} + if content.get("__typename") != "Issue": + continue + + number = content["number"] + title = content["title"] + state = content["state"] + status = None + + for field_value in item["fieldValues"]["nodes"]: + if ( + field_value.get("__typename") == "ProjectV2ItemFieldSingleSelectValue" + and field_value["field"]["name"] == "Status" + ): + status = field_value.get("name") + break + + if status is None: + errors.append(f"#{number} {title}: missing Project 1 status") + continue + + if state == "CLOSED" and status != "Done": + errors.append( + f"#{number} {title}: issue is CLOSED but Project 1 status is {status}" + ) + + if state == "OPEN" and status == "Done": + errors.append( + f"#{number} {title}: issue is OPEN but Project 1 status is Done" + ) + + if state == "OPEN" and status == "Todo": + errors.append( + f"#{number} {title}: issue is OPEN but Project 1 status is Todo; use Ready or Blocked" + ) + +if errors: + print("Project board drift detected:") + for error in errors: + print(f"- {error}") + sys.exit(1) + +print("Project board audit passed.") +PY diff --git a/scripts/ci/repository_checks.sh b/scripts/ci/repository_checks.sh index 252f5d9e..9ff6b5e8 100644 --- a/scripts/ci/repository_checks.sh +++ b/scripts/ci/repository_checks.sh @@ -77,6 +77,7 @@ required_files=( "scripts/ci/mcp_stdio_server_execution_report_smoke.sh" "scripts/ci/mcp_real_agent_exercise_smoke.sh" "scripts/ci/project_board_audit.sh" + "scripts/ci/sync_backlog_on_merge.sh" ".github/ISSUE_TEMPLATE/task.yml" "specs/001-foundation-v0-1/spec.md" "specs/001-foundation-v0-1/plan.md" @@ -94,13 +95,15 @@ for file in "${required_files[@]}"; do test -s "$file" done +stale_name_regex='C[o]g[o]l[o]|C[o]g[o]ll[o]' + if command -v rg >/dev/null 2>&1; then - if rg -n "Cogollo|Cogolo" . --hidden -g '!.git' -g '!scripts/ci/repository_checks.sh'; then + if rg -n "$stale_name_regex" . --hidden -g '!.git' -g '!scripts/ci/repository_checks.sh'; then echo "Found stale project name references; expected 'Traverse'." >&2 exit 1 fi else - if grep -RInE --exclude='repository_checks.sh' --exclude-dir='.git' 'Cogollo|Cogolo' .; then + if grep -RInE --exclude='repository_checks.sh' --exclude-dir='.git' "$stale_name_regex" .; then echo "Found stale project name references; expected 'Traverse'." >&2 exit 1 fi @@ -120,7 +123,9 @@ grep -q "in-progress" docs/project-management.md ! grep -q '^- `ready`$' docs/project-management.md grep -q 'Potential parallel candidates should stay `Ready`' docs/project-management.md grep -q "Project 1 status is the only actionability signal" docs/project-management.md +grep -q 'new tickets should be moved directly to `Ready` or `Blocked`' docs/project-management.md grep -q "project_board_audit.sh" docs/project-management.md +grep -q "backlog-sync workflow" docs/project-management.md grep -q "Note" docs/project-management.md grep -q "separate Codex threads" docs/project-management.md grep -q "Blocked" docs/planning-board.md @@ -129,9 +134,11 @@ grep -q "Only tickets with real active execution" docs/planning-board.md grep -q "Note" docs/ticket-standard.md ! grep -q '^- `ready`$' docs/ticket-standard.md grep -q "Use Project 1 status for availability" docs/ticket-standard.md +grep -q 'Do not leave newly created tickets in `Todo`' docs/ticket-standard.md grep -q "One Codex thread is one active worker" docs/multi-thread-workflow.md grep -q "Starter Prompts" docs/multi-thread-workflow.md grep -q "project_board_audit.sh" docs/multi-thread-workflow.md +grep -q "backlog-sync workflow" docs/multi-thread-workflow.md grep -q "bash scripts/ci/expedition_artifact_smoke.sh" docs/expedition-example-smoke.md grep -q "bash scripts/ci/expedition_execution_smoke.sh" docs/expedition-example-smoke.md grep -q "bash scripts/ci/expedition_trace_smoke.sh" docs/expedition-example-smoke.md @@ -288,5 +295,7 @@ grep -q "Dedicated Traverse MCP WASM Server Model" specs/022-mcp-wasm-server/spe grep -q "Traverse runtime authority" specs/022-mcp-wasm-server/spec.md grep -q "MCP transport concerns" specs/022-mcp-wasm-server/spec.md grep -q "## Governing Spec" .github/pull_request_template.md +test -f .github/workflows/backlog-sync.yml +grep -q "Backlog Sync" .github/workflows/backlog-sync.yml echo "Repository checks passed." diff --git a/scripts/ci/sync_backlog_on_merge.sh b/scripts/ci/sync_backlog_on_merge.sh new file mode 100644 index 00000000..a5701073 --- /dev/null +++ b/scripts/ci/sync_backlog_on_merge.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo="${TRAVERSE_REPO:-enricopiovesan/Traverse}" +project_owner="${PROJECT_OWNER:-enricopiovesan}" +project_number="${PROJECT_NUMBER:-1}" +pr_number="${PR_NUMBER:-}" +pr_event_action="${PR_EVENT_ACTION:-}" +pr_merged="${PR_MERGED:-false}" + +if [[ -z "$pr_number" ]]; then + echo "No PR_NUMBER provided; skipping backlog sync." >&2 + exit 0 +fi + +if [[ "$pr_event_action" == "closed" && "$pr_merged" != "true" ]]; then + echo "PR #$pr_number was closed without merge; no backlog sync performed." >&2 + exit 0 +fi + +if [[ "$pr_event_action" != "closed" && "$pr_event_action" != "opened" && "$pr_event_action" != "reopened" && "$pr_event_action" != "synchronize" && "$pr_event_action" != "ready_for_review" ]]; then + echo "PR #$pr_number action '$pr_event_action' does not require backlog sync." >&2 + exit 0 +fi + +pr_json=$(gh pr view "$pr_number" --repo "$repo" --json body,closingIssuesReferences,title,mergedAt) +body=$(jq -r '.body // ""' <<<"$pr_json") + +mapfile -t issue_numbers < <( + jq -r '.closingIssuesReferences[]?.number // empty' <<<"$pr_json" | sort -n -u +) + +if [[ "${#issue_numbers[@]}" -eq 0 ]]; then + mapfile -t issue_numbers < <( + grep -Eoi '(Closes|Fixes|Resolves|Implements|issue)[[:space:]]*#[0-9]+' <<<"$body" \ + | grep -Eo '[0-9]+' \ + | sort -n -u + ) +fi + +if [[ "${#issue_numbers[@]}" -eq 0 ]]; then + project_item_line=$( + awk ' + BEGIN { in_section = 0 } + /^## Project Item$/ { in_section = 1; next } + /^## [^#]/ && in_section { exit } + in_section && NF { print; exit } + ' <<<"$body" + ) + + project_item_line="${project_item_line#- }" + project_item_line="${project_item_line#Project 1 item: }" + project_item_line="${project_item_line#Project Item: }" + project_item_line="${project_item_line#GitHub Project 1 item for issue #}" + + if [[ "$project_item_line" =~ ^[0-9]+$ ]]; then + issue_numbers+=("$project_item_line") + elif [[ -n "$project_item_line" ]]; then + mapfile -t issue_numbers < <( + gh issue list --repo "$repo" --state all --limit 200 --json number,title \ + | jq -r --arg title "$project_item_line" '.[] | select(.title == $title) | .number' \ + | sort -n -u + ) + fi +fi + +if [[ "${#issue_numbers[@]}" -eq 0 ]]; then + echo "No linked issue found for PR #$pr_number; nothing to sync." >&2 + exit 0 +fi + +project_json=$(gh project view "$project_number" --owner "$project_owner" --format json) +project_id=$(jq -r '.id' <<<"$project_json") +field_json=$(gh project field-list "$project_number" --owner "$project_owner" --format json) +status_field_id=$(jq -r '.fields[] | select(.name == "Status") | .id' <<<"$field_json") +done_option_id=$(jq -r '.fields[] | select(.name == "Status") | .options[] | select(.name == "Done") | .id' <<<"$field_json") +note_field_id=$(jq -r '.fields[] | select(.name == "Note") | .id' <<<"$field_json") +project_items_json=$(gh project item-list "$project_number" --owner "$project_owner" --format json --limit 200) + +workflow_labels=(in-progress blocked ready future needs-spec) + +for issue_number in "${issue_numbers[@]}"; do + issue_json=$(gh issue view "$issue_number" --repo "$repo" --json state,labels,title) + issue_state=$(jq -r '.state' <<<"$issue_json") + issue_labels=$(jq -r '.labels[].name' <<<"$issue_json" 2>/dev/null || true) + + remove_args=() + for label in "${workflow_labels[@]}"; do + if grep -qx "$label" <<<"$issue_labels"; then + remove_args+=(--remove-label "$label") + fi + done + + if [[ "$pr_merged" == "true" ]]; then + if [[ "${#remove_args[@]}" -gt 0 ]]; then + gh issue edit "$issue_number" --repo "$repo" "${remove_args[@]}" + fi + + if [[ "$issue_state" == "OPEN" ]]; then + gh issue close "$issue_number" --repo "$repo" + fi + else + remove_args+=(--add-label in-progress) + if [[ "${#remove_args[@]}" -gt 0 ]]; then + gh issue edit "$issue_number" --repo "$repo" "${remove_args[@]}" + fi + fi + + item_id=$(jq -r --argjson n "$issue_number" '.items[] | select(.content.number == $n) | .id' <<<"$project_items_json" | head -n1) + if [[ -z "${item_id:-}" || "$item_id" == "null" ]]; then + echo "Issue #$issue_number has no Project 1 row; skipping project sync." >&2 + continue + fi + + current_status=$(jq -r --argjson n "$issue_number" '.items[] | select(.content.number == $n) | .status' <<<"$project_items_json" | head -n1) + + if [[ "$pr_merged" == "true" ]]; then + if [[ "$current_status" != "Done" ]]; then + gh project item-edit \ + --project-id "$project_id" \ + --id "$item_id" \ + --field-id "$status_field_id" \ + --single-select-option-id "$done_option_id" + fi + + gh project item-edit \ + --project-id "$project_id" \ + --id "$item_id" \ + --field-id "$note_field_id" \ + --text "Done: merged in PR #$pr_number." + else + if [[ "$current_status" != "In Progress" ]]; then + gh project item-edit \ + --project-id "$project_id" \ + --id "$item_id" \ + --field-id "$status_field_id" \ + --single-select-option-id "$(jq -r '.fields[] | select(.name == \"Status\") | .options[] | select(.name == \"In Progress\") | .id' <<<"$field_json")" + fi + + gh project item-edit \ + --project-id "$project_id" \ + --id "$item_id" \ + --field-id "$note_field_id" \ + --text "In progress: PR #$pr_number is open." + fi +done + +if [[ "$pr_merged" == "true" ]]; then + echo "Backlog sync passed for merged PR #$pr_number." +else + echo "Backlog sync passed for active PR #$pr_number." +fi diff --git a/specs/022-mcp-wasm-server-model/data-model.md b/specs/022-mcp-wasm-server-model/data-model.md new file mode 100644 index 00000000..4c237cbd --- /dev/null +++ b/specs/022-mcp-wasm-server-model/data-model.md @@ -0,0 +1,237 @@ +# Data Model: MCP WASM Server Model + +## Purpose + +This document defines the implementation-tight artifacts for the `022-mcp-wasm-server-model` slice. + +It governs the first dedicated Traverse MCP server package, its supported host model, its core and convenience operations, and its runtime-authoritative execution mapping. + +## 1. MCP Server Package Record + +Represents one dedicated Traverse MCP server package. + +### Required Fields + +- `server_id` +- `package_kind` +- `supported_host_modes` +- `runtime_authority` +- `exposed_operation_sets` +- `supported_artifact_classes` + +### Shape + +```json +{ + "server_id": "traverse.mcp.server.v0", + "package_kind": "dedicated_traverse_mcp_server", + "supported_host_modes": [ + "stdio" + ], + "runtime_authority": "traverse_runtime", + "exposed_operation_sets": [ + "core", + "convenience" + ], + "supported_artifact_classes": [ + "capability_contract", + "workflow_artifact", + "wasm_agent_package" + ] +} +``` + +### Rules + +- `supported_host_modes` for the first slice must contain `stdio`. +- `runtime_authority` must resolve to Traverse runtime authority; no alternate execution authority is valid in v0.1. + +## 2. MCP Core Operation Record + +Represents one Traverse-native MCP operation. + +### Required Fields + +- `operation_id` +- `surface_kind` +- `subject_kind` +- `runtime_mapping` +- `returns` + +### Shape + +```json +{ + "operation_id": "run_workflow", + "surface_kind": "core", + "subject_kind": "workflow_backed_capability", + "runtime_mapping": "translate_to_runtime_request_and_execute", + "returns": "machine_readable_terminal_result" +} +``` + +### Rules + +- `surface_kind` must be `core`. +- `subject_kind` must be one of: + - `capability` + - `workflow` + - `workflow_backed_capability` + - `trace` + - `terminal_result` +- `runtime_mapping` must remain explicit; direct business-logic execution is invalid. + +## 3. MCP Convenience Operation Record + +Represents one generic workflow-oriented convenience operation. + +### Required Fields + +- `operation_id` +- `surface_kind` +- `entrypoint_subject` +- `delegates_to` +- `returns` + +### Shape + +```json +{ + "operation_id": "run_entrypoint", + "surface_kind": "convenience", + "entrypoint_subject": "governed_entrypoint_record", + "delegates_to": [ + "describe_workflow", + "run_workflow" + ], + "returns": "machine_readable_terminal_result" +} +``` + +### Rules + +- `surface_kind` must be `convenience`. +- Convenience operations must delegate to one or more core operations. +- Convenience operations must remain generic and must not encode domain-specific scenario ids or chapter-specific terminology. + +## 4. MCP Entrypoint Record + +Represents one invocable governed entrypoint exposed through the dedicated server. + +### Required Fields + +- `entrypoint_id` +- `entrypoint_kind` +- `target_id` +- `target_version` +- `input_contract_ref` +- `output_contract_ref` + +### Shape + +```json +{ + "entrypoint_id": "expedition.planning.plan-expedition", + "entrypoint_kind": "workflow_backed_capability", + "target_id": "expedition.planning.plan-expedition", + "target_version": "1.0.0", + "input_contract_ref": "contracts/examples/expedition/capabilities/plan-expedition/contract.json#input", + "output_contract_ref": "contracts/examples/expedition/capabilities/plan-expedition/contract.json#output" +} +``` + +### Rules + +- `entrypoint_kind` must be one of: + - `capability` + - `workflow_backed_capability` +- Entry points must resolve to governed registered artifacts, not ad hoc demo handlers. + +## 5. Runtime Delegation Record + +Represents how one MCP operation delegates to Traverse runtime authority. + +### Required Fields + +- `delegation_id` +- `mcp_operation_id` +- `request_translation_rule` +- `validation_phase` +- `terminal_artifacts` + +### Shape + +```json +{ + "delegation_id": "run_workflow_to_runtime_request", + "mcp_operation_id": "run_workflow", + "request_translation_rule": "map_mcp_input_to_governed_runtime_request", + "validation_phase": "pre_execution_runtime_validation", + "terminal_artifacts": [ + "terminal_result", + "trace_ref" + ] +} +``` + +### Rules + +- `validation_phase` must occur before execution and must remain runtime-owned. +- `terminal_artifacts` may include rendered forms, but the underlying artifacts must remain runtime-derived. + +## 6. MCP Rendered Artifact Record + +Represents one structured artifact returned by a rendering-oriented MCP operation. + +### Required Fields + +- `artifact_id` +- `source_kind` +- `render_kind` +- `machine_readable` + +### Shape + +```json +{ + "artifact_id": "execution_report", + "source_kind": "trace_or_terminal_result", + "render_kind": "structured_report", + "machine_readable": true +} +``` + +### Rules + +- `source_kind` must derive from governed runtime outputs, not private server-only state. +- `machine_readable` must be `true` for the first dedicated server slice. + +## 7. WASM Exposure Record + +Represents one governed WASM-hosted capability or agent exposed through the server. + +### Required Fields + +- `exposure_id` +- `manifest_ref` +- `governed_target_id` +- `binary_format` +- `exception_policy` + +### Shape + +```json +{ + "exposure_id": "expedition_intent_agent_stdio_exposure", + "manifest_ref": "examples/agents/expedition-intent-agent/manifest.json", + "governed_target_id": "expedition.planning.interpret-expedition-intent", + "binary_format": "wasm", + "exception_policy": "no_host_api_network_or_filesystem_exceptions" +} +``` + +### Rules + +- `binary_format` for this slice must be `wasm`. +- `governed_target_id` must resolve to an approved governed capability or workflow-backed target. +- The server must not expose a WASM package whose manifest violates approved exception constraints. diff --git a/specs/022-mcp-wasm-server-model/spec.md b/specs/022-mcp-wasm-server-model/spec.md new file mode 100644 index 00000000..ba48be62 --- /dev/null +++ b/specs/022-mcp-wasm-server-model/spec.md @@ -0,0 +1,158 @@ +# Feature Specification: MCP WASM Server Model + +**Feature Branch**: `022-mcp-wasm-server-model` +**Created**: 2026-04-06 +**Status**: Draft +**Input**: The agreed direction that Traverse needs a dedicated MCP server product surface for downstream consumers such as `youaskm3`, using the useful patterns from UMA Chapter 13 while keeping Traverse runtime authority, governed artifacts, and product-shaped APIs. + +## Purpose + +This specification defines the first dedicated MCP WASM server model for Traverse. + +It narrows the broader MCP and downstream-consumer goals into one explicit product model for: + +- a dedicated Traverse MCP server package +- a first supported `stdio` host mode +- Traverse-native MCP operations plus a generic workflow-oriented convenience layer +- runtime-authoritative capability and workflow execution behind the MCP façade +- governed WASM-hosted capability and agent participation under approved contracts and manifests + +This slice exists so Traverse can expose MCP as a first-class product surface rather than only as a foundation concept, chapter reference, or app-specific integration trick. + +This slice does **not** define browser transport, HTTP/SSE MCP hosting, UI behavior, or federation behavior. It is intentionally focused on the first dedicated, local-first, stdio-hosted MCP server shape. + +## User Scenarios and Testing + +### User Story 1 - Discover and Invoke Governed Traverse Surfaces Through MCP (Priority: P1) + +As a downstream developer, I want one dedicated Traverse MCP server so that I can discover governed capabilities and workflows and invoke them through MCP without reimplementing Traverse runtime behavior. + +**Why this priority**: Your chosen architecture makes Traverse the runtime and MCP substrate under apps like `youaskm3`, so MCP must become a usable product surface rather than a theoretical future. + +**Independent Test**: Start the server in the first supported host mode, list the governed entrypoints it exposes, invoke at least one governed execution path, and verify the result is produced through runtime-authoritative behavior. + +**Acceptance Scenarios**: + +1. **Given** a local downstream client connects through the supported MCP host mode, **When** it lists available Traverse entrypoints, **Then** it receives a deterministic machine-readable description of the governed MCP surface. +2. **Given** a client invokes one governed capability or workflow-backed capability, **When** execution begins, **Then** the server delegates to Traverse runtime authority instead of bypassing runtime validation. +3. **Given** a requested execution fails validation or runtime checks, **When** the client inspects the MCP result, **Then** the failure remains structured and explainable. + +### User Story 2 - Expose a Rich but Product-Shaped MCP Surface (Priority: P1) + +As a downstream app or agent-tool developer, I want a richer MCP surface than bare discovery so that Traverse can support real product consumption rather than only low-level metadata inspection. + +**Why this priority**: The UMA Chapter 13 reference app proved that a richer MCP surface is more useful in practice, but Traverse needs that richness without inheriting chapter-specific semantics. + +**Independent Test**: Inspect the governed MCP toolset and verify that it includes Traverse-native discovery and execution plus a generic workflow-oriented convenience layer, without hard-coding expedition or chapter-specific scenario vocabulary into the core. + +**Acceptance Scenarios**: + +1. **Given** a client needs basic product operations, **When** it inspects the core MCP surface, **Then** it can list and describe capabilities and workflows, run them, and render execution artifacts. +2. **Given** a client wants a friendlier workflow-oriented surface, **When** it inspects the convenience layer, **Then** it sees generic entrypoint-oriented operations rather than chapter-specific scenario language. +3. **Given** the expedition example remains the first proving domain, **When** the client uses the convenience layer, **Then** the surface stays product-generic rather than domain-locked. + +### User Story 3 - Keep Traverse Runtime Authoritative Behind MCP (Priority: P1) + +As a platform steward, I want the MCP server to remain a thin façade over Traverse runtime authority so that validation, state transitions, trace semantics, and execution policy do not drift behind an MCP-specific execution path. + +**Why this priority**: The strongest architectural lesson from the UMA runtime model is that the runtime, not the transport surface, stays authoritative. + +**Independent Test**: Review one governed MCP execution path and verify that request validation, candidate selection, execution, runtime-state behavior, and terminal results still flow through the Traverse runtime model. + +**Acceptance Scenarios**: + +1. **Given** a client invokes MCP execution, **When** the server handles the request, **Then** it translates the request into governed Traverse runtime execution rather than directly running business logic. +2. **Given** the runtime emits terminal results and trace outputs, **When** the MCP server returns or renders those artifacts, **Then** it does not redefine their semantics. +3. **Given** future host modes are added, **When** they are proposed, **Then** this slice still preserves runtime-authoritative execution as a non-negotiable boundary. + +## Scope + +In scope: + +- first dedicated Traverse MCP server model +- first supported `stdio` host mode +- Traverse-native MCP discovery and execution operations +- generic workflow-oriented convenience operations +- MCP exposure of governed capability and workflow-backed execution +- WASM-hosted capability and agent participation under Traverse governance +- structured report and trace rendering through MCP-facing operations + +Out of scope: + +- browser transport or browser-native MCP hosting +- HTTP/SSE or remote network hosting in v0.1 +- domain-specific scenario APIs modeled after UMA chapters +- federation across multiple MCP servers +- auth, identity, or multi-tenant deployment policy + +## Functional Requirements + +- **FR-001**: Traverse MUST define one dedicated MCP server model as a first-class product surface. +- **FR-002**: The first supported MCP host mode MUST be `stdio`. +- **FR-003**: This slice MUST explicitly allow future host modes later without making them part of the first implementation requirement. +- **FR-004**: The MCP server MUST remain a thin façade over Traverse runtime execution. +- **FR-005**: MCP requests that trigger execution MUST be translated into governed Traverse runtime requests rather than directly executing business logic inside the server. +- **FR-006**: The MCP server MUST expose Traverse-native core operations for discovery and description of governed capabilities and workflows. +- **FR-007**: The MCP server MUST expose Traverse-native execution operations for both direct capability execution and workflow-backed capability execution. +- **FR-008**: The MCP server MUST expose a generic workflow-oriented convenience layer for entrypoint-style discovery, description, execution, and execution-artifact rendering. +- **FR-009**: The convenience layer MUST remain product-generic and MUST NOT hard-code chapter- or domain-specific scenario semantics into the core server contract. +- **FR-010**: The first dedicated MCP server MUST support exposure of governed WASM-hosted capabilities or agents through approved manifests and contracts. +- **FR-011**: The MCP server MUST preserve governed validation before execution begins. +- **FR-012**: The MCP server MUST preserve machine-readable runtime failure behavior when validation or execution fails. +- **FR-013**: The MCP server MUST support rendering one structured execution artifact, such as a trace-derived or terminal-result-derived report, through the MCP surface. +- **FR-014**: The MCP server MUST expose enough machine-readable metadata for a downstream client to identify what is invocable and what input shape it expects. +- **FR-015**: The MCP server MUST remain aligned with the downstream-consumer contract and MCP validation slices rather than redefining them separately. +- **FR-016**: The first server implementation MUST be broad enough to feel product-usable and MUST NOT stop at discovery-only metadata inspection. +- **FR-017**: Approved implementation under this slice MUST be decomposable into multiple production-grade tickets with independent validation, not one unreviewable monolith. +- **FR-018**: Approved implementation and validation under this slice MUST be checked against this governing spec before merge. + +## Non-Functional Requirements + +- **NFR-001 Runtime Authority**: Validation, execution, state transitions, and terminal semantics MUST remain runtime-authoritative behind the MCP façade. +- **NFR-002 Determinism**: Discovery ordering, entrypoint description, execution behavior, and failure reporting MUST remain deterministic for equivalent inputs and registry state. +- **NFR-003 Product Boundary**: The first dedicated MCP server MUST stay product-shaped and reusable, not chapter-shaped or demo-specific. +- **NFR-004 Explainability**: MCP-visible outputs for execution, failure, and rendered artifacts MUST remain explainable without private internal logs. +- **NFR-005 Testability**: The dedicated server model MUST be specifiable and implementable in slices that support deterministic local and CI validation. +- **NFR-006 Portability**: The server model MUST preserve portability of governed WASM-hosted capabilities and agents across supported Traverse hosts, even though the first server host mode is `stdio`. +- **NFR-007 Maintainability**: Core Traverse-native operations and convenience-layer operations MUST remain separable so future surface growth does not pollute the core server boundary. + +## Non-Negotiable Quality Gates + +- **QG-001**: Traverse MUST NOT present the first dedicated MCP server as authoritative for execution logic; Traverse runtime remains authoritative. +- **QG-002**: The first dedicated MCP server MUST NOT depend on undocumented app-specific glue or private repo semantics. +- **QG-003**: The first dedicated MCP server MUST expose more than discovery-only metadata before it is considered release-usable. +- **QG-004**: The first convenience layer MUST remain generic and MUST NOT encode expedition-only or chapter-only scenario semantics into the core server contract. +- **QG-005**: The first server implementation MUST be decomposed into multiple production-grade slices with explicit validation paths. + +## Key Entities + +- **Traverse MCP Server**: The dedicated server package that exposes governed Traverse surfaces through MCP. +- **Core MCP Operation**: One Traverse-native discovery, description, or execution operation directly aligned with capabilities, workflows, traces, or terminal results. +- **Convenience MCP Operation**: One generic workflow-oriented MCP operation that presents a friendlier entrypoint-shaped façade over core Traverse semantics. +- **MCP Entrypoint Record**: One machine-readable description of an invocable governed capability or workflow-backed capability exposed through the server. +- **MCP Rendered Artifact**: One structured report or trace-derived representation returned by an MCP operation without redefining the underlying runtime semantics. + +## Success Criteria + +- **SC-001**: Traverse has one explicit governed model for a dedicated MCP server package rather than only MCP foundation pieces. +- **SC-002**: A downstream client can understand the first server host mode, core operations, convenience operations, and runtime-authoritative boundary from this spec alone. +- **SC-003**: The first implementation can be split into multiple production-grade tickets without guessing the product surface. +- **SC-004**: The Traverse MCP server reuses the useful lessons of UMA Chapter 13 while remaining a product surface for Traverse itself rather than a transplanted chapter artifact. + +## Governing Relationship + +This specification is governed by: + +- `001-foundation-v0-1` +- `006-runtime-request-execution` +- `010-runtime-state-machine` +- `019-downstream-consumer-contract` +- `020-downstream-integration-validation` +- constitution version `1.2.0` + +This specification is intended to govern future implementation and validation in: + +- dedicated Traverse MCP server packaging +- dedicated MCP discovery and execution operations +- convenience entrypoint-oriented MCP operations +- downstream validation of the dedicated Traverse MCP server diff --git a/specs/023-packaged-runtime-artifact/spec.md b/specs/023-packaged-runtime-artifact/spec.md new file mode 100644 index 00000000..b1bf2d67 --- /dev/null +++ b/specs/023-packaged-runtime-artifact/spec.md @@ -0,0 +1,24 @@ +# Spec 023: Packaged Runtime Artifact + +**Feature Branch**: `023-packaged-runtime-artifact` +**Created**: 2026-04-09 +**Status**: approved + +## Summary + +Define the governed runtime release artifact used by downstream consumers. + +## User Story + +As a Traverse consumer, I want a packaged runtime artifact that can be validated and consumed downstream so I can install and execute the governed runtime without rebuilding the repository. + +## Requirements + +- The runtime artifact must be reproducible from the approved repository state. +- The runtime artifact must expose clear metadata for version, build provenance, and validation evidence. +- The runtime artifact must support downstream validation through the approved CI and traceability path. + +## Non-Goals + +- No new runtime behavior beyond packaging and release metadata. +- No ad hoc artifact shapes outside the governed release contract. diff --git a/specs/024-packaged-mcp-server-artifact/spec.md b/specs/024-packaged-mcp-server-artifact/spec.md new file mode 100644 index 00000000..54f7e5eb --- /dev/null +++ b/specs/024-packaged-mcp-server-artifact/spec.md @@ -0,0 +1,24 @@ +# Spec 024: Packaged MCP Server Artifact + +**Feature Branch**: `024-packaged-mcp-server-artifact` +**Created**: 2026-04-09 +**Status**: approved + +## Summary + +Define the governed packaged MCP server artifact for downstream consumption. + +## User Story + +As a Traverse consumer, I want a packaged MCP server artifact that I can validate and run downstream so that the server surface is consumable without guessing about build outputs. + +## Requirements + +- The MCP server artifact must preserve the approved entrypoint and runtime contracts. +- The artifact must be suitable for downstream validation and smoke testing. +- The artifact must include the metadata needed to explain provenance and supported usage. + +## Non-Goals + +- No transport redesign in this slice. +- No new MCP protocol surface beyond the governed package contract. diff --git a/specs/025-downstream-published-artifact-validation/spec.md b/specs/025-downstream-published-artifact-validation/spec.md new file mode 100644 index 00000000..66313249 --- /dev/null +++ b/specs/025-downstream-published-artifact-validation/spec.md @@ -0,0 +1,24 @@ +# Spec 025: Downstream Published Artifact Validation + +**Feature Branch**: `025-downstream-published-artifact-validation` +**Created**: 2026-04-09 +**Status**: approved + +## Summary + +Define the validation rules for published artifacts consumed by downstream projects. + +## User Story + +As a release steward, I want published artifacts to be validated before downstream use so that consumers only receive governed, auditable, and runnable outputs. + +## Requirements + +- Published artifacts must be validated against the approved spec registry. +- Validation must produce deterministic evidence for success and failure. +- Validation must cover the runtime artifact, MCP server artifact, and any downstream smoke requirements. + +## Non-Goals + +- No new packaging format in this slice. +- No downstream-specific business logic beyond validation and evidence. diff --git a/specs/026-federation-registry-routing/checklists/requirements.md b/specs/026-federation-registry-routing/checklists/requirements.md new file mode 100644 index 00000000..41d64ac9 --- /dev/null +++ b/specs/026-federation-registry-routing/checklists/requirements.md @@ -0,0 +1,34 @@ +# Specification Quality Checklist: Federated Traverse registry, routing, and trust model + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-04-09 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [ ] No implementation details (languages, frameworks, APIs) +- [ ] Focused on user value and business needs +- [ ] Written for non-technical stakeholders +- [ ] All mandatory sections completed + +## Requirement Completeness + +- [ ] No [NEEDS CLARIFICATION] markers remain +- [ ] Requirements are testable and unambiguous +- [ ] Success criteria are measurable +- [ ] Success criteria are technology-agnostic (no implementation details) +- [ ] All acceptance scenarios are defined +- [ ] Edge cases are identified +- [ ] Scope is clearly bounded +- [ ] Dependencies and assumptions identified + +## Feature Readiness + +- [ ] All functional requirements have clear acceptance criteria +- [ ] User scenarios cover primary flows +- [ ] Feature meets measurable outcomes defined in Success Criteria +- [ ] No implementation details leak into specification + +## Notes + +- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan` diff --git a/specs/026-federation-registry-routing/data-model.md b/specs/026-federation-registry-routing/data-model.md new file mode 100644 index 00000000..2e0d01b9 --- /dev/null +++ b/specs/026-federation-registry-routing/data-model.md @@ -0,0 +1,134 @@ +# Data Model: Federated Traverse registry, routing, and trust model + +## FederationPeer + +Represents a trusted Traverse instance participating in federation. + +### Fields + +- `peer_id`: stable peer identifier +- `display_name`: human-readable peer label +- `trust_state`: trusted, blocked, pending, or revoked +- `identity_fingerprint`: certificate or trust-anchor fingerprint +- `sync_enabled`: whether the peer participates in sync +- `last_sync_at`: timestamp of the most recent sync +- `last_sync_status`: success, partial, failed, or unknown +- `visible_registry_scopes`: which registry scopes this peer may see + +## TrustRecord + +Represents the approved relationship that allows a peer to participate in federation. + +### Fields + +- `peer_id`: referenced FederationPeer +- `trust_model`: how the peer is trusted +- `allowed_scopes`: scopes or registry classes the peer may access +- `approved_spec_refs`: governing spec references that authorize trust +- `approved_at`: timestamp of approval +- `revoked_at`: optional timestamp if trust is revoked + +## FederationSyncSession + +Represents one manual sync attempt. + +### Fields + +- `session_id`: unique sync session identifier +- `peer_id`: peer being synced +- `started_at`: sync start time +- `finished_at`: sync end time +- `status`: success, partial, failed +- `registry_types`: capability, event, workflow, or a subset +- `validated_entries`: number of accepted entries +- `rejected_entries`: number of rejected entries +- `conflict_count`: number of conflicts detected +- `evidence_ref`: pointer to sync evidence or audit artifact + +## PeerRegistrySnapshot + +Represents the remote registry state accepted from a peer. + +### Fields + +- `peer_id`: source peer +- `registry_type`: capability, event, or workflow +- `entry_id`: stable registry entry id +- `version`: semver or equivalent version identifier +- `scope`: public or private +- `approval_state`: approved, draft, deprecated, or rejected +- `contract_ref`: reference to the governing contract artifact +- `provenance_ref`: origin metadata for the remote entry + +## FederatedInvocation + +Represents a routed request sent to the owning peer. + +### Fields + +- `invocation_id`: stable routed invocation id +- `origin_peer_id`: peer initiating the invocation +- `target_peer_id`: owning peer +- `capability_id`: requested capability +- `request_ref`: governing request or payload reference +- `status`: success, failure, retryable_failure +- `response_ref`: response or failure payload reference +- `trace_provenance_ref`: provenance for the routed trace + +## ConflictRecord + +Represents a detected divergence between peers. + +### Fields + +- `conflict_id`: stable conflict identifier +- `peer_ids`: peers involved in the divergence +- `registry_type`: capability, event, or workflow +- `entry_key`: conflicting scope/id/version key +- `conflict_reason`: human-readable explanation +- `resolution_state`: open, resolved, escalated +- `audit_ref`: evidence pointer for review + +## CrossPeerTraceProvenance + +Represents the audit trail for a routed invocation across peers. + +### Fields + +- `trace_id`: governing trace id +- `origin_peer_id`: peer that initiated the request +- `owning_peer_id`: peer that executed the request +- `route_reason`: why the owning peer was selected +- `sync_session_ref`: federation sync evidence ref if relevant +- `response_status`: success or failure outcome +- `evidence_ref`: reviewable evidence pointer + +## FederationStatusSummary + +Represents the operator-facing summary for federation health. + +### Fields + +- `peer_count`: number of trusted peers +- `last_sync_outcome`: latest sync result +- `sync_age`: age since last successful sync +- `conflict_count`: current unresolved conflicts +- `blocked_entries`: number of entries rejected by trust or spec validation +- `route_failures`: number of failed cross-peer invocations + +## Relationships + +- A `FederationPeer` can have many `TrustRecord` entries. +- A `FederationPeer` can have many `FederationSyncSession` records. +- A `FederationSyncSession` can produce many `PeerRegistrySnapshot` entries. +- A `FederatedInvocation` references exactly one origin peer and one owning peer. +- A `ConflictRecord` can reference many peers and many rejected registry entries. +- A `CrossPeerTraceProvenance` record can be attached to a `FederatedInvocation`. + +## Validation Rules + +- A peer without a valid `TrustRecord` cannot participate in federation. +- A `PeerRegistrySnapshot` must match an approved spec or be rejected. +- A `FederatedInvocation` must always identify the owning peer. +- A `ConflictRecord` must retain an audit reference. +- A `CrossPeerTraceProvenance` record must not omit origin and owning peer ids. diff --git a/specs/026-federation-registry-routing/spec.md b/specs/026-federation-registry-routing/spec.md new file mode 100644 index 00000000..7332f6da --- /dev/null +++ b/specs/026-federation-registry-routing/spec.md @@ -0,0 +1,151 @@ +# Feature Specification: Federated Traverse registry, routing, and trust model + +**Feature Branch**: `026-federation-registry-routing` +**Created**: 2026-04-09 +**Status**: Approved +**Input**: User description: "Create the governing federation spec from the long-term end-to-end decisions, with future work split into separate tickets." + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Register and Sync Trusted Peers (Priority: P1) + +As a Traverse operator, I want to register trusted peers and manually sync registry state so that distributed Traverse instances can discover each other and share governed registry entries without a central coordinator. + +**Why this priority**: Peer registration and sync are the foundation of federation; nothing else in the federation chain is useful if peers cannot establish trusted shared state. + +**Independent Test**: A reviewer can verify that a peer can be added, listed, synced on demand, and reported as healthy or unhealthy without needing to inspect implementation code. + +**Acceptance Scenarios**: + +1. **Given** two trusted Traverse peers, **When** one peer is registered and a manual sync is run, **Then** the local instance records the remote peer, sync outcome, and the discovered registry entries. +2. **Given** a remote entry fails trust or spec validation, **When** sync runs, **Then** the entry is rejected and the sync evidence reports the reason. + +### User Story 2 - Discover and Route Cross-Peer Invocations (Priority: P1) + +As a downstream consumer or agent, I want a capability discovered on a remote peer to be invoked through the owning peer so that federation is useful as a real end-to-end execution path, not just a registry mirror. + +**Why this priority**: Federation must deliver a real cross-peer call path; otherwise it only proves discovery and state replication. + +**Independent Test**: A reviewer can verify that a discovered remote capability is routed to its owning peer, that the result or failure is returned, and that trace provenance is preserved. + +**Acceptance Scenarios**: + +1. **Given** a capability is registered on peer B and synced to peer A, **When** peer A invokes that capability, **Then** the request is routed to peer B and the response is returned with cross-peer provenance. +2. **Given** the owning peer is unavailable, **When** peer A invokes the remote capability, **Then** the failure is explicit, retryable if policy allows, and the trace records the reason. + +### User Story 3 - Trust, Visibility, and Auditability Across Peers (Priority: P2) + +As a federation steward, I want remote registry entries to be validated against local approved specs and tracked with audit evidence so that federation remains governable and explainable over time. + +**Why this priority**: The federation model needs a durable trust story and reviewable evidence so the system can grow without becoming opaque. + +**Independent Test**: A reviewer can confirm that remote entries are validated before acceptance, peer visibility is exposed, and conflicts or rejections produce audit evidence. + +**Acceptance Scenarios**: + +1. **Given** a remote registry entry from a trusted peer, **When** it is synced, **Then** the entry is validated against the local approved spec registry before acceptance. +2. **Given** a sync detects a divergent or conflicting entry, **When** the sync completes, **Then** the conflict is reported with audit evidence instead of being silently hidden. +3. **Given** an operator inspects federation state, **When** they query peers and sync status, **Then** the current peer list, sync outcomes, and trust state are visible through the supported operator surface. + +## Scope + +In scope: + +- peer-to-peer federation with no central coordinator in the first governed slice +- trusted peer registration and peer identity +- manual on-demand synchronization of registry state +- federated discovery for capability, event, and workflow registries +- public/private visibility rules across trusted peers, with explicit authorization for private entries +- routed cross-peer invocation to the owning peer +- explicit trust and validation of remote entries against local approved specs +- peer listing, sync status, and audit evidence for operators and agents +- cross-peer trace provenance for routed invocations +- explicit conflict reporting when sync detects divergence +- a governed first transport path that can be implemented and validated without guessing at the federation boundary + +Out of scope: + +- a central federation coordinator +- automatic background sync after registration +- streaming sync transport +- gossip-based replication +- load-balanced invocation to any eligible peer +- transport protocols beyond the first governed peer-sync path + +## Edge Cases + +- A peer presents a registry entry that is valid structurally but not approved locally. +- A remote peer is reachable for discovery but unavailable for invocation. +- Two peers present the same `(scope, id, version)` entry with different provenance. +- A peer is trusted for discovery but not trusted for private capability visibility. +- A sync completes successfully for some registry types but not others. +- An invocation is routed successfully but the remote peer returns a structured failure. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: Traverse MUST support trusted peer registration for federation. +- **FR-002**: Traverse MUST support manual on-demand synchronization of registry state between peers. +- **FR-003**: Traverse MUST expose peer listing and sync status to operators and agents. +- **FR-004**: Traverse MUST validate remote registry entries against the local approved spec registry before accepting them. +- **FR-005**: Traverse MUST federate capability, event, and workflow registry entries through the first governed sync path. +- **FR-006**: Traverse MUST route a discovered remote capability invocation to the owning peer. +- **FR-007**: Traverse MUST preserve cross-peer trace provenance for routed invocations. +- **FR-008**: Traverse MUST produce explicit failure evidence when the owning peer is unavailable. +- **FR-009**: Traverse MUST report conflicts or divergent registry entries with audit evidence. +- **FR-010**: Traverse MUST preserve public/private visibility rules across peers and deny unauthorized access to private registry entries. +- **FR-011**: Approved implementation under this slice MUST be checked against the governing federation spec before merge. +- **FR-012**: Future federation extensions beyond this slice MUST be split into separate tickets rather than expanded in place. + +### Key Entities *(include if feature involves data)* + +- **FederationPeer**: A trusted Traverse instance participating in federation, identified by peer identity and trust metadata. +- **PeerRegistrySnapshot**: A synchronized view of the capability, event, and workflow registry entries received from a peer. +- **FederationSyncSession**: A single manual sync operation with outcome, timing, and validation evidence. +- **FederatedInvocation**: A routed request sent to the owning peer and the resulting response or failure. +- **TrustRecord**: The approved relationship that allows a peer to participate in federation. +- **ConflictRecord**: Audit evidence that records divergent or conflicting registry entries. +- **CrossPeerTraceProvenance**: Evidence showing which peer owned the invocation and how the result returned. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: A reviewer can register a peer, run a manual sync, and verify the peer and sync outcome without reading implementation code. +- **SC-002**: A discovered remote capability can be invoked through the owning peer and return a recorded response or explicit failure. +- **SC-003**: Remote entries are accepted only after local approved-spec validation, and rejected entries produce reviewable evidence. +- **SC-004**: Peer list, sync status, and conflict evidence are visible through the supported operator surface. +- **SC-005**: Cross-peer provenance is present in routed invocation evidence and can be reviewed after the call completes. + +## Assumptions + +- The first governed federation slice is peer-to-peer and does not require a central coordinator. +- The first supported sync path is manual and on-demand. +- The first supported transport path is explicit and reviewable rather than inferred from internal implementation structure. +- Future improvements such as automatic sync, streaming updates, and central coordination will be tracked as separate tickets. +- The federation model must stay compatible with the approved spec registry and the existing runtime/tracing model. + +## Governing Relationship + +This specification is intended to govern the following open issues: + +- #213 Implement multi-instance registry federation +- #214 Implement cross-instance capability invocation +- #215 Distributed governance and trust model + +This specification is governed by: + +- `001-foundation-v0-1` +- `019-downstream-consumer-contract` +- `020-downstream-integration-validation` +- `021-app-facing-operational-constraints` + +This specification also preserves the current bounded future-work model: + +- [#236](https://github.com/enricopiovesan/Traverse/issues/236) automatic federation sync after peer registration +- [#237](https://github.com/enricopiovesan/Traverse/issues/237) central federation coordinator +- [#238](https://github.com/enricopiovesan/Traverse/issues/238) federation conflict auto-resolution policy +- [#239](https://github.com/enricopiovesan/Traverse/issues/239) streaming federation sync transport + +coordinator behavior, automatic sync, streaming updates, and other nonessential federation extensions remain future tickets diff --git a/specs/governance/approved-specs.json b/specs/governance/approved-specs.json index 5d1aa2e8..9b0f182b 100644 --- a/specs/governance/approved-specs.json +++ b/specs/governance/approved-specs.json @@ -47,12 +47,13 @@ "status": "approved", "immutable": true, "path": "specs/004-spec-alignment-gate/spec.md", - "governs": [ - ".github/workflows/ci.yml", - "scripts/ci/", - "specs/governance/" - ] - }, + "governs": [ + ".github/workflows/", + ".github/workflows/ci.yml", + "scripts/ci/", + "specs/governance/" + ] + }, { "id": "005-capability-registry", "version": "1.0.0", @@ -169,6 +170,49 @@ "docs/project-management.md" ] }, + { + "id": "020-downstream-integration-validation", + "version": "1.0.0", + "status": "approved", + "immutable": true, + "path": "specs/020-downstream-integration-validation/spec.md", + "governs": [ + "quickstart.md", + "docs/", + "scripts/ci/", + "apps/" + ] + }, + { + "id": "021-app-facing-operational-constraints", + "version": "1.0.0", + "status": "approved", + "immutable": true, + "path": "specs/021-app-facing-operational-constraints/spec.md", + "governs": [ + "docs/", + "scripts/ci/", + "crates/cogolo-mcp/", + "crates/traverse-mcp/", + "crates/cogolo-runtime/", + "crates/traverse-runtime/" + ] + }, + { + "id": "022-mcp-wasm-server-model", + "version": "1.0.0", + "status": "approved", + "immutable": true, + "path": "specs/022-mcp-wasm-server-model/spec.md", + "governs": [ + "crates/cogolo-mcp/", + "crates/traverse-mcp/", + "crates/cogolo-cli/", + "crates/traverse-cli/", + "examples/agents/", + "examples/expedition/" + ] + }, { "id": "017-ai-agent-packaging", "version": "1.0.0", @@ -295,6 +339,55 @@ "scripts/ci/", "specs/governance/" ] + }, + { + "id": "023-packaged-runtime-artifact", + "version": "1.0.0", + "status": "approved", + "immutable": true, + "path": "specs/023-packaged-runtime-artifact/spec.md", + "governs": [ + "docs/app-consumable-release-checklist.md", + "docs/youaskm3-integration-validation.md", + "docs/app-consumable-requirements-traceability.md", + "scripts/ci/" + ] + }, + { + "id": "024-packaged-mcp-server-artifact", + "version": "1.0.0", + "status": "approved", + "immutable": true, + "path": "specs/024-packaged-mcp-server-artifact/spec.md", + "governs": [ + "docs/mcp-consumption-validation.md", + "docs/youaskm3-integration-validation.md", + "scripts/ci/" + ] + }, + { + "id": "025-downstream-published-artifact-validation", + "version": "1.0.0", + "status": "approved", + "immutable": true, + "path": "specs/025-downstream-published-artifact-validation/spec.md", + "governs": [ + "docs/youaskm3-integration-validation.md", + "docs/mcp-consumption-validation.md", + "scripts/ci/" + ] + }, + { + "id": "026-federation-registry-routing", + "version": "1.0.0", + "status": "approved", + "immutable": true, + "path": "specs/026-federation-registry-routing/spec.md", + "governs": [ + "docs/planning-board.md", + "docs/project-management.md", + "docs/session-notes/" + ] } ] }