Skip to main content

riffdb_types/
service.rs

1//! Closed service-audit vocabulary and canonical target collection.
2
3use std::error::Error;
4use std::fmt;
5
6use crate::limits::MAX_SERVICE_AUDIT_TARGETS;
7use crate::{
8    AdministrationSequence, CapabilityId, CommandId, CommitSequence, ContractLineage,
9    ContractVersion, EntityTypeId, EventConsumerIdentityHash, IndexId, ProjectionId, ProvenanceId,
10    ReactiveModuleHash, ReactiveOperationName, ReplicationFollowerAuditTargetV1,
11};
12
13/// The closed v1 application-service operation registry.
14#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
15pub enum ServiceOperationV1 {
16    /// Validate contract source without deploying it.
17    ValidateContract,
18    /// Explain one compiled command.
19    ExplainCommand,
20    /// Deploy one contract version.
21    DeployContract,
22    /// Read the active contract metadata.
23    GetActiveContract,
24    /// Read one contract version.
25    GetContractVersion,
26    /// Execute one classified command.
27    ExecuteCommand,
28    /// Resolve a previously submitted command outcome.
29    ResolveCommandOutcome,
30    /// Read one entity.
31    GetEntity,
32    /// Scan one index.
33    ScanIndex,
34    /// Query one derived projection.
35    QueryProjection,
36    /// Read one projection's status.
37    GetProjectionStatus,
38    /// Read one application commit.
39    GetCommit,
40    /// Scan application commits.
41    ScanCommits,
42    /// Subscribe to application commits.
43    SubscribeToCommits,
44    /// Trace one provenance selector.
45    TraceProvenance,
46    /// Read server health.
47    GetHealth,
48    /// Read server statistics.
49    GetStatistics,
50    /// Create or bootstrap a capability.
51    CreateCapability,
52    /// Revoke a capability.
53    RevokeCapability,
54    /// List pending outbox deliveries.
55    ListPendingOutboxDeliveries,
56    /// Discover policy-visible command tools.
57    DiscoverCommandTools,
58    /// Discover policy-visible resources.
59    DiscoverResources,
60    /// Describe one exact symbolic contract catalog.
61    DescribeContract,
62    /// Parse, resolve, type-check, and plan one symbolic query.
63    CheckQuery,
64    /// Return one bounded symbolic query plan explanation.
65    ExplainQuery,
66    /// Execute one closed symbolic query.
67    ExecuteQuery,
68    /// Compile and atomically activate one immutable query module.
69    DeployQueryModule,
70    /// Apply one accepted offline contract migration.
71    ApplyContractMigration,
72    /// Describe one active symbolic domain event.
73    DescribeEvent,
74    /// Replay one bounded upper-fenced symbolic event page.
75    ReplayEvents,
76    /// Wait for and return one bounded symbolic event page.
77    TailEvents,
78    /// Execute one projected columnar query under a freshness policy.
79    ExecuteProjectedQuery,
80    /// Publish one exact immutable reactive module.
81    DeployReactiveModule,
82    /// Pull or stream leased events from one exact symbolic stream.
83    ConsumeEventStream,
84    /// Acknowledge one exact leased event attempt.
85    AcknowledgeEventStream,
86    /// Negatively acknowledge one exact leased event attempt.
87    NegativeAcknowledgeEventStream,
88    /// Administratively seek one exact durable event consumer.
89    SeekEventStreamConsumer,
90    /// Administratively retire one exact durable event consumer.
91    RetireEventStreamConsumer,
92    /// Read one exact durable event consumer status.
93    GetEventStreamConsumerStatus,
94    /// Establish and advance one exact live named-query watch.
95    WatchNamedQuery,
96    /// Pull one bounded contextual work batch.
97    ConsumeContextualSubscription,
98    /// Acknowledge one exact contextual work lease.
99    AcknowledgeContextualSubscription,
100    /// Negatively acknowledge one exact contextual work lease.
101    NegativeAcknowledgeContextualSubscription,
102    /// Read one exact contextual consumer status.
103    GetContextualSubscriptionStatus,
104    /// Execute one causally fenced contextual reaction.
105    ExecuteContextualReaction,
106    /// Read one opaque reactive wakeup generation.
107    GetReactiveWakeup,
108    /// Start or resume one exact application installation campaign.
109    StartApplicationInstallation,
110    /// Observe one exact application installation campaign.
111    GetApplicationInstallation,
112    /// Start or exactly replay one snapshot-bound symbolic application export.
113    StartApplicationExport,
114    /// Release one bounded symbolic application-export page.
115    GetApplicationExportPage,
116    /// Observe one protected application-export checkpoint or receipt.
117    GetApplicationExport,
118    /// Close one nonterminal application export with an incomplete receipt.
119    CancelApplicationExport,
120    /// Start or exactly resume one not-ready application reimport campaign.
121    StartApplicationReimport,
122    /// Apply one exact bounded export page through compiler-owned commands.
123    ApplyApplicationReimportPage,
124    /// Observe one protected application-reimport checkpoint or receipt.
125    GetApplicationReimport,
126    /// Close one nonterminal application reimport without publishing readiness.
127    CancelApplicationReimport,
128    /// Inspect one exact compiler-declared production vector field.
129    InspectVectorState,
130    /// Register one lineage-scoped follower retention policy.
131    RegisterFollower,
132    /// Retire one exact follower registration generation.
133    RetireFollower,
134    /// Durably fence one primary for an exact registered follower generation.
135    FenceReplicationPrimary,
136    /// Establish an audited administrative changelog replication stream.
137    StreamChangelog,
138    /// Promote one drained follower under authenticated source fencing.
139    PromoteFollower,
140}
141
142impl ServiceOperationV1 {
143    /// Every accepted v1 service operation, in tag order.
144    pub const ALL: [Self; 62] = [
145        Self::ValidateContract,
146        Self::ExplainCommand,
147        Self::DeployContract,
148        Self::GetActiveContract,
149        Self::GetContractVersion,
150        Self::ExecuteCommand,
151        Self::ResolveCommandOutcome,
152        Self::GetEntity,
153        Self::ScanIndex,
154        Self::QueryProjection,
155        Self::GetProjectionStatus,
156        Self::GetCommit,
157        Self::ScanCommits,
158        Self::SubscribeToCommits,
159        Self::TraceProvenance,
160        Self::GetHealth,
161        Self::GetStatistics,
162        Self::CreateCapability,
163        Self::RevokeCapability,
164        Self::ListPendingOutboxDeliveries,
165        Self::DiscoverCommandTools,
166        Self::DiscoverResources,
167        Self::DescribeContract,
168        Self::CheckQuery,
169        Self::ExplainQuery,
170        Self::ExecuteQuery,
171        Self::DeployQueryModule,
172        Self::ApplyContractMigration,
173        Self::DescribeEvent,
174        Self::ReplayEvents,
175        Self::TailEvents,
176        Self::ExecuteProjectedQuery,
177        Self::DeployReactiveModule,
178        Self::ConsumeEventStream,
179        Self::AcknowledgeEventStream,
180        Self::NegativeAcknowledgeEventStream,
181        Self::SeekEventStreamConsumer,
182        Self::RetireEventStreamConsumer,
183        Self::GetEventStreamConsumerStatus,
184        Self::WatchNamedQuery,
185        Self::ConsumeContextualSubscription,
186        Self::AcknowledgeContextualSubscription,
187        Self::NegativeAcknowledgeContextualSubscription,
188        Self::GetContextualSubscriptionStatus,
189        Self::ExecuteContextualReaction,
190        Self::GetReactiveWakeup,
191        Self::StartApplicationInstallation,
192        Self::GetApplicationInstallation,
193        Self::StartApplicationExport,
194        Self::GetApplicationExportPage,
195        Self::GetApplicationExport,
196        Self::CancelApplicationExport,
197        Self::StartApplicationReimport,
198        Self::ApplyApplicationReimportPage,
199        Self::GetApplicationReimport,
200        Self::CancelApplicationReimport,
201        Self::InspectVectorState,
202        Self::RegisterFollower,
203        Self::RetireFollower,
204        Self::FenceReplicationPrimary,
205        Self::StreamChangelog,
206        Self::PromoteFollower,
207    ];
208
209    /// Returns the stable v1 semantic tag.
210    #[must_use]
211    pub const fn tag(self) -> u8 {
212        match self {
213            Self::ValidateContract => 0x01,
214            Self::ExplainCommand => 0x02,
215            Self::DeployContract => 0x03,
216            Self::GetActiveContract => 0x04,
217            Self::GetContractVersion => 0x05,
218            Self::ExecuteCommand => 0x06,
219            Self::ResolveCommandOutcome => 0x07,
220            Self::GetEntity => 0x08,
221            Self::ScanIndex => 0x09,
222            Self::QueryProjection => 0x0a,
223            Self::GetProjectionStatus => 0x0b,
224            Self::GetCommit => 0x0c,
225            Self::ScanCommits => 0x0d,
226            Self::SubscribeToCommits => 0x0e,
227            Self::TraceProvenance => 0x0f,
228            Self::GetHealth => 0x10,
229            Self::GetStatistics => 0x11,
230            Self::CreateCapability => 0x12,
231            Self::RevokeCapability => 0x13,
232            Self::ListPendingOutboxDeliveries => 0x14,
233            Self::DiscoverCommandTools => 0x15,
234            Self::DiscoverResources => 0x16,
235            Self::DescribeContract => 0x17,
236            Self::CheckQuery => 0x18,
237            Self::ExplainQuery => 0x19,
238            Self::ExecuteQuery => 0x1a,
239            Self::DeployQueryModule => 0x1b,
240            Self::ApplyContractMigration => 0x1c,
241            Self::DescribeEvent => 0x1d,
242            Self::ReplayEvents => 0x1e,
243            Self::TailEvents => 0x1f,
244            Self::ExecuteProjectedQuery => 0x20,
245            Self::DeployReactiveModule => 0x21,
246            Self::ConsumeEventStream => 0x22,
247            Self::AcknowledgeEventStream => 0x23,
248            Self::NegativeAcknowledgeEventStream => 0x24,
249            Self::SeekEventStreamConsumer => 0x25,
250            Self::RetireEventStreamConsumer => 0x26,
251            Self::GetEventStreamConsumerStatus => 0x27,
252            Self::WatchNamedQuery => 0x28,
253            Self::ConsumeContextualSubscription => 0x29,
254            Self::AcknowledgeContextualSubscription => 0x2a,
255            Self::NegativeAcknowledgeContextualSubscription => 0x2b,
256            Self::GetContextualSubscriptionStatus => 0x2c,
257            Self::ExecuteContextualReaction => 0x2d,
258            Self::GetReactiveWakeup => 0x2e,
259            Self::StartApplicationInstallation => 0x2f,
260            Self::GetApplicationInstallation => 0x30,
261            Self::StartApplicationExport => 0x31,
262            Self::GetApplicationExportPage => 0x32,
263            Self::GetApplicationExport => 0x33,
264            Self::CancelApplicationExport => 0x34,
265            Self::StartApplicationReimport => 0x35,
266            Self::ApplyApplicationReimportPage => 0x36,
267            Self::GetApplicationReimport => 0x37,
268            Self::CancelApplicationReimport => 0x38,
269            Self::InspectVectorState => 0x39,
270            Self::RegisterFollower => 0x3a,
271            Self::RetireFollower => 0x3b,
272            Self::FenceReplicationPrimary => 0x3c,
273            Self::StreamChangelog => 0x3d,
274            Self::PromoteFollower => 0x3e,
275        }
276    }
277
278    /// Decodes a stable v1 semantic tag, rejecting zero and unknown values.
279    #[must_use]
280    pub const fn from_tag(tag: u8) -> Option<Self> {
281        match tag {
282            0x01 => Some(Self::ValidateContract),
283            0x02 => Some(Self::ExplainCommand),
284            0x03 => Some(Self::DeployContract),
285            0x04 => Some(Self::GetActiveContract),
286            0x05 => Some(Self::GetContractVersion),
287            0x06 => Some(Self::ExecuteCommand),
288            0x07 => Some(Self::ResolveCommandOutcome),
289            0x08 => Some(Self::GetEntity),
290            0x09 => Some(Self::ScanIndex),
291            0x0a => Some(Self::QueryProjection),
292            0x0b => Some(Self::GetProjectionStatus),
293            0x0c => Some(Self::GetCommit),
294            0x0d => Some(Self::ScanCommits),
295            0x0e => Some(Self::SubscribeToCommits),
296            0x0f => Some(Self::TraceProvenance),
297            0x10 => Some(Self::GetHealth),
298            0x11 => Some(Self::GetStatistics),
299            0x12 => Some(Self::CreateCapability),
300            0x13 => Some(Self::RevokeCapability),
301            0x14 => Some(Self::ListPendingOutboxDeliveries),
302            0x15 => Some(Self::DiscoverCommandTools),
303            0x16 => Some(Self::DiscoverResources),
304            0x17 => Some(Self::DescribeContract),
305            0x18 => Some(Self::CheckQuery),
306            0x19 => Some(Self::ExplainQuery),
307            0x1a => Some(Self::ExecuteQuery),
308            0x1b => Some(Self::DeployQueryModule),
309            0x1c => Some(Self::ApplyContractMigration),
310            0x1d => Some(Self::DescribeEvent),
311            0x1e => Some(Self::ReplayEvents),
312            0x1f => Some(Self::TailEvents),
313            0x20 => Some(Self::ExecuteProjectedQuery),
314            0x21 => Some(Self::DeployReactiveModule),
315            0x22 => Some(Self::ConsumeEventStream),
316            0x23 => Some(Self::AcknowledgeEventStream),
317            0x24 => Some(Self::NegativeAcknowledgeEventStream),
318            0x25 => Some(Self::SeekEventStreamConsumer),
319            0x26 => Some(Self::RetireEventStreamConsumer),
320            0x27 => Some(Self::GetEventStreamConsumerStatus),
321            0x28 => Some(Self::WatchNamedQuery),
322            0x29 => Some(Self::ConsumeContextualSubscription),
323            0x2a => Some(Self::AcknowledgeContextualSubscription),
324            0x2b => Some(Self::NegativeAcknowledgeContextualSubscription),
325            0x2c => Some(Self::GetContextualSubscriptionStatus),
326            0x2d => Some(Self::ExecuteContextualReaction),
327            0x2e => Some(Self::GetReactiveWakeup),
328            0x2f => Some(Self::StartApplicationInstallation),
329            0x30 => Some(Self::GetApplicationInstallation),
330            0x31 => Some(Self::StartApplicationExport),
331            0x32 => Some(Self::GetApplicationExportPage),
332            0x33 => Some(Self::GetApplicationExport),
333            0x34 => Some(Self::CancelApplicationExport),
334            0x35 => Some(Self::StartApplicationReimport),
335            0x36 => Some(Self::ApplyApplicationReimportPage),
336            0x37 => Some(Self::GetApplicationReimport),
337            0x38 => Some(Self::CancelApplicationReimport),
338            0x39 => Some(Self::InspectVectorState),
339            0x3a => Some(Self::RegisterFollower),
340            0x3b => Some(Self::RetireFollower),
341            0x3c => Some(Self::FenceReplicationPrimary),
342            0x3d => Some(Self::StreamChangelog),
343            0x3e => Some(Self::PromoteFollower),
344            _ => None,
345        }
346    }
347}
348
349/// The lifecycle phase of one durable service-audit record.
350#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
351pub enum ServiceAuditPhaseV1 {
352    /// Protected work has been durably admitted for this invocation.
353    Started,
354    /// A safe result or protected output is ready for release.
355    Succeeded,
356    /// Current policy denied the invocation.
357    Denied,
358    /// Work was cancelled without a known authoritative result or released output.
359    Cancelled,
360    /// Work failed without a known authoritative application or control-plane result.
361    Failed,
362    /// The authoritative command outcome is uncertain.
363    OutcomeUncertain,
364}
365
366impl ServiceAuditPhaseV1 {
367    /// Every accepted v1 audit phase, in tag order.
368    pub const ALL: [Self; 6] = [
369        Self::Started,
370        Self::Succeeded,
371        Self::Denied,
372        Self::Cancelled,
373        Self::Failed,
374        Self::OutcomeUncertain,
375    ];
376
377    /// Returns the stable v1 semantic tag.
378    #[must_use]
379    pub const fn tag(self) -> u8 {
380        match self {
381            Self::Started => 0x01,
382            Self::Succeeded => 0x02,
383            Self::Denied => 0x03,
384            Self::Cancelled => 0x04,
385            Self::Failed => 0x05,
386            Self::OutcomeUncertain => 0x06,
387        }
388    }
389
390    /// Decodes a stable v1 semantic tag, rejecting zero and unknown values.
391    #[must_use]
392    pub const fn from_tag(tag: u8) -> Option<Self> {
393        match tag {
394            0x01 => Some(Self::Started),
395            0x02 => Some(Self::Succeeded),
396            0x03 => Some(Self::Denied),
397            0x04 => Some(Self::Cancelled),
398            0x05 => Some(Self::Failed),
399            0x06 => Some(Self::OutcomeUncertain),
400            _ => None,
401        }
402    }
403}
404
405/// The trusted ingress by which an application-service invocation arrived.
406#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
407pub enum ServiceIngressKindV1 {
408    /// Public gRPC, including CLI, SDK, and production MCP stdio clients.
409    Grpc,
410    /// Hosted MCP Streamable HTTP.
411    McpHttp,
412    /// The in-process test and comparison application surface.
413    InProcessTestComparison,
414}
415
416impl ServiceIngressKindV1 {
417    /// Every accepted v1 ingress kind, in tag order.
418    pub const ALL: [Self; 3] = [Self::Grpc, Self::McpHttp, Self::InProcessTestComparison];
419
420    /// Returns the stable v1 semantic tag.
421    #[must_use]
422    pub const fn tag(self) -> u8 {
423        match self {
424            Self::Grpc => 0x01,
425            Self::McpHttp => 0x02,
426            Self::InProcessTestComparison => 0x03,
427        }
428    }
429
430    /// Decodes a stable v1 semantic tag, rejecting zero and unknown values.
431    #[must_use]
432    pub const fn from_tag(tag: u8) -> Option<Self> {
433        match tag {
434            0x01 => Some(Self::Grpc),
435            0x02 => Some(Self::McpHttp),
436            0x03 => Some(Self::InProcessTestComparison),
437            _ => None,
438        }
439    }
440}
441
442/// The closed authoritative-result link carried by a service-audit record.
443#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
444pub enum ServiceAuditLinkV1 {
445    /// No authoritative application or control-plane result is known.
446    None,
447    /// A known committed command result, including a terminal replay.
448    Command {
449        /// The original application commit sequence.
450        commit_sequence: CommitSequence,
451        /// The original durable provenance identity.
452        provenance_id: ProvenanceId,
453    },
454    /// A known control-plane mutation result.
455    ControlPlane {
456        /// The administration sequence assigned to the result.
457        administration_sequence: AdministrationSequence,
458    },
459}
460
461/// A bounded stable object reference addressed by a service invocation.
462#[derive(Clone, Eq, Hash, PartialEq)]
463pub enum ServiceAuditTargetV1 {
464    /// One exact contract lineage.
465    ContractLineage(ContractLineage),
466    /// One version scoped to its exact contract lineage.
467    ContractVersion {
468        /// Exact contract lineage.
469        lineage: ContractLineage,
470        /// Nonzero contract version.
471        version: ContractVersion,
472    },
473    /// One entity type scoped to its exact contract lineage.
474    EntityType {
475        /// Exact contract lineage.
476        lineage: ContractLineage,
477        /// Nonzero compiler-assigned entity type identity.
478        entity_type_id: EntityTypeId,
479    },
480    /// One command scoped to its exact contract lineage.
481    Command {
482        /// Exact contract lineage.
483        lineage: ContractLineage,
484        /// Nonzero compiler-assigned command identity.
485        command_id: CommandId,
486    },
487    /// One projection scoped to its exact contract lineage.
488    Projection {
489        /// Exact contract lineage.
490        lineage: ContractLineage,
491        /// Nonzero compiler-assigned projection identity.
492        projection_id: ProjectionId,
493    },
494    /// One index scoped to its exact contract lineage.
495    Index {
496        /// Exact contract lineage.
497        lineage: ContractLineage,
498        /// Nonzero compiler-assigned index identity.
499        index_id: IndexId,
500    },
501    /// One application commit.
502    Commit(CommitSequence),
503    /// One durable provenance record.
504    Provenance(ProvenanceId),
505    /// One authorization capability.
506    Capability(CapabilityId),
507    /// One exact immutable reactive operation and durable consumer identity.
508    EventConsumer {
509        /// Exact contract lineage.
510        lineage: ContractLineage,
511        /// Immutable reactive module identity.
512        module_hash: ReactiveModuleHash,
513        /// Exact stream operation name.
514        operation_name: ReactiveOperationName,
515        /// Domain-separated complete consumer identity.
516        consumer_identity_hash: EventConsumerIdentityHash,
517    },
518    /// One follower registration scoped to its exact source lineage.
519    ReplicationFollower(ReplicationFollowerAuditTargetV1),
520}
521
522impl ServiceAuditTargetV1 {
523    /// Returns the stable ADR-0021 semantic tag.
524    #[must_use]
525    pub const fn tag(&self) -> u8 {
526        match self {
527            Self::ContractLineage(_) => 0x01,
528            Self::ContractVersion { .. } => 0x02,
529            Self::EntityType { .. } => 0x03,
530            Self::Command { .. } => 0x04,
531            Self::Projection { .. } => 0x05,
532            Self::Index { .. } => 0x06,
533            Self::Commit(_) => 0x07,
534            Self::Provenance(_) => 0x08,
535            Self::Capability(_) => 0x09,
536            Self::EventConsumer { .. } => 0x0a,
537            Self::ReplicationFollower(_) => 0x0b,
538        }
539    }
540
541    /// Constructs the immutable ADR-0021 canonical comparison key.
542    #[must_use]
543    pub fn canonical_key(&self) -> Vec<u8> {
544        let mut key = Vec::new();
545        key.push(self.tag());
546        match self {
547            Self::ContractLineage(lineage) => append_lineage(&mut key, lineage),
548            Self::ContractVersion { lineage, version } => {
549                append_lineage(&mut key, lineage);
550                key.extend_from_slice(&version.to_be_bytes());
551            }
552            Self::EntityType {
553                lineage,
554                entity_type_id,
555            } => {
556                append_lineage(&mut key, lineage);
557                key.extend_from_slice(&entity_type_id.to_be_bytes());
558            }
559            Self::Command {
560                lineage,
561                command_id,
562            } => {
563                append_lineage(&mut key, lineage);
564                key.extend_from_slice(&command_id.to_be_bytes());
565            }
566            Self::Projection {
567                lineage,
568                projection_id,
569            } => {
570                append_lineage(&mut key, lineage);
571                key.extend_from_slice(&projection_id.to_be_bytes());
572            }
573            Self::Index { lineage, index_id } => {
574                append_lineage(&mut key, lineage);
575                key.extend_from_slice(&index_id.to_be_bytes());
576            }
577            Self::Commit(sequence) => key.extend_from_slice(&sequence.to_be_bytes()),
578            Self::Provenance(provenance_id) => key.extend_from_slice(provenance_id.as_bytes()),
579            Self::Capability(capability_id) => key.extend_from_slice(capability_id.as_bytes()),
580            Self::ReplicationFollower(target) => {
581                key.extend_from_slice(target.database_id().as_bytes());
582                key.extend_from_slice(&target.history_incarnation().to_be_bytes());
583                key.extend_from_slice(&target.leadership_epoch().get().to_be_bytes());
584                key.extend_from_slice(target.hold_id().as_bytes());
585            }
586            Self::EventConsumer {
587                lineage,
588                module_hash,
589                operation_name,
590                consumer_identity_hash,
591            } => {
592                append_lineage(&mut key, lineage);
593                key.extend_from_slice(module_hash.as_bytes());
594                let operation = operation_name.as_str().as_bytes();
595                key.extend_from_slice(&(operation.len() as u32).to_be_bytes());
596                key.extend_from_slice(operation);
597                key.extend_from_slice(consumer_identity_hash.as_bytes());
598            }
599        }
600        key
601    }
602}
603
604impl fmt::Debug for ServiceAuditTargetV1 {
605    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
606        let variant = match self {
607            Self::ContractLineage(_) => "ContractLineage",
608            Self::ContractVersion { .. } => "ContractVersion",
609            Self::EntityType { .. } => "EntityType",
610            Self::Command { .. } => "Command",
611            Self::Projection { .. } => "Projection",
612            Self::Index { .. } => "Index",
613            Self::Commit(_) => "Commit",
614            Self::Provenance(_) => "Provenance",
615            Self::Capability(_) => "Capability",
616            Self::EventConsumer { .. } => "EventConsumer",
617            Self::ReplicationFollower(_) => "ReplicationFollower",
618        };
619        write!(formatter, "ServiceAuditTargetV1::{variant}([REDACTED])")
620    }
621}
622
623fn append_lineage(key: &mut Vec<u8>, lineage: &ContractLineage) {
624    let lineage_bytes = lineage.as_bytes();
625    // `ContractLineage` validation fixes this length at no more than 256 bytes.
626    key.extend_from_slice(&(lineage_bytes.len() as u32).to_be_bytes());
627    key.extend_from_slice(lineage_bytes);
628}
629
630/// A safe validation failure for a service-audit target collection.
631#[derive(Clone, Copy, Debug, Eq, PartialEq)]
632pub enum ServiceAuditTargetsError {
633    /// The collection contains more than the accepted v1 maximum.
634    TooMany {
635        /// The accepted maximum number of targets.
636        maximum: usize,
637    },
638    /// The collection contains the same complete canonical target twice.
639    Duplicate,
640}
641
642impl fmt::Display for ServiceAuditTargetsError {
643    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
644        match self {
645            Self::TooMany { maximum } => {
646                write!(formatter, "service audit target count exceeds {maximum}")
647            }
648            Self::Duplicate => formatter.write_str("service audit targets contain a duplicate"),
649        }
650    }
651}
652
653impl Error for ServiceAuditTargetsError {}
654
655/// A checked, duplicate-free service-audit target list in canonical order.
656#[derive(Clone, Eq, Hash, PartialEq)]
657pub struct ServiceAuditTargetsV1(Vec<ServiceAuditTargetV1>);
658
659impl ServiceAuditTargetsV1 {
660    /// Checks operation-specific target shape without granting authority.
661    /// Follower lifecycle audits identify exactly their selected registration.
662    #[must_use]
663    pub fn is_valid_for_operation(&self, operation: ServiceOperationV1) -> bool {
664        match operation {
665            ServiceOperationV1::RegisterFollower
666            | ServiceOperationV1::RetireFollower
667            | ServiceOperationV1::FenceReplicationPrimary
668            | ServiceOperationV1::PromoteFollower => {
669                matches!(
670                    self.as_slice(),
671                    [ServiceAuditTargetV1::ReplicationFollower(_)]
672                )
673            }
674            ServiceOperationV1::StreamChangelog => matches!(
675                self.as_slice(),
676                [] | [ServiceAuditTargetV1::ReplicationFollower(_)]
677            ),
678            _ => true,
679        }
680    }
681
682    /// Validates, canonicalizes, and constructs a list of zero through 16 targets.
683    pub fn new(
684        targets: impl IntoIterator<Item = ServiceAuditTargetV1>,
685    ) -> Result<Self, ServiceAuditTargetsError> {
686        let mut keyed_targets = Vec::with_capacity(MAX_SERVICE_AUDIT_TARGETS);
687        for target in targets {
688            if keyed_targets.len() == MAX_SERVICE_AUDIT_TARGETS {
689                return Err(ServiceAuditTargetsError::TooMany {
690                    maximum: MAX_SERVICE_AUDIT_TARGETS,
691                });
692            }
693            keyed_targets.push((target.canonical_key(), target));
694        }
695
696        keyed_targets.sort_unstable_by(|left, right| left.0.cmp(&right.0));
697        if keyed_targets
698            .windows(2)
699            .any(|pair| pair[0].0.as_slice() == pair[1].0.as_slice())
700        {
701            return Err(ServiceAuditTargetsError::Duplicate);
702        }
703
704        Ok(Self(
705            keyed_targets
706                .into_iter()
707                .map(|(_, target)| target)
708                .collect(),
709        ))
710    }
711
712    /// Constructs the canonical empty target list.
713    #[must_use]
714    pub const fn empty() -> Self {
715        Self(Vec::new())
716    }
717
718    /// Borrows the checked targets in canonical order.
719    #[must_use]
720    pub fn as_slice(&self) -> &[ServiceAuditTargetV1] {
721        &self.0
722    }
723
724    /// Returns the target count.
725    #[must_use]
726    pub fn len(&self) -> usize {
727        self.0.len()
728    }
729
730    /// Reports whether the canonical target list is empty.
731    #[must_use]
732    pub fn is_empty(&self) -> bool {
733        self.0.is_empty()
734    }
735
736    /// Consumes the collection and returns its canonical target vector.
737    #[must_use]
738    pub fn into_vec(self) -> Vec<ServiceAuditTargetV1> {
739        self.0
740    }
741}
742
743impl fmt::Debug for ServiceAuditTargetsV1 {
744    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
745        formatter
746            .debug_struct("ServiceAuditTargetsV1")
747            .field("len", &self.len())
748            .field("targets", &"[REDACTED]")
749            .finish()
750    }
751}
752
753impl TryFrom<Vec<ServiceAuditTargetV1>> for ServiceAuditTargetsV1 {
754    type Error = ServiceAuditTargetsError;
755
756    fn try_from(targets: Vec<ServiceAuditTargetV1>) -> Result<Self, Self::Error> {
757        Self::new(targets)
758    }
759}
760
761#[cfg(test)]
762mod tests {
763    use super::*;
764
765    fn lineage(value: &str) -> ContractLineage {
766        ContractLineage::new(value).expect("valid test lineage")
767    }
768
769    fn version(value: u64) -> ContractVersion {
770        ContractVersion::new(value).expect("nonzero test contract version")
771    }
772
773    fn entity_type_id(value: u32) -> EntityTypeId {
774        EntityTypeId::new(value).expect("nonzero test entity type ID")
775    }
776
777    fn command_id(value: u32) -> CommandId {
778        CommandId::new(value).expect("nonzero test command ID")
779    }
780
781    fn projection_id(value: u32) -> ProjectionId {
782        ProjectionId::new(value).expect("nonzero test projection ID")
783    }
784
785    fn index_id(value: u32) -> IndexId {
786        IndexId::new(value).expect("nonzero test index ID")
787    }
788
789    fn commit_sequence(value: u64) -> CommitSequence {
790        CommitSequence::new(value).expect("nonzero test commit sequence")
791    }
792
793    fn uuid_bytes(seed: u8) -> [u8; 16] {
794        let mut bytes = [seed; 16];
795        bytes[6] = 0x70 | (seed & 0x0f);
796        bytes[8] = 0x80 | (seed & 0x3f);
797        bytes
798    }
799
800    fn provenance_id(seed: u8) -> ProvenanceId {
801        ProvenanceId::from_bytes(uuid_bytes(seed)).expect("valid test UUIDv7")
802    }
803
804    fn capability_id(seed: u8) -> CapabilityId {
805        CapabilityId::from_bytes(uuid_bytes(seed)).expect("valid test UUIDv7")
806    }
807
808    // req: REP-003, REP-005
809    #[test]
810    fn replication_stream_audit_targets_are_empty_or_one_selected_follower() {
811        let follower = |seed| {
812            ServiceAuditTargetV1::ReplicationFollower(
813                ReplicationFollowerAuditTargetV1::new(
814                    crate::DatabaseId::from_bytes(uuid_bytes(1)).unwrap(),
815                    1,
816                    crate::LeadershipEpochV1::initial(),
817                    crate::ReplicationSourceHoldIdV1::new([seed; 16]).unwrap(),
818                )
819                .unwrap(),
820            )
821        };
822        for targets in [vec![], vec![follower(1)]] {
823            assert!(
824                ServiceAuditTargetsV1::new(targets)
825                    .unwrap()
826                    .is_valid_for_operation(ServiceOperationV1::StreamChangelog)
827            );
828        }
829        for targets in [
830            vec![ServiceAuditTargetV1::Capability(capability_id(1))],
831            vec![follower(1), follower(2)],
832            vec![
833                follower(1),
834                ServiceAuditTargetV1::Commit(commit_sequence(1)),
835            ],
836        ] {
837            assert!(
838                !ServiceAuditTargetsV1::new(targets)
839                    .unwrap()
840                    .is_valid_for_operation(ServiceOperationV1::StreamChangelog)
841            );
842        }
843    }
844
845    #[test]
846    fn service_operation_registry_is_exact_and_closed() {
847        let expected: Vec<u8> = (0x01..=0x3e).collect();
848        assert_eq!(
849            ServiceOperationV1::ALL
850                .into_iter()
851                .map(ServiceOperationV1::tag)
852                .collect::<Vec<_>>(),
853            expected
854        );
855        for operation in ServiceOperationV1::ALL {
856            assert_eq!(
857                ServiceOperationV1::from_tag(operation.tag()),
858                Some(operation)
859            );
860        }
861        assert_eq!(ServiceOperationV1::from_tag(0), None);
862        assert_eq!(
863            ServiceOperationV1::from_tag(0x2e),
864            Some(ServiceOperationV1::GetReactiveWakeup)
865        );
866        assert_eq!(
867            ServiceOperationV1::from_tag(0x30),
868            Some(ServiceOperationV1::GetApplicationInstallation)
869        );
870        assert_eq!(
871            ServiceOperationV1::from_tag(0x31),
872            Some(ServiceOperationV1::StartApplicationExport)
873        );
874        assert_eq!(
875            ServiceOperationV1::from_tag(0x34),
876            Some(ServiceOperationV1::CancelApplicationExport)
877        );
878        assert_eq!(
879            ServiceOperationV1::from_tag(0x38),
880            Some(ServiceOperationV1::CancelApplicationReimport)
881        );
882        assert_eq!(
883            ServiceOperationV1::from_tag(0x39),
884            Some(ServiceOperationV1::InspectVectorState)
885        );
886        assert_eq!(
887            ServiceOperationV1::from_tag(0x3c),
888            Some(ServiceOperationV1::FenceReplicationPrimary)
889        );
890        assert_eq!(
891            ServiceOperationV1::from_tag(0x3d),
892            Some(ServiceOperationV1::StreamChangelog)
893        );
894        assert_eq!(
895            ServiceOperationV1::from_tag(0x3e),
896            Some(ServiceOperationV1::PromoteFollower)
897        );
898        assert_eq!(ServiceOperationV1::from_tag(0x3f), None);
899        assert_eq!(ServiceOperationV1::from_tag(u8::MAX), None);
900    }
901
902    #[test]
903    // req: REP-006
904    fn follower_lifecycle_operations_have_distinct_additive_audit_tags() {
905        for (tag, name) in [(0x3a, "RegisterFollower"), (0x3b, "RetireFollower")] {
906            let operation =
907                ServiceOperationV1::from_tag(tag).expect("approved lifecycle operation");
908            assert_eq!(format!("{operation:?}"), name);
909            assert_eq!(operation.tag(), tag);
910        }
911        assert_eq!(ServiceOperationV1::InspectVectorState.tag(), 0x39);
912    }
913
914    #[test]
915    fn audit_phase_and_ingress_registries_are_exact_and_closed() {
916        assert_eq!(
917            ServiceAuditPhaseV1::ALL.map(ServiceAuditPhaseV1::tag),
918            [1, 2, 3, 4, 5, 6]
919        );
920        for phase in ServiceAuditPhaseV1::ALL {
921            assert_eq!(ServiceAuditPhaseV1::from_tag(phase.tag()), Some(phase));
922        }
923        assert_eq!(ServiceAuditPhaseV1::from_tag(0), None);
924        assert_eq!(ServiceAuditPhaseV1::from_tag(7), None);
925
926        assert_eq!(
927            ServiceIngressKindV1::ALL.map(ServiceIngressKindV1::tag),
928            [1, 2, 3]
929        );
930        for ingress in ServiceIngressKindV1::ALL {
931            assert_eq!(ServiceIngressKindV1::from_tag(ingress.tag()), Some(ingress));
932        }
933        assert_eq!(ServiceIngressKindV1::from_tag(0), None);
934        assert_eq!(ServiceIngressKindV1::from_tag(4), None);
935    }
936
937    #[test]
938    fn result_link_is_closed_and_command_link_is_all_or_nothing() {
939        let command = ServiceAuditLinkV1::Command {
940            commit_sequence: commit_sequence(7),
941            provenance_id: provenance_id(0x41),
942        };
943        assert!(matches!(
944            command,
945            ServiceAuditLinkV1::Command {
946                commit_sequence: sequence,
947                provenance_id: _
948            } if sequence == commit_sequence(7)
949        ));
950
951        let control_plane = ServiceAuditLinkV1::ControlPlane {
952            administration_sequence: AdministrationSequence::new(9)
953                .expect("nonzero test administration sequence"),
954        };
955        assert!(matches!(
956            control_plane,
957            ServiceAuditLinkV1::ControlPlane {
958                administration_sequence
959            } if administration_sequence.get() == 9
960        ));
961        assert_eq!(ServiceAuditLinkV1::None, ServiceAuditLinkV1::None);
962    }
963
964    #[test]
965    fn all_nine_target_tags_and_canonical_keys_are_frozen() {
966        let contract_lineage = lineage("LegalSpend");
967        let targets = [
968            ServiceAuditTargetV1::ContractLineage(contract_lineage.clone()),
969            ServiceAuditTargetV1::ContractVersion {
970                lineage: contract_lineage.clone(),
971                version: version(2),
972            },
973            ServiceAuditTargetV1::EntityType {
974                lineage: contract_lineage.clone(),
975                entity_type_id: entity_type_id(3),
976            },
977            ServiceAuditTargetV1::Command {
978                lineage: contract_lineage.clone(),
979                command_id: command_id(4),
980            },
981            ServiceAuditTargetV1::Projection {
982                lineage: contract_lineage.clone(),
983                projection_id: projection_id(5),
984            },
985            ServiceAuditTargetV1::Index {
986                lineage: contract_lineage.clone(),
987                index_id: index_id(6),
988            },
989            ServiceAuditTargetV1::Commit(commit_sequence(7)),
990            ServiceAuditTargetV1::Provenance(provenance_id(8)),
991            ServiceAuditTargetV1::Capability(capability_id(9)),
992        ];
993
994        assert_eq!(
995            targets.each_ref().map(|target| target.tag()),
996            [1, 2, 3, 4, 5, 6, 7, 8, 9]
997        );
998
999        let mut lineage_frame = Vec::from(10_u32.to_be_bytes());
1000        lineage_frame.extend_from_slice(b"LegalSpend");
1001        for (target, suffix) in targets[..6].iter().zip([
1002            Vec::new(),
1003            2_u64.to_be_bytes().to_vec(),
1004            3_u32.to_be_bytes().to_vec(),
1005            4_u32.to_be_bytes().to_vec(),
1006            5_u32.to_be_bytes().to_vec(),
1007            6_u32.to_be_bytes().to_vec(),
1008        ]) {
1009            let mut expected = vec![target.tag()];
1010            expected.extend_from_slice(&lineage_frame);
1011            expected.extend_from_slice(&suffix);
1012            assert_eq!(target.canonical_key(), expected);
1013        }
1014
1015        let mut expected_commit = vec![0x07];
1016        expected_commit.extend_from_slice(&7_u64.to_be_bytes());
1017        assert_eq!(targets[6].canonical_key(), expected_commit);
1018
1019        let mut expected_provenance = vec![0x08];
1020        expected_provenance.extend_from_slice(&uuid_bytes(8));
1021        assert_eq!(targets[7].canonical_key(), expected_provenance);
1022
1023        let mut expected_capability = vec![0x09];
1024        expected_capability.extend_from_slice(&uuid_bytes(9));
1025        assert_eq!(targets[8].canonical_key(), expected_capability);
1026    }
1027
1028    #[test]
1029    fn construction_is_permutation_independent_and_uses_complete_keys() {
1030        let short_lineage = ServiceAuditTargetV1::Command {
1031            lineage: lineage("z"),
1032            command_id: command_id(1),
1033        };
1034        let long_lineage = ServiceAuditTargetV1::Command {
1035            lineage: lineage("aa"),
1036            command_id: command_id(1),
1037        };
1038        let other_tag = ServiceAuditTargetV1::Commit(commit_sequence(1));
1039
1040        let first = ServiceAuditTargetsV1::new([
1041            other_tag.clone(),
1042            long_lineage.clone(),
1043            short_lineage.clone(),
1044        ])
1045        .expect("distinct bounded targets are valid");
1046        let second = ServiceAuditTargetsV1::new([
1047            short_lineage.clone(),
1048            other_tag.clone(),
1049            long_lineage.clone(),
1050        ])
1051        .expect("permutation is valid");
1052
1053        assert_eq!(first, second);
1054        assert_eq!(first.as_slice(), &[short_lineage, long_lineage, other_tag]);
1055    }
1056
1057    #[test]
1058    fn equal_local_ids_in_distinct_lineages_are_distinct() {
1059        let targets = ServiceAuditTargetsV1::new([
1060            ServiceAuditTargetV1::EntityType {
1061                lineage: lineage("A"),
1062                entity_type_id: entity_type_id(1),
1063            },
1064            ServiceAuditTargetV1::EntityType {
1065                lineage: lineage("B"),
1066                entity_type_id: entity_type_id(1),
1067            },
1068        ])
1069        .expect("lineage scopes make the targets distinct");
1070        assert_eq!(targets.len(), 2);
1071    }
1072
1073    #[test]
1074    fn duplicate_and_seventeenth_targets_fail_closed_without_payloads() {
1075        let duplicate = ServiceAuditTargetV1::Command {
1076            lineage: lineage("SensitiveLineage"),
1077            command_id: command_id(42),
1078        };
1079        let error = ServiceAuditTargetsV1::new([duplicate.clone(), duplicate])
1080            .expect_err("exact duplicate must reject");
1081        assert_eq!(error, ServiceAuditTargetsError::Duplicate);
1082        assert!(!error.to_string().contains("SensitiveLineage"));
1083        assert!(!format!("{error:?}").contains("SensitiveLineage"));
1084
1085        let too_many = (1..=17)
1086            .map(commit_sequence)
1087            .map(ServiceAuditTargetV1::Commit);
1088        let error =
1089            ServiceAuditTargetsV1::new(too_many).expect_err("a seventeenth target must reject");
1090        assert_eq!(
1091            error,
1092            ServiceAuditTargetsError::TooMany {
1093                maximum: MAX_SERVICE_AUDIT_TARGETS,
1094            }
1095        );
1096    }
1097
1098    #[test]
1099    fn empty_and_exactly_sixteen_targets_are_valid() {
1100        let empty = ServiceAuditTargetsV1::new([]).expect("empty list is canonical");
1101        assert!(empty.is_empty());
1102        assert_eq!(empty, ServiceAuditTargetsV1::empty());
1103
1104        let full = ServiceAuditTargetsV1::new(
1105            (1..=16)
1106                .map(commit_sequence)
1107                .rev()
1108                .map(ServiceAuditTargetV1::Commit),
1109        )
1110        .expect("exactly sixteen distinct targets are valid");
1111        assert_eq!(full.len(), MAX_SERVICE_AUDIT_TARGETS);
1112        for (index, target) in full.as_slice().iter().enumerate() {
1113            assert_eq!(
1114                target,
1115                &ServiceAuditTargetV1::Commit(commit_sequence((index + 1) as u64))
1116            );
1117        }
1118    }
1119
1120    #[test]
1121    fn target_and_collection_debug_output_is_redacted() {
1122        let target = ServiceAuditTargetV1::ContractVersion {
1123            lineage: lineage("SensitiveLineage"),
1124            version: version(123),
1125        };
1126        let debug = format!("{target:?}");
1127        assert!(debug.contains("ContractVersion"));
1128        assert!(debug.contains("[REDACTED]"));
1129        assert!(!debug.contains("SensitiveLineage"));
1130        assert!(!debug.contains("123"));
1131
1132        let targets = ServiceAuditTargetsV1::new([target]).expect("one target is valid");
1133        let debug = format!("{targets:?}");
1134        assert!(debug.contains("len: 1"));
1135        assert!(!debug.contains("SensitiveLineage"));
1136    }
1137}