1use std::collections::{BTreeMap, BTreeSet};
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize, de::DeserializeOwned};
5use serde_json::Value;
6use utoipa::ToSchema;
7
8use crate::extraction_input_digest;
9use crate::{
10 CANARY_DECISION_PROTOCOL, CANARY_PLAN_PROTOCOL, CONFIG_ACTIVATION_RECEIPT_PROTOCOL,
11 CONFIG_REVISION_PROTOCOL, CanaryDecision, CanaryPlan, ConfigActivationReceipt, ConfigRevision,
12 DEPLOYMENT_OBSERVATION_PROTOCOL, DEPLOYMENT_PLAN_PROTOCOL, DEPLOYMENT_RECEIPT_PROTOCOL,
13 DeploymentObservation, DeploymentPlan, DeploymentReceipt, EDGE_CONTRACT_PROTOCOL,
14 ENVIRONMENT_VERIFICATION_PROTOCOL, EdgeContract, EnvironmentVerification,
15 GATEWAY_OBSERVATION_PROTOCOL, GATEWAY_PLAN_PROTOCOL, GatewayConfigurationPlan,
16 GatewayObservation, POLICY_EVIDENCE_PROTOCOL, PROMOTION_APPROVAL_PROTOCOL,
17 PROMOTION_PLAN_PROTOCOL, PROMOTION_RECEIPT_PROTOCOL, PolicyEvidence, PromotionApproval,
18 PromotionPlan, PromotionReceipt, RELEASE_TRUST_EVIDENCE_PROTOCOL,
19 RELIABILITY_OBSERVATION_PROTOCOL, ROLLBACK_PLAN_PROTOCOL, ROLLBACK_RECEIPT_PROTOCOL,
20 ReleaseTrustEvidence, ReliabilityObservation, RollbackPlan, RollbackReceipt,
21 SERVICE_RELEASE_PROTOCOL, ServiceRelease, canary_decision_integrity_is_valid,
22 canary_plan_integrity_is_valid, config_revision_integrity_is_valid,
23 deployment_observation_content_integrity_is_valid, deployment_plan_integrity_is_valid,
24 edge_contract_integrity_is_valid, environment_verification_integrity_is_valid,
25 gateway_observation_content_integrity_is_valid, gateway_plan_integrity_is_valid,
26 policy_evidence_integrity_is_valid, promotion_plan_integrity_is_valid,
27 rollback_plan_integrity_is_valid, secret_reference_metadata_is_safe,
28 service_release_integrity_is_valid,
29};
30use crate::{
31 CONTRACT_RETIREMENT_PLAN_PROTOCOL, CONTRACT_RETIREMENT_RECEIPT_PROTOCOL,
32 ContractRetirementPlan, ContractRetirementReceipt, DELIVERY_FAILURE_RECOVERY_PROTOCOL,
33 DISASTER_RECOVERY_EVIDENCE_PROTOCOL, DeliveryFailureRecoveryEvidence, DisasterRecoveryEvidence,
34 GA_SUPPORT_MANIFEST_PROTOCOL, GaSupportManifest, PERFORMANCE_PROFILE_PROTOCOL,
35 PerformanceProfile, SECURITY_REVIEW_PROTOCOL, SERVICE_RESTORE_EVIDENCE_PROTOCOL,
36 SUPPORT_ENVELOPE_PROTOCOL, SecurityReviewEvidence, ServiceRestoreEvidence, SupportEnvelope,
37 contract_retirement_plan_integrity_is_valid, contract_retirement_receipt_integrity_is_valid,
38 delivery_failure_recovery_integrity_is_valid, disaster_recovery_evidence_integrity_is_valid,
39 ga_support_manifest_integrity_valid, performance_profile_integrity_is_valid,
40 security_review_integrity_is_valid, service_restore_integrity_is_valid,
41 support_envelope_integrity_is_valid,
42};
43
44pub const DELIVERY_CONSOLE_PROJECTION_PROTOCOL: &str = "lenso.delivery-console.v1";
45pub const DELIVERY_ARTIFACT_BATCH_PROTOCOL: &str = "lenso.delivery-artifact-batch.v1";
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
48#[serde(rename_all = "camelCase")]
49pub struct DeliveryConsoleArtifacts {
50 #[serde(default)]
51 pub artifacts: Vec<Value>,
52}
53
54#[derive(
55 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, ToSchema,
56)]
57#[serde(rename_all = "snake_case")]
58pub enum DeliveryConsoleState {
59 Planned,
60 Blocked,
61 Staged,
62 Approved,
63 Canary,
64 Converging,
65 Ready,
66 RollingBack,
67 RolledBack,
68 Paused,
69 InterventionRequired,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
73#[serde(rename_all = "camelCase")]
74pub struct DeliveryConsoleRelease {
75 pub service_id: String,
76 pub release_id: String,
77 pub release_digest: String,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
81#[serde(rename_all = "camelCase")]
82pub struct DeliveryConsoleSupplyChainWorkload {
83 pub workload_id: String,
84 pub artifact_digest: String,
85 pub signature_status: String,
86 pub sbom_reference: String,
87 pub provenance_reference: String,
88 pub provenance_subject_matches: bool,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
92#[serde(rename_all = "camelCase")]
93pub struct DeliveryConsolePolicy {
94 pub evidence_id: String,
95 pub pack_id: String,
96 pub decision: String,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
100#[serde(rename_all = "camelCase")]
101pub struct DeliveryConsoleSecretReference {
102 pub reference_id: String,
103 pub provider: String,
104 pub purpose: String,
105 pub scope: String,
106 pub status: String,
107 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub rotation_revision: Option<String>,
109}
110
111#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
112#[serde(rename_all = "camelCase")]
113pub struct DeliveryConsoleConfiguration {
114 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub desired_revision_id: Option<String>,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub active_revision_id: Option<String>,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub previous_revision_id: Option<String>,
120 pub drifted: bool,
121 #[serde(default)]
122 pub secret_references: Vec<DeliveryConsoleSecretReference>,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
126#[serde(rename_all = "camelCase")]
127pub struct DeliveryConsoleDeployment {
128 pub environment: String,
129 pub desired_release_id: String,
130 pub observed_release_id: String,
131 pub config_revision_id: String,
132 pub drifted: bool,
133 pub fresh: bool,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
137#[serde(rename_all = "camelCase")]
138pub struct DeliveryConsoleAdapterDrift {
139 pub environment: String,
140 pub drifted: bool,
141 pub fresh: bool,
142 #[serde(default)]
143 pub next_actions: Vec<String>,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
147#[serde(rename_all = "camelCase")]
148pub struct DeliveryConsoleEdge {
149 pub contract_id: String,
150 #[serde(default)]
151 pub public_routes: Vec<String>,
152}
153
154#[derive(
155 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, ToSchema,
156)]
157#[serde(rename_all = "camelCase")]
158pub struct DeliveryConsoleIssue {
159 pub code: String,
160 pub message: String,
161 #[serde(default)]
162 pub evidence_references: Vec<String>,
163 pub remediation: String,
164 #[serde(default)]
165 pub next_actions: Vec<String>,
166}
167
168#[derive(
169 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, ToSchema,
170)]
171#[serde(rename_all = "camelCase")]
172pub struct DeliveryConsoleTimelineEntry {
173 pub protocol: String,
174 pub artifact_id: String,
175 pub state: String,
176 #[serde(default)]
177 pub evidence_references: Vec<String>,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
181#[serde(rename_all = "camelCase")]
182pub struct DeliveryConsoleDependencyObservation {
183 pub dependency_id: String,
184 pub available: bool,
185 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub active_degraded_mode: Option<String>,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
190#[serde(rename_all = "camelCase")]
191pub struct DeliveryConsoleCanaryObservation {
192 pub observation_id: String,
193 pub observed_revision: u64,
194 pub fresh: bool,
195 pub observation_window_seconds: u64,
196 pub sample_count: u64,
197 pub generic_process_healthy: bool,
198 #[serde(default)]
199 pub workload_readiness: BTreeMap<String, bool>,
200 #[serde(default)]
201 pub workload_liveness: BTreeMap<String, bool>,
202 pub availability_basis_points: Option<u32>,
203 pub latency_p99_ms: Option<u64>,
204 pub error_budget_used_basis_points: Option<u32>,
205 pub queue_backlog: Option<u64>,
206 pub workflow_backlog: Option<u64>,
207 pub timer_lag_ms: Option<u64>,
208 pub retry_exhaustion: Option<u64>,
209 pub compensation_pressure: Option<u64>,
210 #[serde(default)]
211 pub dependencies: Vec<DeliveryConsoleDependencyObservation>,
212 #[serde(default)]
213 pub failure_domains: BTreeMap<String, bool>,
214 pub scaling_check_passed: Option<bool>,
215 pub disruption_check_passed: Option<bool>,
216 pub availability_check_passed: Option<bool>,
217 #[serde(default)]
218 pub evidence_references: Vec<String>,
219}
220
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
222#[serde(rename_all = "camelCase")]
223pub struct DeliveryConsoleGaEvidence {
224 pub protocol: String,
225 pub evidence_id: String,
226 pub status: String,
227 pub stale: bool,
228 #[serde(default)]
229 pub subjects: BTreeMap<String, String>,
230 #[serde(default)]
231 pub details: BTreeMap<String, Value>,
232 #[serde(default)]
233 pub issue_codes: Vec<String>,
234 #[serde(default)]
235 pub next_actions: Vec<String>,
236}
237
238#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
239#[serde(rename_all = "camelCase")]
240pub struct DeliveryConsoleGaOperations {
241 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub support_manifest: Option<DeliveryConsoleGaEvidence>,
243 #[serde(default)]
244 pub delivery_recovery: Vec<DeliveryConsoleGaEvidence>,
245 #[serde(default, skip_serializing_if = "Option::is_none")]
246 pub restore: Option<DeliveryConsoleGaEvidence>,
247 #[serde(default, skip_serializing_if = "Option::is_none")]
248 pub disaster_recovery: Option<DeliveryConsoleGaEvidence>,
249 #[serde(default, skip_serializing_if = "Option::is_none")]
250 pub performance: Option<DeliveryConsoleGaEvidence>,
251 #[serde(default, skip_serializing_if = "Option::is_none")]
252 pub support_envelope: Option<DeliveryConsoleGaEvidence>,
253 #[serde(default, skip_serializing_if = "Option::is_none")]
254 pub security_review: Option<DeliveryConsoleGaEvidence>,
255 #[serde(default)]
256 pub contract_lifecycle: Vec<DeliveryConsoleGaEvidence>,
257}
258
259#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
260#[serde(rename_all = "camelCase")]
261pub struct DeliveryConsoleProjection {
262 pub protocol: String,
263 pub projection_digest: String,
264 pub state: DeliveryConsoleState,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
266 pub release: Option<DeliveryConsoleRelease>,
267 #[serde(default)]
268 pub supply_chain: Vec<DeliveryConsoleSupplyChainWorkload>,
269 #[serde(default, skip_serializing_if = "Option::is_none")]
270 pub policy: Option<DeliveryConsolePolicy>,
271 pub configuration: DeliveryConsoleConfiguration,
272 #[serde(default)]
273 pub deployments: Vec<DeliveryConsoleDeployment>,
274 #[serde(default, skip_serializing_if = "Option::is_none")]
275 pub edge: Option<DeliveryConsoleEdge>,
276 #[serde(default)]
277 pub adapter_drift: Vec<DeliveryConsoleAdapterDrift>,
278 #[serde(default)]
279 pub promotion_history: Vec<DeliveryConsoleTimelineEntry>,
280 #[serde(default)]
281 pub canary_timeline: Vec<DeliveryConsoleTimelineEntry>,
282 #[serde(default)]
283 pub canary_observations: Vec<DeliveryConsoleCanaryObservation>,
284 #[serde(default)]
285 pub rollback_timeline: Vec<DeliveryConsoleTimelineEntry>,
286 #[serde(default)]
287 pub issues: Vec<DeliveryConsoleIssue>,
288 #[serde(default)]
289 pub next_actions: Vec<String>,
290 #[serde(default)]
291 pub runtime_story_references: Vec<String>,
292 #[serde(default)]
293 pub ga_operations: DeliveryConsoleGaOperations,
294 pub read_only: bool,
295 #[serde(default)]
296 pub apply_actions: Vec<String>,
297}
298
299#[must_use]
300pub fn project_delivery_console(input: DeliveryConsoleArtifacts) -> DeliveryConsoleProjection {
301 let release_artifact = latest(&input.artifacts, "lenso.service-release.v1");
304 let trust_artifact = latest(&input.artifacts, "lenso.release-trust-evidence.v1");
305 let policy_artifact = input
306 .artifacts
307 .iter()
308 .rev()
309 .find(|artifact| protocol(artifact).contains("policy-evidence"));
310 let config_artifact = latest(&input.artifacts, "lenso.config-revision.v1");
311 let config_receipt = latest(&input.artifacts, "lenso.config-activation-receipt.v1");
312 let release = release_artifact.map(|artifact| DeliveryConsoleRelease {
313 service_id: text(artifact, "serviceId").unwrap_or_else(|| "unknown".to_owned()),
314 release_id: text(artifact, "releaseId").unwrap_or_else(|| "unknown".to_owned()),
315 release_digest: text(artifact, "releaseDigest").unwrap_or_else(|| "unknown".to_owned()),
316 });
317 let signature_status = trust_artifact
318 .and_then(|artifact| array(artifact, "signatures").into_iter().next())
319 .and_then(|signature| text(signature, "status"))
320 .unwrap_or_else(|| "unknown".to_owned());
321 let trust_workloads = trust_artifact
322 .map(|artifact| array(artifact, "workloads"))
323 .unwrap_or_default();
324 let mut supply_chain = release_artifact
325 .map(|artifact| array(artifact, "workloads"))
326 .unwrap_or_default()
327 .into_iter()
328 .map(|workload| {
329 let workload_id = text(workload, "workloadId").unwrap_or_else(|| "unknown".to_owned());
330 let trust = trust_workloads
331 .iter()
332 .find(|item| text(item, "workloadId").as_deref() == Some(workload_id.as_str()));
333 DeliveryConsoleSupplyChainWorkload {
334 workload_id,
335 artifact_digest: text(workload, "artifactDigest")
336 .unwrap_or_else(|| "unknown".to_owned()),
337 signature_status: signature_status.clone(),
338 sbom_reference: nested_text(workload, "sbom", "reference")
339 .unwrap_or_else(|| "missing".to_owned()),
340 provenance_reference: nested_text(workload, "provenance", "reference")
341 .unwrap_or_else(|| "missing".to_owned()),
342 provenance_subject_matches: trust
343 .and_then(|item| item.get("provenanceSubjectMatches"))
344 .and_then(Value::as_bool)
345 .unwrap_or(false),
346 }
347 })
348 .collect::<Vec<_>>();
349 supply_chain.sort_by(|left, right| left.workload_id.cmp(&right.workload_id));
350 let policy = policy_artifact.map(|artifact| DeliveryConsolePolicy {
351 evidence_id: text(artifact, "evidenceId").unwrap_or_else(|| "unknown".to_owned()),
352 pack_id: text(artifact, "packId").unwrap_or_else(|| "unknown".to_owned()),
353 decision: text(artifact, "decision").unwrap_or_else(|| "unknown".to_owned()),
354 });
355 let desired_revision_id = config_artifact.and_then(|artifact| text(artifact, "revisionId"));
356 let active_revision_id =
357 config_receipt.and_then(|artifact| match text(artifact, "activation").as_deref() {
358 Some("active" | "rolled_back") => text(artifact, "targetRevisionId"),
359 Some("staged") => text(artifact, "previousRevisionId"),
360 _ => None,
361 });
362 let previous_revision_id =
363 config_receipt.and_then(|artifact| text(artifact, "previousRevisionId"));
364 let mut secret_references = config_artifact
365 .map(|artifact| array(artifact, "secretReferences"))
366 .unwrap_or_default()
367 .into_iter()
368 .map(|reference| DeliveryConsoleSecretReference {
369 reference_id: text(reference, "referenceId").unwrap_or_else(|| "unknown".to_owned()),
370 provider: text(reference, "provider").unwrap_or_else(|| "unknown".to_owned()),
371 purpose: text(reference, "purpose").unwrap_or_else(|| "unknown".to_owned()),
372 scope: text(reference, "scope").unwrap_or_else(|| "unknown".to_owned()),
373 status: text(reference, "status").unwrap_or_else(|| "unknown".to_owned()),
374 rotation_revision: reference
375 .get("metadata")
376 .and_then(|metadata| text(metadata, "rotationRevision")),
377 })
378 .collect::<Vec<_>>();
379 secret_references.sort_by(|left, right| left.reference_id.cmp(&right.reference_id));
380 let mut latest_deployments = std::collections::BTreeMap::new();
381 for artifact in input
382 .artifacts
383 .iter()
384 .filter(|artifact| protocol(artifact) == "lenso.deployment-observation.v1")
385 {
386 let deployment = DeliveryConsoleDeployment {
387 environment: text(artifact, "environment").unwrap_or_else(|| "unknown".to_owned()),
388 desired_release_id: text(artifact, "desiredReleaseId")
389 .unwrap_or_else(|| "unknown".to_owned()),
390 observed_release_id: text(artifact, "observedReleaseId")
391 .unwrap_or_else(|| "unknown".to_owned()),
392 config_revision_id: text(artifact, "configRevisionId")
393 .unwrap_or_else(|| "unknown".to_owned()),
394 drifted: boolean(artifact, "drifted"),
395 fresh: boolean(artifact, "fresh"),
396 };
397 latest_deployments.insert(deployment.environment.clone(), deployment);
398 }
399 let mut deployments = latest_deployments.into_values().collect::<Vec<_>>();
400 deployments.sort_by(|left, right| left.environment.cmp(&right.environment));
401 let adapter_drift = deployments
402 .iter()
403 .map(|deployment| DeliveryConsoleAdapterDrift {
404 environment: deployment.environment.clone(),
405 drifted: deployment.drifted,
406 fresh: deployment.fresh,
407 next_actions: if deployment.drifted || !deployment.fresh {
408 vec![
409 "Refresh adapter observations and reconcile the exact Deployment plan."
410 .to_owned(),
411 ]
412 } else {
413 Vec::new()
414 },
415 })
416 .collect::<Vec<_>>();
417 let edge = latest(&input.artifacts, "lenso.edge-contract.v1").map(|artifact| {
418 let mut public_routes = array(artifact, "routes")
419 .into_iter()
420 .filter_map(|route| text(route, "publicPath"))
421 .collect::<Vec<_>>();
422 public_routes.sort();
423 DeliveryConsoleEdge {
424 contract_id: text(artifact, "edgeContractId").unwrap_or_else(|| "unknown".to_owned()),
425 public_routes,
426 }
427 });
428 let mut timeline = input
429 .artifacts
430 .iter()
431 .filter(|artifact| is_timeline_protocol(protocol(artifact)))
432 .map(timeline_entry)
433 .collect::<Vec<_>>();
434 let mut seen_timeline = BTreeSet::new();
435 timeline.retain(|entry| seen_timeline.insert(entry.clone()));
436 let promotion_history = filter_timeline(&timeline, "promotion");
437 let canary_timeline = filter_timeline(&timeline, "canary");
438 let canary_observations = input
439 .artifacts
440 .iter()
441 .filter(|artifact| protocol(artifact) == "lenso.reliability-observation.v1")
442 .filter_map(canary_observation)
443 .collect::<Vec<_>>();
444 let rollback_timeline = filter_timeline(&timeline, "rollback");
445 let mut issues = input
446 .artifacts
447 .iter()
448 .flat_map(issues_from)
449 .collect::<Vec<_>>();
450 issues.sort();
451 issues.dedup();
452 let mut next_actions = issues
453 .iter()
454 .flat_map(|issue| issue.next_actions.iter().cloned())
455 .collect::<Vec<_>>();
456 next_actions.sort();
457 next_actions.dedup();
458 let mut runtime_story_references = BTreeSet::new();
459 for artifact in &input.artifacts {
460 collect_runtime_story_references(artifact, &mut runtime_story_references);
461 }
462 let configuration = DeliveryConsoleConfiguration {
463 drifted: desired_revision_id != active_revision_id
464 || deployments.iter().any(|deployment| {
465 desired_revision_id
466 .as_deref()
467 .is_some_and(|desired| deployment.config_revision_id != desired)
468 }),
469 desired_revision_id,
470 active_revision_id,
471 previous_revision_id,
472 secret_references,
473 };
474 let state = derive_state(&input.artifacts, !issues.is_empty(), &deployments);
475 let ga_operations = ga_operations(&input.artifacts);
476 let mut projection = DeliveryConsoleProjection {
477 protocol: DELIVERY_CONSOLE_PROJECTION_PROTOCOL.to_owned(),
478 projection_digest: String::new(),
479 state,
480 release,
481 supply_chain,
482 policy,
483 configuration,
484 deployments,
485 edge,
486 adapter_drift,
487 promotion_history,
488 canary_timeline,
489 canary_observations,
490 rollback_timeline,
491 issues,
492 next_actions,
493 runtime_story_references: runtime_story_references.into_iter().collect(),
494 ga_operations,
495 read_only: true,
496 apply_actions: Vec::new(),
497 };
498 projection.projection_digest = digest(&projection);
499 projection
500}
501
502fn ga_operations(artifacts: &[Value]) -> DeliveryConsoleGaOperations {
503 let summaries = |protocol_name: &str| {
504 artifacts
505 .iter()
506 .filter(|artifact| protocol(artifact) == protocol_name)
507 .filter_map(ga_evidence)
508 .collect::<Vec<_>>()
509 };
510 let newest = |protocol_name: &str| summaries(protocol_name).pop();
511 let mut contract_lifecycle = summaries("lenso.contract-retirement-plan.v1");
512 contract_lifecycle.extend(summaries("lenso.contract-retirement-receipt.v2"));
513 DeliveryConsoleGaOperations {
514 support_manifest: newest("lenso.ga-support-manifest.v1"),
515 delivery_recovery: summaries("lenso.delivery-failure-recovery-evidence.v1"),
516 restore: newest("lenso.service-restore-evidence.v1"),
517 disaster_recovery: newest("lenso.disaster-recovery-evidence.v1"),
518 performance: newest("lenso.performance-profile.v1"),
519 support_envelope: newest("lenso.support-envelope.v1"),
520 security_review: newest("lenso.security-review-evidence.v1"),
521 contract_lifecycle,
522 }
523}
524
525fn ga_evidence(artifact: &Value) -> Option<DeliveryConsoleGaEvidence> {
526 let protocol = text(artifact, "protocol")?;
527 let evidence_id = [
528 "evidenceId",
529 "profileId",
530 "envelopeId",
531 "reviewId",
532 "manifestId",
533 "planId",
534 "receiptId",
535 ]
536 .into_iter()
537 .find_map(|key| text(artifact, key))
538 .unwrap_or_else(|| "unknown".to_owned());
539 let status = ["decision", "status", "outcome"]
540 .into_iter()
541 .find_map(|key| text(artifact, key))
542 .unwrap_or_else(|| "unknown".to_owned());
543 let issue_codes = array(artifact, "issues")
544 .into_iter()
545 .filter_map(|issue| text(issue, "code"))
546 .collect::<Vec<_>>();
547 let stale = issue_codes
548 .iter()
549 .any(|code| code.contains("stale") || code.contains("freshness"));
550 let subjects = [
551 "serviceId",
552 "workloadId",
553 "releaseId",
554 "releaseDigest",
555 "configRevisionId",
556 "configRevisionDigest",
557 "contractId",
558 "contractSetDigest",
559 "deploymentId",
560 "storyId",
561 "backupId",
562 "supportManifestDigest",
563 "primaryRegion",
564 "passiveRegion",
565 "phase",
566 "observedRpoMs",
567 "observedRtoMs",
568 "recoveryTimeMs",
569 "intentionalLossBoundMs",
570 "replayBoundCount",
571 "freshnessHorizonUnixMs",
572 "upgradeStatus",
573 "rollbackStatus",
574 ]
575 .into_iter()
576 .filter_map(|key| scalar_text(artifact, key).map(|value| (key.to_owned(), value)))
577 .chain(
578 [
579 ("activeConsumerCount", "activeConsumers"),
580 ("performanceBudgetCount", "budgets"),
581 ("findingCount", "findings"),
582 ("contractVersionCount", "contractVersionDigests"),
583 ("remainingStoryGapCount", "remainingStoryGaps"),
584 ]
585 .into_iter()
586 .filter_map(|(label, key)| {
587 artifact
588 .get(key)
589 .and_then(|value| match value {
590 Value::Array(values) => Some(values.len()),
591 Value::Object(values) => Some(values.len()),
592 _ => None,
593 })
594 .map(|count| (label.to_owned(), count.to_string()))
595 }),
596 )
597 .collect();
598 let details = [
599 "components",
600 "manifestFormats",
601 "stateVersions",
602 "adapterVersions",
603 "combinations",
604 "upgradeEdges",
605 "activeConsumers",
606 "budgets",
607 "measurements",
608 "findings",
609 "contractVersionDigests",
610 "remainingStoryGaps",
611 "reconciliation",
612 "environmentObservation",
613 ]
614 .into_iter()
615 .filter_map(|key| {
616 artifact
617 .get(key)
618 .filter(|value| !value.is_null())
619 .cloned()
620 .map(|value| (key.to_owned(), value))
621 })
622 .collect();
623 Some(DeliveryConsoleGaEvidence {
624 protocol,
625 evidence_id,
626 status,
627 stale,
628 subjects,
629 details,
630 issue_codes,
631 next_actions: strings(artifact, "nextActions"),
632 })
633}
634
635fn scalar_text(value: &Value, key: &str) -> Option<String> {
636 value.get(key).and_then(|value| match value {
637 Value::String(value) => Some(value.clone()),
638 Value::Number(value) => Some(value.to_string()),
639 Value::Bool(value) => Some(value.to_string()),
640 _ => None,
641 })
642}
643
644fn canary_observation(artifact: &Value) -> Option<DeliveryConsoleCanaryObservation> {
645 Some(DeliveryConsoleCanaryObservation {
646 observation_id: text(artifact, "observationId")?,
647 observed_revision: artifact.get("observedRevision")?.as_u64()?,
648 fresh: boolean(artifact, "fresh"),
649 observation_window_seconds: artifact.get("observationWindowSeconds")?.as_u64()?,
650 sample_count: artifact.get("sampleCount")?.as_u64()?,
651 generic_process_healthy: boolean(artifact, "genericProcessHealthy"),
652 workload_readiness: bool_map(artifact, "workloadReadiness"),
653 workload_liveness: bool_map(artifact, "workloadLiveness"),
654 availability_basis_points: artifact
655 .get("availabilityBasisPoints")
656 .and_then(Value::as_u64)
657 .and_then(|value| u32::try_from(value).ok()),
658 latency_p99_ms: artifact.get("latencyP99Ms").and_then(Value::as_u64),
659 error_budget_used_basis_points: artifact
660 .get("errorBudgetUsedBasisPoints")
661 .and_then(Value::as_u64)
662 .and_then(|value| u32::try_from(value).ok()),
663 queue_backlog: artifact.get("queueBacklog").and_then(Value::as_u64),
664 workflow_backlog: artifact.get("workflowBacklog").and_then(Value::as_u64),
665 timer_lag_ms: artifact.get("timerLagMs").and_then(Value::as_u64),
666 retry_exhaustion: artifact.get("retryExhaustion").and_then(Value::as_u64),
667 compensation_pressure: artifact.get("compensationPressure").and_then(Value::as_u64),
668 dependencies: array(artifact, "dependencies")
669 .into_iter()
670 .filter_map(|dependency| {
671 Some(DeliveryConsoleDependencyObservation {
672 dependency_id: text(dependency, "dependencyId")?,
673 available: boolean(dependency, "available"),
674 active_degraded_mode: text(dependency, "activeDegradedMode"),
675 })
676 })
677 .collect(),
678 failure_domains: bool_map(artifact, "failureDomains"),
679 scaling_check_passed: artifact.get("scalingCheckPassed").and_then(Value::as_bool),
680 disruption_check_passed: artifact
681 .get("disruptionCheckPassed")
682 .and_then(Value::as_bool),
683 availability_check_passed: artifact
684 .get("availabilityCheckPassed")
685 .and_then(Value::as_bool),
686 evidence_references: array(artifact, "evidenceReferences")
687 .into_iter()
688 .filter_map(Value::as_str)
689 .map(str::to_owned)
690 .collect(),
691 })
692}
693
694fn bool_map(artifact: &Value, field: &str) -> BTreeMap<String, bool> {
695 artifact
696 .get(field)
697 .and_then(Value::as_object)
698 .into_iter()
699 .flatten()
700 .filter_map(|(key, value)| value.as_bool().map(|value| (key.clone(), value)))
701 .collect()
702}
703
704pub async fn record_delivery_artifact(
706 pool: &sqlx::PgPool,
707 delivery_id: &str,
708 artifact: &Value,
709) -> Result<(), sqlx::Error> {
710 let persisted = persisted_delivery_artifact(artifact)?;
711 sqlx::query(
712 r#"
713 insert into platform.delivery_artifacts
714 (delivery_id, artifact_id, protocol, artifact_digest, artifact_json)
715 values ($1, $2, $3, $4, $5)
716 on conflict (delivery_id, artifact_id, artifact_digest) do nothing
717 "#,
718 )
719 .bind(delivery_id)
720 .bind(artifact_id(&persisted))
721 .bind(protocol(&persisted))
722 .bind(digest(artifact))
723 .bind(&persisted)
724 .execute(pool)
725 .await?;
726 Ok(())
727}
728
729#[must_use]
731pub fn delivery_artifact_batch_subject(delivery_id: &str, artifacts: &[Value]) -> String {
732 extraction_input_digest(
733 serde_json::to_vec(&(DELIVERY_ARTIFACT_BATCH_PROTOCOL, delivery_id, artifacts))
734 .expect("delivery artifact batches must serialize"),
735 )
736}
737
738pub async fn record_delivery_artifacts(
740 pool: &sqlx::PgPool,
741 delivery_id: &str,
742 artifacts: &[Value],
743) -> Result<(), sqlx::Error> {
744 let persisted = artifacts
745 .iter()
746 .map(persisted_delivery_artifact)
747 .collect::<Result<Vec<_>, _>>()?;
748 let mut transaction = pool.begin().await?;
749 for (artifact, persisted) in artifacts.iter().zip(&persisted) {
750 sqlx::query(
751 r#"
752 insert into platform.delivery_artifacts
753 (delivery_id, artifact_id, protocol, artifact_digest, artifact_json)
754 values ($1, $2, $3, $4, $5)
755 on conflict (delivery_id, artifact_id, artifact_digest) do nothing
756 "#,
757 )
758 .bind(delivery_id)
759 .bind(artifact_id(persisted))
760 .bind(protocol(persisted))
761 .bind(digest(artifact))
762 .bind(persisted)
763 .execute(&mut *transaction)
764 .await?;
765 }
766 transaction.commit().await?;
767 Ok(())
768}
769
770fn persisted_delivery_artifact(artifact: &Value) -> Result<Value, sqlx::Error> {
771 let protocol = protocol(artifact);
772 if !artifact.is_object() {
773 return Err(sqlx::Error::Protocol(
774 "delivery artifact must be an identified Lenso protocol object".to_owned(),
775 ));
776 }
777 let persisted = match protocol {
778 SERVICE_RELEASE_PROTOCOL => validated_canonical_artifact::<ServiceRelease>(
779 artifact,
780 "Service Release",
781 service_release_integrity_is_valid,
782 )?,
783 RELEASE_TRUST_EVIDENCE_PROTOCOL => {
784 canonical_artifact::<ReleaseTrustEvidence>(artifact, "Release Trust Evidence")?
785 }
786 POLICY_EVIDENCE_PROTOCOL => validated_canonical_artifact::<PolicyEvidence>(
787 artifact,
788 "Policy Evidence",
789 policy_evidence_integrity_is_valid,
790 )?,
791 CONFIG_REVISION_PROTOCOL => {
792 let revision = canonical_typed_artifact::<ConfigRevision>(artifact, "Config Revision")?;
793 if !config_revision_integrity_is_valid(&revision)
794 || !revision
795 .secret_references
796 .iter()
797 .all(secret_reference_metadata_is_safe)
798 {
799 return Err(sqlx::Error::Protocol(
800 "Config Revision identity, digest, or Secret Reference metadata is invalid"
801 .to_owned(),
802 ));
803 }
804 let mut value = serde_json::to_value(revision).expect("Config Revision serializes");
805 let object = value.as_object_mut().expect("Config Revision is an object");
806 object.insert("values".to_owned(), Value::Object(serde_json::Map::new()));
807 object.insert("valuesRedacted".to_owned(), Value::Bool(true));
808 value
809 }
810 CONFIG_ACTIVATION_RECEIPT_PROTOCOL => {
811 canonical_artifact::<ConfigActivationReceipt>(artifact, "Config Activation Receipt")?
812 }
813 EDGE_CONTRACT_PROTOCOL => validated_canonical_artifact::<EdgeContract>(
814 artifact,
815 "Edge Contract",
816 edge_contract_integrity_is_valid,
817 )?,
818 GATEWAY_PLAN_PROTOCOL => validated_canonical_artifact::<GatewayConfigurationPlan>(
819 artifact,
820 "Gateway Configuration Plan",
821 gateway_plan_integrity_is_valid,
822 )?,
823 GATEWAY_OBSERVATION_PROTOCOL => validated_canonical_artifact::<GatewayObservation>(
824 artifact,
825 "Gateway Observation",
826 gateway_observation_content_integrity_is_valid,
827 )?,
828 DEPLOYMENT_PLAN_PROTOCOL => validated_canonical_artifact::<DeploymentPlan>(
829 artifact,
830 "Deployment Plan",
831 deployment_plan_integrity_is_valid,
832 )?,
833 DEPLOYMENT_RECEIPT_PROTOCOL => {
834 canonical_artifact::<DeploymentReceipt>(artifact, "Deployment Receipt")?
835 }
836 DEPLOYMENT_OBSERVATION_PROTOCOL => validated_canonical_artifact::<DeploymentObservation>(
837 artifact,
838 "Deployment Observation",
839 deployment_observation_content_integrity_is_valid,
840 )?,
841 ENVIRONMENT_VERIFICATION_PROTOCOL => {
842 validated_canonical_artifact::<EnvironmentVerification>(
843 artifact,
844 "Environment Verification",
845 environment_verification_integrity_is_valid,
846 )?
847 }
848 PROMOTION_PLAN_PROTOCOL => validated_canonical_artifact::<PromotionPlan>(
849 artifact,
850 "Promotion Plan",
851 promotion_plan_integrity_is_valid,
852 )?,
853 PROMOTION_APPROVAL_PROTOCOL => {
854 canonical_artifact::<PromotionApproval>(artifact, "Promotion Approval")?
855 }
856 PROMOTION_RECEIPT_PROTOCOL => {
857 canonical_artifact::<PromotionReceipt>(artifact, "Promotion Receipt")?
858 }
859 CANARY_PLAN_PROTOCOL => validated_canonical_artifact::<CanaryPlan>(
860 artifact,
861 "Canary Plan",
862 canary_plan_integrity_is_valid,
863 )?,
864 RELIABILITY_OBSERVATION_PROTOCOL => {
865 canonical_artifact::<ReliabilityObservation>(artifact, "Reliability Observation")?
866 }
867 CANARY_DECISION_PROTOCOL => validated_canonical_artifact::<CanaryDecision>(
868 artifact,
869 "Canary Decision",
870 canary_decision_integrity_is_valid,
871 )?,
872 ROLLBACK_PLAN_PROTOCOL => validated_canonical_artifact::<RollbackPlan>(
873 artifact,
874 "Rollback Plan",
875 rollback_plan_integrity_is_valid,
876 )?,
877 ROLLBACK_RECEIPT_PROTOCOL => {
878 canonical_artifact::<RollbackReceipt>(artifact, "Rollback Receipt")?
879 }
880 DELIVERY_FAILURE_RECOVERY_PROTOCOL => {
881 validated_canonical_artifact::<DeliveryFailureRecoveryEvidence>(
882 artifact,
883 "Delivery Failure Recovery Evidence",
884 delivery_failure_recovery_integrity_is_valid,
885 )?
886 }
887 SERVICE_RESTORE_EVIDENCE_PROTOCOL => {
888 validated_canonical_artifact::<ServiceRestoreEvidence>(
889 artifact,
890 "Service Restore Evidence",
891 service_restore_integrity_is_valid,
892 )?
893 }
894 DISASTER_RECOVERY_EVIDENCE_PROTOCOL => {
895 validated_canonical_artifact::<DisasterRecoveryEvidence>(
896 artifact,
897 "Disaster Recovery Evidence",
898 disaster_recovery_evidence_integrity_is_valid,
899 )?
900 }
901 PERFORMANCE_PROFILE_PROTOCOL => validated_canonical_artifact::<PerformanceProfile>(
902 artifact,
903 "Performance Profile",
904 performance_profile_integrity_is_valid,
905 )?,
906 SUPPORT_ENVELOPE_PROTOCOL => validated_canonical_artifact::<SupportEnvelope>(
907 artifact,
908 "Support Envelope",
909 support_envelope_integrity_is_valid,
910 )?,
911 SECURITY_REVIEW_PROTOCOL => validated_canonical_artifact::<SecurityReviewEvidence>(
912 artifact,
913 "Security Review Evidence",
914 security_review_integrity_is_valid,
915 )?,
916 GA_SUPPORT_MANIFEST_PROTOCOL => validated_canonical_artifact::<GaSupportManifest>(
917 artifact,
918 "GA Support Manifest",
919 ga_support_manifest_integrity_valid,
920 )?,
921 CONTRACT_RETIREMENT_PLAN_PROTOCOL => {
922 validated_canonical_artifact::<ContractRetirementPlan>(
923 artifact,
924 "Contract Retirement Plan",
925 contract_retirement_plan_integrity_is_valid,
926 )?
927 }
928 CONTRACT_RETIREMENT_RECEIPT_PROTOCOL => {
929 validated_canonical_artifact::<ContractRetirementReceipt>(
930 artifact,
931 "Contract Retirement Receipt",
932 contract_retirement_receipt_integrity_is_valid,
933 )?
934 }
935 _ => {
936 return Err(sqlx::Error::Protocol(format!(
937 "unsupported production delivery artifact protocol `{protocol}`"
938 )));
939 }
940 };
941 if delivery_artifact_contains_secret_shaped_field(&persisted) {
942 return Err(sqlx::Error::Protocol(
943 "delivery artifact contains a forbidden secret-shaped field".to_owned(),
944 ));
945 }
946 Ok(persisted)
947}
948
949fn canonical_typed_artifact<T: DeserializeOwned + Serialize>(
950 artifact: &Value,
951 label: &str,
952) -> Result<T, sqlx::Error> {
953 let typed = serde_json::from_value::<T>(artifact.clone())
954 .map_err(|error| sqlx::Error::Protocol(format!("invalid {label} artifact: {error}")))?;
955 let canonical = serde_json::to_value(&typed).expect("typed delivery artifact serializes");
956 if canonical != *artifact {
957 return Err(sqlx::Error::Protocol(format!(
958 "{label} artifact is non-canonical or contains unknown fields"
959 )));
960 }
961 Ok(typed)
962}
963
964fn canonical_artifact<T: DeserializeOwned + Serialize>(
965 artifact: &Value,
966 label: &str,
967) -> Result<Value, sqlx::Error> {
968 canonical_typed_artifact::<T>(artifact, label)?;
969 Ok(artifact.clone())
970}
971
972fn validated_canonical_artifact<T: DeserializeOwned + Serialize>(
973 artifact: &Value,
974 label: &str,
975 is_valid: impl FnOnce(&T) -> bool,
976) -> Result<Value, sqlx::Error> {
977 let typed = canonical_typed_artifact::<T>(artifact, label)?;
978 if !is_valid(&typed) {
979 return Err(sqlx::Error::Protocol(format!(
980 "{label} identity or content digest is invalid"
981 )));
982 }
983 Ok(artifact.clone())
984}
985
986fn delivery_artifact_contains_secret_shaped_field(value: &Value) -> bool {
987 match value {
988 Value::Object(object) => object.iter().any(|(key, value)| {
989 let key = key.to_ascii_lowercase();
990 [
991 "secretvalue",
992 "passwordvalue",
993 "credential",
994 "privatekey",
995 "signingkey",
996 "accesstoken",
997 "token",
998 ]
999 .iter()
1000 .any(|forbidden| key.contains(forbidden))
1001 || delivery_artifact_contains_secret_shaped_field(value)
1002 }),
1003 Value::Array(values) => values
1004 .iter()
1005 .any(delivery_artifact_contains_secret_shaped_field),
1006 _ => false,
1007 }
1008}
1009
1010pub async fn load_delivery_console_projection(
1012 pool: &sqlx::PgPool,
1013 requested_delivery_id: Option<&str>,
1014) -> Result<DeliveryConsoleProjection, sqlx::Error> {
1015 let exists = sqlx::query_scalar::<_, Option<String>>(
1016 "select to_regclass('platform.delivery_artifacts')::text",
1017 )
1018 .fetch_one(pool)
1019 .await?
1020 .is_some();
1021 if !exists {
1022 return Ok(project_delivery_console(DeliveryConsoleArtifacts {
1023 artifacts: Vec::new(),
1024 }));
1025 }
1026 let delivery_id = match requested_delivery_id {
1027 Some(delivery_id) => Some(delivery_id.to_owned()),
1028 None => sqlx::query_scalar::<_, String>(
1029 "select delivery_id from platform.delivery_artifacts order by recorded_at desc, record_index desc, delivery_id desc limit 1",
1030 )
1031 .fetch_optional(pool)
1032 .await?,
1033 };
1034 let Some(delivery_id) = delivery_id else {
1035 return Ok(project_delivery_console(DeliveryConsoleArtifacts {
1036 artifacts: Vec::new(),
1037 }));
1038 };
1039 let artifacts = sqlx::query_scalar::<_, Value>(
1040 "select artifact_json from platform.delivery_artifacts where delivery_id = $1 order by record_index",
1041 )
1042 .bind(delivery_id)
1043 .fetch_all(pool)
1044 .await?;
1045 Ok(project_delivery_console(DeliveryConsoleArtifacts {
1046 artifacts,
1047 }))
1048}
1049
1050fn derive_state(
1051 artifacts: &[Value],
1052 blocked: bool,
1053 deployments: &[DeliveryConsoleDeployment],
1054) -> DeliveryConsoleState {
1055 if let Some(explicit) = latest(artifacts, "lenso.delivery-state.v1")
1056 .and_then(|artifact| text(artifact, "state"))
1057 .and_then(|state| serde_json::from_value(Value::String(state)).ok())
1058 {
1059 return explicit;
1060 }
1061 let converged = !deployments.is_empty()
1062 && deployments
1063 .iter()
1064 .all(|deployment| deployment.fresh && !deployment.drifted);
1065 let latest_lifecycle = artifacts.iter().rev().find(|artifact| {
1066 matches!(
1067 protocol(artifact),
1068 "lenso.rollback-receipt.v1"
1069 | "lenso.rollback-plan.v1"
1070 | "lenso.canary-decision.v1"
1071 | "lenso.promotion-receipt.v1"
1072 | "lenso.promotion-approval.v1"
1073 | "lenso.environment-verification.v1"
1074 )
1075 });
1076 if let Some(artifact) = latest_lifecycle {
1077 match protocol(artifact) {
1078 "lenso.rollback-receipt.v1" => {
1079 if text(artifact, "outcome").as_deref() == Some("intervention_required") {
1080 DeliveryConsoleState::InterventionRequired
1081 } else {
1082 DeliveryConsoleState::RolledBack
1083 }
1084 }
1085 "lenso.rollback-plan.v1" if !boolean(artifact, "automaticAllowed") => {
1086 DeliveryConsoleState::Paused
1087 }
1088 "lenso.rollback-plan.v1" => DeliveryConsoleState::RollingBack,
1089 "lenso.canary-decision.v1" => match text(artifact, "outcome").as_deref() {
1090 Some("rollback") => DeliveryConsoleState::RollingBack,
1091 Some("pause") => DeliveryConsoleState::Paused,
1092 Some("expand" | "hold_degraded") => DeliveryConsoleState::Canary,
1093 Some("converged") => DeliveryConsoleState::Ready,
1094 _ => DeliveryConsoleState::Blocked,
1095 },
1096 "lenso.promotion-receipt.v1" => {
1097 if converged {
1098 DeliveryConsoleState::Ready
1099 } else {
1100 DeliveryConsoleState::Converging
1101 }
1102 }
1103 "lenso.promotion-approval.v1" => DeliveryConsoleState::Approved,
1104 "lenso.environment-verification.v1"
1105 if text(artifact, "decision").as_deref() == Some("passed") =>
1106 {
1107 DeliveryConsoleState::Staged
1108 }
1109 _ => DeliveryConsoleState::Blocked,
1110 }
1111 } else if converged {
1112 DeliveryConsoleState::Ready
1113 } else if blocked {
1114 DeliveryConsoleState::Blocked
1115 } else {
1116 DeliveryConsoleState::Planned
1117 }
1118}
1119
1120fn issues_from(artifact: &Value) -> Vec<DeliveryConsoleIssue> {
1121 ["issues", "remainingRisks"]
1122 .into_iter()
1123 .flat_map(|field| array(artifact, field))
1124 .map(|value| DeliveryConsoleIssue {
1125 code: text(value, "code").unwrap_or_else(|| "unknown".to_owned()),
1126 message: text(value, "message").unwrap_or_else(|| "Unknown delivery issue.".to_owned()),
1127 evidence_references: strings(value, "evidenceReferences"),
1128 remediation: text(value, "remediation")
1129 .unwrap_or_else(|| "Inspect the linked delivery evidence.".to_owned()),
1130 next_actions: strings(value, "nextActions"),
1131 })
1132 .collect()
1133}
1134
1135fn timeline_entry(artifact: &Value) -> DeliveryConsoleTimelineEntry {
1136 let protocol = protocol(artifact).to_owned();
1137 let state = text(artifact, "outcome")
1138 .or_else(|| text(artifact, "decision"))
1139 .or_else(|| text(artifact, "activation"))
1140 .unwrap_or_else(|| "recorded".to_owned());
1141 let mut evidence_references = strings(artifact, "evidenceReferences");
1142 evidence_references.extend(
1143 issues_from(artifact)
1144 .into_iter()
1145 .flat_map(|issue| issue.evidence_references),
1146 );
1147 evidence_references.sort();
1148 evidence_references.dedup();
1149 DeliveryConsoleTimelineEntry {
1150 protocol,
1151 artifact_id: artifact_id(artifact),
1152 state,
1153 evidence_references,
1154 }
1155}
1156
1157fn filter_timeline(
1158 timeline: &[DeliveryConsoleTimelineEntry],
1159 needle: &str,
1160) -> Vec<DeliveryConsoleTimelineEntry> {
1161 timeline
1162 .iter()
1163 .filter(|entry| entry.protocol.contains(needle))
1164 .cloned()
1165 .collect()
1166}
1167
1168fn is_timeline_protocol(protocol: &str) -> bool {
1169 ["promotion", "canary", "rollback", "config-activation"]
1170 .iter()
1171 .any(|needle| protocol.contains(needle))
1172}
1173
1174fn collect_runtime_story_references(value: &Value, found: &mut BTreeSet<String>) {
1175 match value {
1176 Value::String(value) if value.starts_with("runtime-story:") => {
1177 found.insert(value.clone());
1178 }
1179 Value::Array(values) => {
1180 for value in values {
1181 collect_runtime_story_references(value, found);
1182 }
1183 }
1184 Value::Object(values) => {
1185 for value in values.values() {
1186 collect_runtime_story_references(value, found);
1187 }
1188 }
1189 _ => {}
1190 }
1191}
1192
1193fn latest<'a>(artifacts: &'a [Value], expected_protocol: &str) -> Option<&'a Value> {
1194 artifacts
1195 .iter()
1196 .rev()
1197 .find(|artifact| protocol(artifact) == expected_protocol)
1198}
1199
1200fn protocol(value: &Value) -> &str {
1201 value
1202 .get("protocol")
1203 .and_then(Value::as_str)
1204 .unwrap_or("unknown")
1205}
1206
1207fn artifact_id(value: &Value) -> String {
1208 [
1209 "releaseId",
1210 "receiptId",
1211 "decisionId",
1212 "evidenceId",
1213 "verificationId",
1214 "planId",
1215 "revisionId",
1216 "contractId",
1217 "observationId",
1218 "proofId",
1219 ]
1220 .into_iter()
1221 .find_map(|field| text(value, field))
1222 .unwrap_or_else(|| format!("artifact:{}", digest(value)))
1223}
1224
1225fn text(value: &Value, field: &str) -> Option<String> {
1226 value.get(field).and_then(Value::as_str).map(str::to_owned)
1227}
1228
1229fn nested_text(value: &Value, parent: &str, field: &str) -> Option<String> {
1230 value.get(parent).and_then(|value| text(value, field))
1231}
1232
1233fn boolean(value: &Value, field: &str) -> bool {
1234 value.get(field).and_then(Value::as_bool).unwrap_or(false)
1235}
1236
1237fn array<'a>(value: &'a Value, field: &str) -> Vec<&'a Value> {
1238 value
1239 .get(field)
1240 .and_then(Value::as_array)
1241 .map(|values| values.iter().collect())
1242 .unwrap_or_default()
1243}
1244
1245fn strings(value: &Value, field: &str) -> Vec<String> {
1246 array(value, field)
1247 .into_iter()
1248 .filter_map(Value::as_str)
1249 .map(str::to_owned)
1250 .collect()
1251}
1252
1253fn digest(value: &impl Serialize) -> String {
1254 extraction_input_digest(
1255 serde_json::to_vec(value).expect("delivery projection values serialize"),
1256 )
1257}
1258
1259#[cfg(test)]
1260mod persistence_tests {
1261 use super::*;
1262 use std::collections::BTreeMap;
1263
1264 #[test]
1265 fn persistence_rejects_unknown_delivery_protocols() {
1266 let error = persisted_delivery_artifact(&serde_json::json!({
1267 "protocol": "lenso.forged-delivery-object.v1",
1268 "receiptId": "forged:1"
1269 }))
1270 .expect_err("unknown delivery objects must fail closed");
1271
1272 assert!(error.to_string().contains("unsupported"));
1273 }
1274
1275 #[test]
1276 fn persistence_rejects_extra_token_fields_on_typed_receipts() {
1277 let receipt = crate::RollbackReceipt {
1278 protocol: crate::ROLLBACK_RECEIPT_PROTOCOL.to_owned(),
1279 receipt_id: "rollback-receipt:test".to_owned(),
1280 plan_id: "rollback-plan:test".to_owned(),
1281 actor: "automation:test".to_owned(),
1282 outcome: crate::RollbackOutcome::RolledBack,
1283 restored_release_id: "release:previous".to_owned(),
1284 restored_config_revision_id: "config:previous".to_owned(),
1285 environment_revision_before: 7,
1286 environment_revision_after: 8,
1287 exposure_percent: 0,
1288 remaining_risks: Vec::new(),
1289 approval_boundary_required: false,
1290 evidence_references: Vec::new(),
1291 effects: crate::DeliveryEffects {
1292 mutates_environment: true,
1293 mutates_configuration: true,
1294 mutates_gateway: true,
1295 mutates_deployment: true,
1296 appends_ledger: true,
1297 },
1298 };
1299 let mut artifact = serde_json::to_value(receipt).expect("receipt serializes");
1300 artifact
1301 .as_object_mut()
1302 .expect("receipt is an object")
1303 .insert(
1304 "metadata".to_owned(),
1305 serde_json::json!({"token": "forged"}),
1306 );
1307
1308 let error = persisted_delivery_artifact(&artifact)
1309 .expect_err("unknown token-bearing receipt fields must fail closed");
1310 assert!(error.to_string().contains("non-canonical"));
1311 }
1312
1313 #[test]
1314 fn persistence_accepts_integrity_valid_ga_support_and_rejects_tampering() {
1315 let manifest = crate::assemble_ga_support_manifest_with_trust(
1316 crate::GaSupportManifestInput {
1317 status: crate::SupportStatus::Candidate,
1318 components: vec![crate::GaComponent {
1319 kind: crate::ComponentKind::Runtime,
1320 component_id: "lenso-service".to_owned(),
1321 version: "0.1.14".to_owned(),
1322 digest: crate::extraction_input_digest(b"runtime"),
1323 }],
1324 manifest_formats: vec![crate::ManifestFormat {
1325 kind: crate::ManifestKind::Service,
1326 version: "lenso.service.v2".to_owned(),
1327 }],
1328 state_versions: vec!["service-store.v1".to_owned()],
1329 adapter_versions: BTreeMap::from([("postgresql".to_owned(), "18".to_owned())]),
1330 documentation: crate::DocumentationIdentity {
1331 version: "m6-ga".to_owned(),
1332 digest: crate::extraction_input_digest(b"docs"),
1333 },
1334 combinations: vec![crate::SupportCombinationInput {
1335 combination_id: "candidate".to_owned(),
1336 component_references: vec!["runtime:lenso-service@0.1.14".to_owned()],
1337 state_version: "service-store.v1".to_owned(),
1338 status: crate::SupportStatus::Candidate,
1339 }],
1340 upgrade_edges: Vec::new(),
1341 },
1342 crate::EvidenceReceiptTrust {
1343 authorities: BTreeMap::from([(
1344 crate::PERFORMANCE_PROFILE_PROTOCOL.to_owned(),
1345 "test-authority".to_owned(),
1346 )]),
1347 public_keys: BTreeMap::from([(
1348 "test-authority".to_owned(),
1349 "-----BEGIN PUBLIC KEY-----\ntest\n-----END PUBLIC KEY-----".to_owned(),
1350 )]),
1351 },
1352 )
1353 .expect("manifest is valid");
1354 let artifact = serde_json::to_value(&manifest).expect("manifest serializes");
1355 assert_eq!(
1356 persisted_delivery_artifact(&artifact).expect("manifest persists"),
1357 artifact
1358 );
1359
1360 let mut tampered = artifact;
1361 tampered["status"] = serde_json::json!("general_availability");
1362 persisted_delivery_artifact(&tampered).expect_err("tampered manifest fails");
1363 }
1364}