| | | 1 | | using System.Collections.ObjectModel; |
| | | 2 | | using AsiBackbone.Core.Audit; |
| | | 3 | | using AsiBackbone.Core.Serialization; |
| | | 4 | | |
| | | 5 | | namespace AsiBackbone.Core.Emissions; |
| | | 6 | | |
| | | 7 | | /// <summary> |
| | | 8 | | /// Represents a provider-neutral governance emission envelope that can be handed to outbox storage or downstream provid |
| | | 9 | | /// </summary> |
| | | 10 | | /// <remarks> |
| | | 11 | | /// The envelope carries minimized governance context and safe diagnostics. It does not contain provider SDK payloads, c |
| | | 12 | | /// </remarks> |
| | | 13 | | public sealed class GovernanceEmissionEnvelope |
| | | 14 | | { |
| | 5 | 15 | | private static readonly IReadOnlyDictionary<string, string> EmptyMetadata = |
| | 5 | 16 | | new ReadOnlyDictionary<string, string>( |
| | 5 | 17 | | new Dictionary<string, string>(StringComparer.Ordinal)); |
| | | 18 | | |
| | 241 | 19 | | private GovernanceEmissionEnvelope( |
| | 241 | 20 | | string envelopeId, |
| | 241 | 21 | | string schemaVersion, |
| | 241 | 22 | | GovernanceEmissionEventType eventType, |
| | 241 | 23 | | string? eventId, |
| | 241 | 24 | | DateTimeOffset occurredUtc, |
| | 241 | 25 | | DateTimeOffset createdUtc, |
| | 241 | 26 | | string? correlationId, |
| | 241 | 27 | | string? auditResidueId, |
| | 241 | 28 | | AuditResidueLifecycleStage? lifecycleStage, |
| | 241 | 29 | | string? policyVersion, |
| | 241 | 30 | | string? policyHash, |
| | 241 | 31 | | string? traceId, |
| | 241 | 32 | | string? spanId, |
| | 241 | 33 | | string? parentSpanId, |
| | 241 | 34 | | string? operationName, |
| | 241 | 35 | | string? outcome, |
| | 241 | 36 | | string? actorId, |
| | 241 | 37 | | string? emitterStatus, |
| | 241 | 38 | | string? emitterProvider, |
| | 241 | 39 | | long? outboxSequence, |
| | 241 | 40 | | string? gatewayExecutionId, |
| | 241 | 41 | | string? decisionStage, |
| | 241 | 42 | | GovernanceEmissionPayload? payload, |
| | 241 | 43 | | IReadOnlyDictionary<string, string> metadata) |
| | | 44 | | { |
| | 241 | 45 | | ArgumentException.ThrowIfNullOrWhiteSpace(envelopeId); |
| | | 46 | | |
| | 241 | 47 | | if (!Enum.IsDefined(eventType)) |
| | | 48 | | { |
| | 1 | 49 | | throw new ArgumentOutOfRangeException(nameof(eventType), eventType, "Governance emission event type must be |
| | | 50 | | } |
| | | 51 | | |
| | 240 | 52 | | if (lifecycleStage.HasValue && !Enum.IsDefined(lifecycleStage.Value)) |
| | | 53 | | { |
| | 1 | 54 | | throw new ArgumentOutOfRangeException(nameof(lifecycleStage), lifecycleStage, "Lifecycle stage must be defin |
| | | 55 | | } |
| | | 56 | | |
| | 239 | 57 | | EnvelopeId = envelopeId.Trim(); |
| | 239 | 58 | | SchemaVersion = AsiBackboneSchemaVersions.Normalize(schemaVersion); |
| | 239 | 59 | | EventType = eventType; |
| | 239 | 60 | | EventId = NormalizeOptional(eventId); |
| | 239 | 61 | | OccurredUtc = occurredUtc.ToUniversalTime(); |
| | 239 | 62 | | CreatedUtc = createdUtc.ToUniversalTime(); |
| | 239 | 63 | | CorrelationId = NormalizeOptional(correlationId); |
| | 239 | 64 | | AuditResidueId = NormalizeOptional(auditResidueId); |
| | 239 | 65 | | LifecycleStage = lifecycleStage; |
| | 239 | 66 | | LifecycleStageSequence = lifecycleStage.HasValue ? (int)lifecycleStage.Value : null; |
| | 239 | 67 | | PolicyVersion = NormalizeOptional(policyVersion); |
| | 239 | 68 | | PolicyHash = NormalizeOptional(policyHash); |
| | 239 | 69 | | TraceId = NormalizeOptional(traceId); |
| | 239 | 70 | | SpanId = NormalizeOptional(spanId); |
| | 239 | 71 | | ParentSpanId = NormalizeOptional(parentSpanId); |
| | 239 | 72 | | OperationName = NormalizeOptional(operationName); |
| | 239 | 73 | | Outcome = NormalizeOptional(outcome); |
| | 239 | 74 | | ActorId = NormalizeOptional(actorId); |
| | 239 | 75 | | EmitterStatus = NormalizeOptional(emitterStatus); |
| | 239 | 76 | | EmitterProvider = NormalizeOptional(emitterProvider); |
| | 239 | 77 | | OutboxSequence = NormalizeNonNegative(outboxSequence, nameof(outboxSequence)); |
| | 238 | 78 | | GatewayExecutionId = NormalizeOptional(gatewayExecutionId); |
| | 238 | 79 | | DecisionStage = NormalizeOptional(decisionStage); |
| | 238 | 80 | | Payload = payload; |
| | 238 | 81 | | Metadata = metadata; |
| | 238 | 82 | | } |
| | | 83 | | |
| | | 84 | | /// <summary> |
| | | 85 | | /// Gets the stable envelope identifier. |
| | | 86 | | /// </summary> |
| | 126 | 87 | | public string EnvelopeId { get; } |
| | | 88 | | |
| | | 89 | | /// <summary> |
| | | 90 | | /// Gets the schema version for this provider-neutral envelope shape. |
| | | 91 | | /// </summary> |
| | 114 | 92 | | public string SchemaVersion { get; } |
| | | 93 | | |
| | | 94 | | /// <summary> |
| | | 95 | | /// Gets the provider-neutral event category. |
| | | 96 | | /// </summary> |
| | 108 | 97 | | public GovernanceEmissionEventType EventType { get; } |
| | | 98 | | |
| | | 99 | | /// <summary> |
| | | 100 | | /// Gets the source governance event identifier, when available. |
| | | 101 | | /// </summary> |
| | 101 | 102 | | public string? EventId { get; } |
| | | 103 | | |
| | | 104 | | /// <summary> |
| | | 105 | | /// Gets the UTC timestamp when the source governance event occurred. |
| | | 106 | | /// </summary> |
| | 94 | 107 | | public DateTimeOffset OccurredUtc { get; } |
| | | 108 | | |
| | | 109 | | /// <summary> |
| | | 110 | | /// Gets the UTC timestamp when this emission envelope was created. |
| | | 111 | | /// </summary> |
| | 92 | 112 | | public DateTimeOffset CreatedUtc { get; } |
| | | 113 | | |
| | | 114 | | /// <summary> |
| | | 115 | | /// Gets the correlation identifier that links the emission to the host workflow, when available. |
| | | 116 | | /// </summary> |
| | 144 | 117 | | public string? CorrelationId { get; } |
| | | 118 | | |
| | | 119 | | /// <summary> |
| | | 120 | | /// Gets the audit residue identifier linked to this emission, when available. |
| | | 121 | | /// </summary> |
| | 108 | 122 | | public string? AuditResidueId { get; } |
| | | 123 | | |
| | | 124 | | /// <summary> |
| | | 125 | | /// Gets the audit residue lifecycle stage linked to this emission, when available. |
| | | 126 | | /// </summary> |
| | 133 | 127 | | public AuditResidueLifecycleStage? LifecycleStage { get; } |
| | | 128 | | |
| | | 129 | | /// <summary> |
| | | 130 | | /// Gets the stable lifecycle stage sequence value, when a lifecycle stage is supplied. |
| | | 131 | | /// </summary> |
| | 98 | 132 | | public int? LifecycleStageSequence { get; } |
| | | 133 | | |
| | | 134 | | /// <summary> |
| | | 135 | | /// Gets the policy version associated with the source governance event, when available. |
| | | 136 | | /// </summary> |
| | 101 | 137 | | public string? PolicyVersion { get; } |
| | | 138 | | |
| | | 139 | | /// <summary> |
| | | 140 | | /// Gets the policy hash associated with the source governance event, when available. |
| | | 141 | | /// </summary> |
| | 101 | 142 | | public string? PolicyHash { get; } |
| | | 143 | | |
| | | 144 | | /// <summary> |
| | | 145 | | /// Gets the trace identifier associated with the source governance event, when available. |
| | | 146 | | /// </summary> |
| | 108 | 147 | | public string? TraceId { get; } |
| | | 148 | | |
| | | 149 | | /// <summary> |
| | | 150 | | /// Gets the span identifier associated with the source governance event, when available. |
| | | 151 | | /// </summary> |
| | 99 | 152 | | public string? SpanId { get; } |
| | | 153 | | |
| | | 154 | | /// <summary> |
| | | 155 | | /// Gets the parent span identifier associated with the source governance event, when available. |
| | | 156 | | /// </summary> |
| | 99 | 157 | | public string? ParentSpanId { get; } |
| | | 158 | | |
| | | 159 | | /// <summary> |
| | | 160 | | /// Gets the operation name associated with the source governance event, when available. |
| | | 161 | | /// </summary> |
| | 102 | 162 | | public string? OperationName { get; } |
| | | 163 | | |
| | | 164 | | /// <summary> |
| | | 165 | | /// Gets the outcome associated with the source governance event, when available. |
| | | 166 | | /// </summary> |
| | 98 | 167 | | public string? Outcome { get; } |
| | | 168 | | |
| | | 169 | | /// <summary> |
| | | 170 | | /// Gets the actor identifier associated with the source governance event, when available. |
| | | 171 | | /// </summary> |
| | 93 | 172 | | public string? ActorId { get; } |
| | | 173 | | |
| | | 174 | | /// <summary> |
| | | 175 | | /// Gets the provider-neutral emitter status, when available. |
| | | 176 | | /// </summary> |
| | 99 | 177 | | public string? EmitterStatus { get; } |
| | | 178 | | |
| | | 179 | | /// <summary> |
| | | 180 | | /// Gets the provider-neutral emitter provider name, when available. |
| | | 181 | | /// </summary> |
| | 93 | 182 | | public string? EmitterProvider { get; } |
| | | 183 | | |
| | | 184 | | /// <summary> |
| | | 185 | | /// Gets the outbox sequence associated with the emission, when available. |
| | | 186 | | /// </summary> |
| | 96 | 187 | | public long? OutboxSequence { get; } |
| | | 188 | | |
| | | 189 | | /// <summary> |
| | | 190 | | /// Gets the gateway execution identifier associated with the emission, when available. |
| | | 191 | | /// </summary> |
| | 99 | 192 | | public string? GatewayExecutionId { get; } |
| | | 193 | | |
| | | 194 | | /// <summary> |
| | | 195 | | /// Gets the provider-neutral decision stage associated with the emission, when available. |
| | | 196 | | /// </summary> |
| | 98 | 197 | | public string? DecisionStage { get; } |
| | | 198 | | |
| | | 199 | | /// <summary> |
| | | 200 | | /// Gets the minimized provider-neutral payload descriptor, when available. |
| | | 201 | | /// </summary> |
| | 122 | 202 | | public GovernanceEmissionPayload? Payload { get; } |
| | | 203 | | |
| | | 204 | | /// <summary> |
| | | 205 | | /// Gets minimized provider-neutral metadata. |
| | | 206 | | /// </summary> |
| | 113 | 207 | | public IReadOnlyDictionary<string, string> Metadata { get; } |
| | | 208 | | |
| | | 209 | | /// <summary> |
| | | 210 | | /// Gets a value indicating whether correlation metadata is available. |
| | | 211 | | /// </summary> |
| | 8 | 212 | | public bool HasCorrelation => CorrelationId is not null || TraceId is not null || AuditResidueId is not null; |
| | | 213 | | |
| | | 214 | | /// <summary> |
| | | 215 | | /// Gets a value indicating whether envelope metadata is present. |
| | | 216 | | /// </summary> |
| | 5 | 217 | | public bool HasMetadata => Metadata.Count > 0; |
| | | 218 | | |
| | | 219 | | /// <summary> |
| | | 220 | | /// Creates a provider-neutral governance emission envelope. |
| | | 221 | | /// </summary> |
| | | 222 | | public static GovernanceEmissionEnvelope Create( |
| | | 223 | | GovernanceEmissionEventType eventType, |
| | | 224 | | string? eventId = null, |
| | | 225 | | DateTimeOffset? occurredUtc = null, |
| | | 226 | | string? envelopeId = null, |
| | | 227 | | DateTimeOffset? createdUtc = null, |
| | | 228 | | string? schemaVersion = null, |
| | | 229 | | string? correlationId = null, |
| | | 230 | | string? auditResidueId = null, |
| | | 231 | | AuditResidueLifecycleStage? lifecycleStage = null, |
| | | 232 | | string? policyVersion = null, |
| | | 233 | | string? policyHash = null, |
| | | 234 | | string? traceId = null, |
| | | 235 | | string? spanId = null, |
| | | 236 | | string? parentSpanId = null, |
| | | 237 | | string? operationName = null, |
| | | 238 | | string? outcome = null, |
| | | 239 | | string? actorId = null, |
| | | 240 | | string? emitterStatus = null, |
| | | 241 | | string? emitterProvider = null, |
| | | 242 | | long? outboxSequence = null, |
| | | 243 | | string? gatewayExecutionId = null, |
| | | 244 | | string? decisionStage = null, |
| | | 245 | | GovernanceEmissionPayload? payload = null, |
| | | 246 | | IReadOnlyDictionary<string, string>? metadata = null) |
| | | 247 | | { |
| | 236 | 248 | | return new GovernanceEmissionEnvelope( |
| | 236 | 249 | | NormalizeIdentifier(envelopeId), |
| | 236 | 250 | | schemaVersion ?? AsiBackboneSchemaVersions.StableArtifactsV1, |
| | 236 | 251 | | eventType, |
| | 236 | 252 | | eventId, |
| | 236 | 253 | | occurredUtc ?? DateTimeOffset.UtcNow, |
| | 236 | 254 | | createdUtc ?? DateTimeOffset.UtcNow, |
| | 236 | 255 | | correlationId, |
| | 236 | 256 | | auditResidueId, |
| | 236 | 257 | | lifecycleStage, |
| | 236 | 258 | | policyVersion, |
| | 236 | 259 | | policyHash, |
| | 236 | 260 | | traceId, |
| | 236 | 261 | | spanId, |
| | 236 | 262 | | parentSpanId, |
| | 236 | 263 | | operationName, |
| | 236 | 264 | | outcome, |
| | 236 | 265 | | actorId, |
| | 236 | 266 | | emitterStatus, |
| | 236 | 267 | | emitterProvider, |
| | 236 | 268 | | outboxSequence, |
| | 236 | 269 | | gatewayExecutionId, |
| | 236 | 270 | | decisionStage, |
| | 236 | 271 | | payload, |
| | 236 | 272 | | NormalizeMetadata(metadata)); |
| | | 273 | | } |
| | | 274 | | |
| | | 275 | | /// <summary> |
| | | 276 | | /// Creates a provider-neutral governance emission envelope from audit residue. |
| | | 277 | | /// </summary> |
| | | 278 | | public static GovernanceEmissionEnvelope FromResidue( |
| | | 279 | | IAsiBackboneAuditResidue residue, |
| | | 280 | | GovernanceEmissionEventType eventType = GovernanceEmissionEventType.AuditResidue, |
| | | 281 | | string? envelopeId = null, |
| | | 282 | | DateTimeOffset? createdUtc = null, |
| | | 283 | | GovernanceEmissionPayload? payload = null, |
| | | 284 | | IReadOnlyDictionary<string, string>? metadata = null) |
| | | 285 | | { |
| | 2 | 286 | | ArgumentNullException.ThrowIfNull(residue); |
| | | 287 | | |
| | 2 | 288 | | return new GovernanceEmissionEnvelope( |
| | 2 | 289 | | NormalizeIdentifier(envelopeId), |
| | 2 | 290 | | residue.SchemaVersion, |
| | 2 | 291 | | eventType, |
| | 2 | 292 | | residue.EventId, |
| | 2 | 293 | | residue.OccurredUtc, |
| | 2 | 294 | | createdUtc ?? DateTimeOffset.UtcNow, |
| | 2 | 295 | | residue.CorrelationId, |
| | 2 | 296 | | residue.AuditResidueId, |
| | 2 | 297 | | null, |
| | 2 | 298 | | residue.PolicyVersion, |
| | 2 | 299 | | residue.PolicyHash, |
| | 2 | 300 | | residue.TraceId, |
| | 2 | 301 | | residue.SpanId, |
| | 2 | 302 | | residue.ParentSpanId, |
| | 2 | 303 | | residue.OperationName, |
| | 2 | 304 | | residue.Outcome, |
| | 2 | 305 | | residue.ActorId, |
| | 2 | 306 | | residue.EmitterStatus, |
| | 2 | 307 | | residue.EmitterProvider, |
| | 2 | 308 | | residue.OutboxSequence, |
| | 2 | 309 | | residue.GatewayExecutionId, |
| | 2 | 310 | | residue.DecisionStage, |
| | 2 | 311 | | payload, |
| | 2 | 312 | | NormalizeMetadata(residue.Metadata, metadata)); |
| | | 313 | | } |
| | | 314 | | |
| | | 315 | | /// <summary> |
| | | 316 | | /// Creates a provider-neutral governance emission envelope from an audit residue lifecycle event. |
| | | 317 | | /// </summary> |
| | | 318 | | public static GovernanceEmissionEnvelope FromLifecycleEvent( |
| | | 319 | | AuditResidueLifecycleEvent lifecycleEvent, |
| | | 320 | | string? envelopeId = null, |
| | | 321 | | DateTimeOffset? createdUtc = null, |
| | | 322 | | GovernanceEmissionPayload? payload = null, |
| | | 323 | | IReadOnlyDictionary<string, string>? metadata = null) |
| | | 324 | | { |
| | 3 | 325 | | ArgumentNullException.ThrowIfNull(lifecycleEvent); |
| | | 326 | | |
| | 3 | 327 | | return new GovernanceEmissionEnvelope( |
| | 3 | 328 | | NormalizeIdentifier(envelopeId), |
| | 3 | 329 | | AsiBackboneSchemaVersions.StableArtifactsV1, |
| | 3 | 330 | | GovernanceEmissionEventType.AuditLifecycle, |
| | 3 | 331 | | lifecycleEvent.EventId, |
| | 3 | 332 | | lifecycleEvent.OccurredUtc, |
| | 3 | 333 | | createdUtc ?? DateTimeOffset.UtcNow, |
| | 3 | 334 | | lifecycleEvent.CorrelationId, |
| | 3 | 335 | | lifecycleEvent.AuditResidueId, |
| | 3 | 336 | | lifecycleEvent.Stage, |
| | 3 | 337 | | null, |
| | 3 | 338 | | null, |
| | 3 | 339 | | lifecycleEvent.TraceId, |
| | 3 | 340 | | null, |
| | 3 | 341 | | null, |
| | 3 | 342 | | lifecycleEvent.OperationName, |
| | 3 | 343 | | lifecycleEvent.Outcome, |
| | 3 | 344 | | null, |
| | 3 | 345 | | null, |
| | 3 | 346 | | null, |
| | 3 | 347 | | null, |
| | 3 | 348 | | null, |
| | 3 | 349 | | lifecycleEvent.Stage.ToString(), |
| | 3 | 350 | | payload, |
| | 3 | 351 | | NormalizeMetadata(lifecycleEvent.Metadata, metadata)); |
| | | 352 | | } |
| | | 353 | | |
| | | 354 | | private static string NormalizeIdentifier(string? identifier) |
| | | 355 | | { |
| | 241 | 356 | | return string.IsNullOrWhiteSpace(identifier) |
| | 241 | 357 | | ? Guid.NewGuid().ToString("N") |
| | 241 | 358 | | : identifier.Trim(); |
| | | 359 | | } |
| | | 360 | | |
| | | 361 | | private static string? NormalizeOptional(string? value) |
| | | 362 | | { |
| | 3583 | 363 | | return string.IsNullOrWhiteSpace(value) |
| | 3583 | 364 | | ? null |
| | 3583 | 365 | | : value.Trim(); |
| | | 366 | | } |
| | | 367 | | |
| | | 368 | | private static long? NormalizeNonNegative(long? value, string parameterName) |
| | | 369 | | { |
| | 239 | 370 | | return value < 0 |
| | 239 | 371 | | ? throw new ArgumentOutOfRangeException(parameterName, value, "Value must be greater than or equal to zero." |
| | 239 | 372 | | : value; |
| | | 373 | | } |
| | | 374 | | |
| | | 375 | | private static IReadOnlyDictionary<string, string> NormalizeMetadata( |
| | | 376 | | params IReadOnlyDictionary<string, string>?[] metadataSets) |
| | | 377 | | { |
| | 241 | 378 | | Dictionary<string, string> normalizedMetadata = new(StringComparer.Ordinal); |
| | | 379 | | |
| | 974 | 380 | | foreach (IReadOnlyDictionary<string, string>? metadata in metadataSets) |
| | | 381 | | { |
| | 246 | 382 | | if (metadata is null || metadata.Count == 0) |
| | | 383 | | { |
| | | 384 | | continue; |
| | | 385 | | } |
| | | 386 | | |
| | 910 | 387 | | foreach (KeyValuePair<string, string> item in metadata) |
| | | 388 | | { |
| | 289 | 389 | | if (string.IsNullOrWhiteSpace(item.Key)) |
| | | 390 | | { |
| | | 391 | | continue; |
| | | 392 | | } |
| | | 393 | | |
| | 287 | 394 | | normalizedMetadata[item.Key.Trim()] = item.Value?.Trim() ?? string.Empty; |
| | | 395 | | } |
| | | 396 | | } |
| | | 397 | | |
| | 241 | 398 | | return normalizedMetadata.Count == 0 |
| | 241 | 399 | | ? EmptyMetadata |
| | 241 | 400 | | : new ReadOnlyDictionary<string, string>(normalizedMetadata); |
| | | 401 | | } |
| | | 402 | | } |