1use super::*;
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
8#[serde(rename_all = "kebab-case")]
9pub enum ReleaseSurfaceStateV1 {
10 Supported,
11 Partial,
12 Deferred,
13 Degraded,
14 Blocked,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
18pub struct ReleaseSurfaceV1 {
19 pub surface_id: String,
20 pub state: ReleaseSurfaceStateV1,
21 pub reason: String,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub command: Option<String>,
24}
25
26impl ReleaseSurfaceV1 {
27 pub fn new(
28 surface_id: impl Into<String>,
29 state: ReleaseSurfaceStateV1,
30 reason: impl Into<String>,
31 ) -> Self {
32 Self {
33 surface_id: surface_id.into(),
34 state,
35 reason: reason.into(),
36 command: None,
37 }
38 }
39
40 pub fn with_command(mut self, command: impl Into<String>) -> Self {
41 self.command = Some(command.into());
42 self
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
47pub struct PublicDocFindingV1 {
48 pub path: String,
49 pub line: u32,
50 pub surface_id: String,
51 pub reason_code: String,
52 pub excerpt: String,
53}
54
55impl PublicDocFindingV1 {
56 pub fn scaffold_claim(
57 path: impl Into<String>,
58 line: u32,
59 surface_id: impl Into<String>,
60 excerpt: impl Into<String>,
61 ) -> Self {
62 Self {
63 path: path.into(),
64 line,
65 surface_id: surface_id.into(),
66 reason_code: "public-doc-claims-scaffold-complete".into(),
67 excerpt: excerpt.into(),
68 }
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
73pub struct ExampleAppEntryV1 {
74 pub example_id: ArtifactId,
75 pub path: String,
76 pub profile_id: String,
77 pub provider_kind: String,
78 pub memory_mode: MemoryModeV1,
79 pub status: ReleaseSurfaceStateV1,
80 #[serde(default, skip_serializing_if = "Vec::is_empty")]
81 pub commands: Vec<String>,
82 #[serde(default, skip_serializing_if = "Vec::is_empty")]
83 pub reason_codes: Vec<String>,
84}
85
86impl ExampleAppEntryV1 {
87 pub fn new(
88 path: impl Into<String>,
89 profile_id: impl Into<String>,
90 provider_kind: impl Into<String>,
91 memory_mode: MemoryModeV1,
92 status: ReleaseSurfaceStateV1,
93 ) -> Self {
94 Self {
95 example_id: display_only_unstable_id("example-app"),
96 path: path.into(),
97 profile_id: profile_id.into(),
98 provider_kind: provider_kind.into(),
99 memory_mode,
100 status,
101 commands: Vec::new(),
102 reason_codes: vec!["example-profile-declared".into()],
103 }
104 }
105
106 pub fn with_command(mut self, command: impl Into<String>) -> Self {
107 self.commands.push(command.into());
108 self
109 }
110
111 pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
112 self.reason_codes.push(reason.into());
113 self.reason_codes.sort();
114 self.reason_codes.dedup();
115 self
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
120pub struct ExampleAppManifestV1 {
121 pub manifest_id: ArtifactId,
122 pub kind: ArtifactKindV1,
123 pub examples: Vec<ExampleAppEntryV1>,
124 pub profiles_covered: Vec<String>,
125 pub unsupported_advanced_features: Vec<String>,
126 #[serde(default, skip_serializing_if = "Vec::is_empty")]
127 pub reason_codes: Vec<String>,
128 pub generated_at: DateTime<Utc>,
129}
130
131impl ExampleAppManifestV1 {
132 pub fn new(
133 mut examples: Vec<ExampleAppEntryV1>,
134 mut unsupported_advanced_features: Vec<String>,
135 ) -> Self {
136 examples.sort_by(|left, right| left.path.cmp(&right.path));
137 let mut profiles_covered = examples
138 .iter()
139 .map(|example| example.profile_id.clone())
140 .collect::<Vec<_>>();
141 profiles_covered.sort();
142 profiles_covered.dedup();
143 unsupported_advanced_features.sort();
144 unsupported_advanced_features.dedup();
145 Self {
146 manifest_id: display_only_unstable_id("example-app-manifest"),
147 kind: ArtifactKindV1::ExampleAppManifest,
148 examples,
149 profiles_covered,
150 unsupported_advanced_features,
151 reason_codes: vec!["examples-declare-supported-and-deferred-surfaces".into()],
152 generated_at: Utc::now(),
153 }
154 }
155
156 pub fn covers_profile(&self, profile_id: &str) -> bool {
157 self.profiles_covered
158 .iter()
159 .any(|profile| profile == profile_id)
160 }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
164pub struct InstallSmokeStepV1 {
165 pub step: String,
166 pub command: String,
167 pub passed: bool,
168 pub output_digest: String,
169 #[serde(default, skip_serializing_if = "Vec::is_empty")]
170 pub reason_codes: Vec<String>,
171}
172
173impl InstallSmokeStepV1 {
174 pub fn passed(step: impl Into<String>, command: impl Into<String>, output: &str) -> Self {
175 Self {
176 step: step.into(),
177 command: command.into(),
178 passed: true,
179 output_digest: non_authoritative_text_display_digest(output),
180 reason_codes: vec!["install-smoke-step-passed".into()],
181 }
182 }
183
184 pub fn failed(
185 step: impl Into<String>,
186 command: impl Into<String>,
187 output: &str,
188 reason: impl Into<String>,
189 ) -> Self {
190 Self {
191 step: step.into(),
192 command: command.into(),
193 passed: false,
194 output_digest: non_authoritative_text_display_digest(output),
195 reason_codes: vec![reason.into()],
196 }
197 }
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
201pub struct InstallSmokeReportV1 {
202 pub receipt_id: ArtifactId,
203 pub kind: ArtifactKindV1,
204 pub passed: bool,
205 pub steps: Vec<InstallSmokeStepV1>,
206 #[serde(default, skip_serializing_if = "Vec::is_empty")]
207 pub reason_codes: Vec<String>,
208 pub recorded_at: DateTime<Utc>,
209}
210
211impl InstallSmokeReportV1 {
212 pub fn new(steps: Vec<InstallSmokeStepV1>) -> Self {
213 let passed = steps.iter().all(|step| step.passed);
214 Self {
215 receipt_id: display_only_unstable_id("install-smoke"),
216 kind: ArtifactKindV1::InstallSmokeReport,
217 passed,
218 steps,
219 reason_codes: if passed {
220 vec!["install-smoke-passed".into()]
221 } else {
222 vec!["install-smoke-failed".into()]
223 },
224 recorded_at: Utc::now(),
225 }
226 }
227}
228
229#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
230pub struct OperatorStatusReportV1 {
231 pub report_id: ArtifactId,
232 pub kind: ArtifactKindV1,
233 pub app_id: String,
234 pub config_status: String,
235 pub provider_route_label: String,
236 pub memory_mode: MemoryModeV1,
237 pub receipt_store_configured: bool,
238 pub doctor: AiDENsDoctorReportV1,
239 #[serde(default, skip_serializing_if = "Vec::is_empty")]
240 pub degraded_modes: Vec<String>,
241 #[serde(default, skip_serializing_if = "Vec::is_empty")]
242 pub blocked_modes: Vec<String>,
243 #[serde(default, skip_serializing_if = "Vec::is_empty")]
244 pub next_commands: Vec<String>,
245 pub generated_at: DateTime<Utc>,
246}
247
248impl OperatorStatusReportV1 {
249 pub fn new(
250 app_id: impl Into<String>,
251 config_status: impl Into<String>,
252 provider_route_label: impl Into<String>,
253 memory_mode: MemoryModeV1,
254 receipt_store_configured: bool,
255 doctor: AiDENsDoctorReportV1,
256 ) -> Self {
257 let mut degraded_modes = Vec::new();
258 let mut blocked_modes = Vec::new();
259 for truth in doctor.sections.values().flat_map(|section| section.iter()) {
260 if truth.states.contains(&CapabilityStateV1::Degraded) {
261 degraded_modes.push(truth.capability_id.clone());
262 }
263 if truth.states.contains(&CapabilityStateV1::BlockedByPolicy)
264 || truth.states.contains(&CapabilityStateV1::Unavailable)
265 {
266 blocked_modes.push(truth.capability_id.clone());
267 }
268 }
269 degraded_modes.sort();
270 degraded_modes.dedup();
271 blocked_modes.sort();
272 blocked_modes.dedup();
273 Self {
274 report_id: display_only_unstable_id("operator-status"),
275 kind: ArtifactKindV1::OperatorStatusReport,
276 app_id: app_id.into(),
277 config_status: config_status.into(),
278 provider_route_label: provider_route_label.into(),
279 memory_mode,
280 receipt_store_configured,
281 doctor,
282 degraded_modes,
283 blocked_modes,
284 next_commands: vec![
285 "aidens provider-check --config <config>".into(),
286 "aidens inspect-tools --config <config>".into(),
287 "aidens receipts list --config <config>".into(),
288 ],
289 generated_at: Utc::now(),
290 }
291 }
292
293 pub fn exposes_degraded_modes(&self) -> bool {
294 !self.degraded_modes.is_empty() || !self.blocked_modes.is_empty()
295 }
296}
297
298#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
299pub struct ReleaseReadinessReportV1 {
300 pub report_id: ArtifactId,
301 pub kind: ArtifactKindV1,
302 pub ready: bool,
303 pub surfaces: Vec<ReleaseSurfaceV1>,
304 #[serde(default, skip_serializing_if = "Vec::is_empty")]
305 pub public_doc_findings: Vec<PublicDocFindingV1>,
306 pub example_manifest: ExampleAppManifestV1,
307 pub install_smoke: InstallSmokeReportV1,
308 #[serde(default, skip_serializing_if = "Vec::is_empty")]
309 pub reason_codes: Vec<String>,
310 pub generated_at: DateTime<Utc>,
311}
312
313impl ReleaseReadinessReportV1 {
314 pub fn new(
315 mut surfaces: Vec<ReleaseSurfaceV1>,
316 public_doc_findings: Vec<PublicDocFindingV1>,
317 example_manifest: ExampleAppManifestV1,
318 install_smoke: InstallSmokeReportV1,
319 ) -> Self {
320 surfaces.sort_by(|left, right| left.surface_id.cmp(&right.surface_id));
321 let has_blocked_surface = surfaces
322 .iter()
323 .any(|surface| surface.state == ReleaseSurfaceStateV1::Blocked);
324 let has_degraded_surface = surfaces
325 .iter()
326 .any(|surface| surface.state == ReleaseSurfaceStateV1::Degraded);
327 let ready = public_doc_findings.is_empty()
328 && install_smoke.passed
329 && !has_blocked_surface
330 && !has_degraded_surface;
331 let mut reason_codes = if ready {
332 vec!["release-readiness-passed".into()]
333 } else {
334 vec!["release-readiness-blocked".into()]
335 };
336 if has_blocked_surface {
337 reason_codes.push("blocked-surfaces-present".into());
338 }
339 if has_degraded_surface {
340 reason_codes.push("degraded-surfaces-require-explicit-waiver".into());
341 }
342 reason_codes.sort();
343 reason_codes.dedup();
344 Self {
345 report_id: display_only_unstable_id("release-readiness"),
346 kind: ArtifactKindV1::ReleaseReadinessReport,
347 ready,
348 surfaces,
349 public_doc_findings,
350 example_manifest,
351 install_smoke,
352 reason_codes,
353 generated_at: Utc::now(),
354 }
355 }
356
357 pub fn blocks_release(&self) -> bool {
358 !self.ready
359 }
360}
361
362#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
363#[serde(rename_all = "kebab-case")]
364pub enum CompletionAuditStateV1 {
365 Complete,
366 NearComplete,
367 DeferredHorizon,
368 Blocked,
369}
370
371#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
372#[serde(rename_all = "kebab-case")]
373pub enum PassCompletionStateV1 {
374 Done,
375 Partial,
376 Deferred,
377 Blocked,
378 Waived,
379}
380
381#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
382#[serde(rename_all = "kebab-case")]
383pub enum GateCommandStatusV1 {
384 Passed,
385 Failed,
386 Waived,
387 NotRun,
388}
389
390#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
391pub struct GateCommandResultV1 {
392 pub command: String,
393 pub status: GateCommandStatusV1,
394 #[serde(default, skip_serializing_if = "Option::is_none")]
395 pub exit_code: Option<i32>,
396 #[serde(default, skip_serializing_if = "Option::is_none")]
397 pub output_digest: Option<String>,
398 #[serde(default, skip_serializing_if = "Option::is_none")]
399 pub evidence_ref: Option<String>,
400 #[serde(default, skip_serializing_if = "Vec::is_empty")]
401 pub reason_codes: Vec<String>,
402 pub completed_at: DateTime<Utc>,
403}
404
405impl GateCommandResultV1 {
406 pub fn passed(command: impl Into<String>, evidence_ref: impl Into<String>) -> Self {
407 Self {
408 command: command.into(),
409 status: GateCommandStatusV1::Passed,
410 exit_code: Some(0),
411 output_digest: None,
412 evidence_ref: Some(evidence_ref.into()),
413 reason_codes: vec!["gate-command-passed".into()],
414 completed_at: Utc::now(),
415 }
416 }
417
418 pub fn failed(
419 command: impl Into<String>,
420 exit_code: i32,
421 output: impl Into<String>,
422 reason: impl Into<String>,
423 ) -> Self {
424 Self {
425 command: command.into(),
426 status: GateCommandStatusV1::Failed,
427 exit_code: Some(exit_code),
428 output_digest: Some(non_authoritative_text_display_digest(&output.into())),
429 evidence_ref: None,
430 reason_codes: vec![reason.into()],
431 completed_at: Utc::now(),
432 }
433 }
434
435 pub fn not_run(command: impl Into<String>, reason: impl Into<String>) -> Self {
436 Self {
437 command: command.into(),
438 status: GateCommandStatusV1::NotRun,
439 exit_code: None,
440 output_digest: None,
441 evidence_ref: None,
442 reason_codes: vec![reason.into()],
443 completed_at: Utc::now(),
444 }
445 }
446
447 pub fn waived(command: impl Into<String>, waiver_receipt_id: impl Into<String>) -> Self {
448 Self {
449 command: command.into(),
450 status: GateCommandStatusV1::Waived,
451 exit_code: None,
452 output_digest: None,
453 evidence_ref: Some(waiver_receipt_id.into()),
454 reason_codes: vec!["gate-command-waived".into()],
455 completed_at: Utc::now(),
456 }
457 }
458
459 pub fn is_satisfied(&self) -> bool {
460 matches!(
461 self.status,
462 GateCommandStatusV1::Passed | GateCommandStatusV1::Waived
463 )
464 }
465}
466
467#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
468#[serde(rename_all = "kebab-case")]
469pub enum ReleaseArtifactKindV1 {
470 Source,
471 Document,
472 Schema,
473 Fixture,
474 Example,
475 Script,
476 Ci,
477 Handoff,
478 Manifest,
479 Generated,
480}
481
482#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
483pub struct ReleaseArtifactEntryV1 {
484 pub path: String,
485 pub artifact_kind: ReleaseArtifactKindV1,
486 pub required: bool,
487 #[serde(default, skip_serializing_if = "Option::is_none")]
488 pub content_digest: Option<String>,
489 pub byte_len: u64,
490 #[serde(default, skip_serializing_if = "Vec::is_empty")]
491 pub reason_codes: Vec<String>,
492}
493
494impl ReleaseArtifactEntryV1 {
495 pub fn present(
496 path: impl Into<String>,
497 artifact_kind: ReleaseArtifactKindV1,
498 required: bool,
499 content_digest: impl Into<String>,
500 byte_len: u64,
501 ) -> Self {
502 Self {
503 path: path.into(),
504 artifact_kind,
505 required,
506 content_digest: Some(content_digest.into()),
507 byte_len,
508 reason_codes: vec!["release-artifact-present".into()],
509 }
510 }
511
512 pub fn missing(
513 path: impl Into<String>,
514 artifact_kind: ReleaseArtifactKindV1,
515 required: bool,
516 ) -> Self {
517 Self {
518 path: path.into(),
519 artifact_kind,
520 required,
521 content_digest: None,
522 byte_len: 0,
523 reason_codes: vec!["release-artifact-missing".into()],
524 }
525 }
526}
527
528#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
529pub struct ReleaseArtifactManifestV1 {
530 pub manifest_id: ArtifactId,
531 pub kind: ArtifactKindV1,
532 pub package_name: String,
533 pub source_basis: String,
534 pub artifacts: Vec<ReleaseArtifactEntryV1>,
535 pub missing_required_paths: Vec<String>,
536 pub complete: bool,
537 #[serde(default, skip_serializing_if = "Vec::is_empty")]
538 pub reason_codes: Vec<String>,
539 pub generated_at: DateTime<Utc>,
540}
541
542impl ReleaseArtifactManifestV1 {
543 pub fn new(
544 package_name: impl Into<String>,
545 source_basis: impl Into<String>,
546 mut artifacts: Vec<ReleaseArtifactEntryV1>,
547 ) -> Self {
548 artifacts.sort_by(|left, right| left.path.cmp(&right.path));
549 let missing_required_paths = artifacts
550 .iter()
551 .filter(|artifact| artifact.required && artifact.content_digest.is_none())
552 .map(|artifact| artifact.path.clone())
553 .collect::<Vec<_>>();
554 let complete = missing_required_paths.is_empty();
555 Self {
556 manifest_id: display_only_unstable_id("release-artifact-manifest"),
557 kind: ArtifactKindV1::ReleaseArtifactManifest,
558 package_name: package_name.into(),
559 source_basis: source_basis.into(),
560 artifacts,
561 missing_required_paths,
562 complete,
563 reason_codes: if complete {
564 vec!["release-artifact-manifest-complete".into()]
565 } else {
566 vec!["release-artifact-manifest-missing-required".into()]
567 },
568 generated_at: Utc::now(),
569 }
570 }
571
572 pub fn blocks_release(&self) -> bool {
573 !self.complete
574 }
575}
576
577#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
578pub struct CrossPassTraceabilityRowV1 {
579 pub requirement_id: String,
580 pub pass_id: String,
581 pub requirement: String,
582 pub state: PassCompletionStateV1,
583 pub crates: Vec<String>,
584 pub artifact_families: Vec<String>,
585 pub tests: Vec<String>,
586 pub docs: Vec<String>,
587 pub acceptance_gates: Vec<String>,
588 pub evidence_refs: Vec<String>,
589 #[serde(default, skip_serializing_if = "Vec::is_empty")]
590 pub waiver_receipt_ids: Vec<String>,
591 #[serde(default, skip_serializing_if = "Vec::is_empty")]
592 pub reason_codes: Vec<String>,
593}
594
595impl CrossPassTraceabilityRowV1 {
596 pub fn new(
597 requirement_id: impl Into<String>,
598 pass_id: impl Into<String>,
599 requirement: impl Into<String>,
600 state: PassCompletionStateV1,
601 ) -> Self {
602 Self {
603 requirement_id: requirement_id.into(),
604 pass_id: pass_id.into(),
605 requirement: requirement.into(),
606 state,
607 crates: Vec::new(),
608 artifact_families: Vec::new(),
609 tests: Vec::new(),
610 docs: Vec::new(),
611 acceptance_gates: Vec::new(),
612 evidence_refs: Vec::new(),
613 waiver_receipt_ids: Vec::new(),
614 reason_codes: vec!["traceability-row-declared".into()],
615 }
616 }
617
618 pub fn with_crates(mut self, crates: Vec<String>) -> Self {
619 self.crates = crates;
620 self
621 }
622
623 pub fn with_artifacts(mut self, artifact_families: Vec<String>) -> Self {
624 self.artifact_families = artifact_families;
625 self
626 }
627
628 pub fn with_tests(mut self, tests: Vec<String>) -> Self {
629 self.tests = tests;
630 self
631 }
632
633 pub fn with_docs(mut self, docs: Vec<String>) -> Self {
634 self.docs = docs;
635 self
636 }
637
638 pub fn with_acceptance_gates(mut self, acceptance_gates: Vec<String>) -> Self {
639 self.acceptance_gates = acceptance_gates;
640 self
641 }
642
643 pub fn with_evidence(mut self, evidence_refs: Vec<String>) -> Self {
644 self.evidence_refs = evidence_refs;
645 self
646 }
647
648 pub fn with_waiver(mut self, waiver_receipt_id: impl Into<String>) -> Self {
649 self.waiver_receipt_ids.push(waiver_receipt_id.into());
650 self
651 }
652
653 pub fn is_satisfied(&self) -> bool {
654 matches!(
655 self.state,
656 PassCompletionStateV1::Done | PassCompletionStateV1::Waived
657 )
658 }
659}
660
661#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
662pub struct CrossPassTraceabilityMatrixV1 {
663 pub matrix_id: ArtifactId,
664 pub kind: ArtifactKindV1,
665 pub rows: Vec<CrossPassTraceabilityRowV1>,
666 pub uncovered_requirement_ids: Vec<String>,
667 pub complete: bool,
668 #[serde(default, skip_serializing_if = "Vec::is_empty")]
669 pub reason_codes: Vec<String>,
670 pub generated_at: DateTime<Utc>,
671}
672
673impl CrossPassTraceabilityMatrixV1 {
674 pub fn new(mut rows: Vec<CrossPassTraceabilityRowV1>) -> Self {
675 rows.sort_by(|left, right| left.requirement_id.cmp(&right.requirement_id));
676 let uncovered_requirement_ids = rows
677 .iter()
678 .filter(|row| {
679 !row.is_satisfied()
680 || row.acceptance_gates.is_empty()
681 || row.docs.is_empty()
682 || row.tests.is_empty()
683 || row.artifact_families.is_empty()
684 })
685 .map(|row| row.requirement_id.clone())
686 .collect::<Vec<_>>();
687 let complete = !rows.is_empty() && uncovered_requirement_ids.is_empty();
688 Self {
689 matrix_id: display_only_unstable_id("cross-pass-traceability-matrix"),
690 kind: ArtifactKindV1::CrossPassTraceabilityMatrix,
691 rows,
692 uncovered_requirement_ids,
693 complete,
694 reason_codes: if complete {
695 vec!["cross-pass-traceability-complete".into()]
696 } else {
697 vec!["cross-pass-traceability-has-gaps".into()]
698 },
699 generated_at: Utc::now(),
700 }
701 }
702
703 pub fn blocks_completion(&self) -> bool {
704 !self.complete
705 }
706}
707
708#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
709pub struct KnownLimitationV1 {
710 pub limitation_id: ArtifactId,
711 pub surface_id: String,
712 pub state: ReleaseSurfaceStateV1,
713 pub description: String,
714 pub impact: String,
715 #[serde(default, skip_serializing_if = "Option::is_none")]
716 pub workaround: Option<String>,
717 #[serde(default, skip_serializing_if = "Option::is_none")]
718 pub review_after_pass: Option<String>,
719 #[serde(default, skip_serializing_if = "Vec::is_empty")]
720 pub reason_codes: Vec<String>,
721}
722
723impl KnownLimitationV1 {
724 pub fn deferred(
725 surface_id: impl Into<String>,
726 description: impl Into<String>,
727 impact: impl Into<String>,
728 ) -> Self {
729 Self {
730 limitation_id: display_only_unstable_id("known-limitation"),
731 surface_id: surface_id.into(),
732 state: ReleaseSurfaceStateV1::Deferred,
733 description: description.into(),
734 impact: impact.into(),
735 workaround: None,
736 review_after_pass: None,
737 reason_codes: vec!["limitation-deferred-not-hidden".into()],
738 }
739 }
740
741 pub fn partial(
742 surface_id: impl Into<String>,
743 description: impl Into<String>,
744 impact: impl Into<String>,
745 ) -> Self {
746 Self {
747 limitation_id: display_only_unstable_id("known-limitation"),
748 surface_id: surface_id.into(),
749 state: ReleaseSurfaceStateV1::Partial,
750 description: description.into(),
751 impact: impact.into(),
752 workaround: None,
753 review_after_pass: None,
754 reason_codes: vec!["limitation-partial-not-hidden".into()],
755 }
756 }
757
758 pub fn blocked(
759 surface_id: impl Into<String>,
760 description: impl Into<String>,
761 impact: impl Into<String>,
762 reason: impl Into<String>,
763 ) -> Self {
764 Self {
765 limitation_id: display_only_unstable_id("known-limitation"),
766 surface_id: surface_id.into(),
767 state: ReleaseSurfaceStateV1::Blocked,
768 description: description.into(),
769 impact: impact.into(),
770 workaround: None,
771 review_after_pass: None,
772 reason_codes: vec![reason.into()],
773 }
774 }
775
776 pub fn with_workaround(mut self, workaround: impl Into<String>) -> Self {
777 self.workaround = Some(workaround.into());
778 self
779 }
780
781 pub fn with_review_after_pass(mut self, pass_id: impl Into<String>) -> Self {
782 self.review_after_pass = Some(pass_id.into());
783 self
784 }
785}
786
787#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
788pub struct KnownLimitationsRegisterV1 {
789 pub register_id: ArtifactId,
790 pub kind: ArtifactKindV1,
791 pub current: bool,
792 pub limitations: Vec<KnownLimitationV1>,
793 #[serde(default, skip_serializing_if = "Vec::is_empty")]
794 pub blocking_limitation_ids: Vec<ArtifactId>,
795 #[serde(default, skip_serializing_if = "Vec::is_empty")]
796 pub reason_codes: Vec<String>,
797 pub generated_at: DateTime<Utc>,
798}
799
800impl KnownLimitationsRegisterV1 {
801 pub fn new(mut limitations: Vec<KnownLimitationV1>) -> Self {
802 limitations.sort_by(|left, right| left.surface_id.cmp(&right.surface_id));
803 let blocking_limitation_ids = limitations
804 .iter()
805 .filter(|limitation| limitation.state == ReleaseSurfaceStateV1::Blocked)
806 .map(|limitation| limitation.limitation_id.clone())
807 .collect::<Vec<_>>();
808 let empty_register = limitations.is_empty();
809 let current = true;
810 Self {
811 register_id: display_only_unstable_id("known-limitations-register"),
812 kind: ArtifactKindV1::KnownLimitationsRegister,
813 current,
814 limitations,
815 blocking_limitation_ids,
816 reason_codes: if current {
817 if empty_register {
818 vec!["known-limitations-empty-register-current".into()]
819 } else {
820 vec!["known-limitations-current".into()]
821 }
822 } else {
823 vec!["known-limitations-register-empty".into()]
824 },
825 generated_at: Utc::now(),
826 }
827 }
828
829 pub fn blocks_completion(&self) -> bool {
830 !self.blocking_limitation_ids.is_empty()
831 }
832}
833
834#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
835#[serde(rename_all = "kebab-case")]
836pub enum RegressionDebtStatusV1 {
837 Guarded,
838 Accepted,
839 Deferred,
840 Blocked,
841}
842
843#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
844pub struct RegressionDebtItemV1 {
845 pub debt_id: ArtifactId,
846 pub surface_id: String,
847 pub status: RegressionDebtStatusV1,
848 pub description: String,
849 pub detection: String,
850 pub guardrail_tests: Vec<String>,
851 pub owner: String,
852 #[serde(default, skip_serializing_if = "Vec::is_empty")]
853 pub reason_codes: Vec<String>,
854}
855
856impl RegressionDebtItemV1 {
857 pub fn guarded(
858 surface_id: impl Into<String>,
859 description: impl Into<String>,
860 detection: impl Into<String>,
861 guardrail_tests: Vec<String>,
862 ) -> Self {
863 Self {
864 debt_id: display_only_unstable_id("regression-debt"),
865 surface_id: surface_id.into(),
866 status: RegressionDebtStatusV1::Guarded,
867 description: description.into(),
868 detection: detection.into(),
869 guardrail_tests,
870 owner: "release".into(),
871 reason_codes: vec!["regression-debt-guarded".into()],
872 }
873 }
874
875 pub fn blocked(
876 surface_id: impl Into<String>,
877 description: impl Into<String>,
878 detection: impl Into<String>,
879 ) -> Self {
880 Self {
881 debt_id: display_only_unstable_id("regression-debt"),
882 surface_id: surface_id.into(),
883 status: RegressionDebtStatusV1::Blocked,
884 description: description.into(),
885 detection: detection.into(),
886 guardrail_tests: Vec::new(),
887 owner: "release".into(),
888 reason_codes: vec!["regression-debt-blocks-release".into()],
889 }
890 }
891}
892
893#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
894pub struct RegressionDebtLedgerV1 {
895 pub ledger_id: ArtifactId,
896 pub kind: ArtifactKindV1,
897 pub items: Vec<RegressionDebtItemV1>,
898 #[serde(default, skip_serializing_if = "Vec::is_empty")]
899 pub blocking_debt_ids: Vec<ArtifactId>,
900 #[serde(default, skip_serializing_if = "Vec::is_empty")]
901 pub reason_codes: Vec<String>,
902 pub generated_at: DateTime<Utc>,
903}
904
905impl RegressionDebtLedgerV1 {
906 pub fn new(mut items: Vec<RegressionDebtItemV1>) -> Self {
907 items.sort_by(|left, right| left.surface_id.cmp(&right.surface_id));
908 let blocking_debt_ids = items
909 .iter()
910 .filter(|item| item.status == RegressionDebtStatusV1::Blocked)
911 .map(|item| item.debt_id.clone())
912 .collect::<Vec<_>>();
913 let reason_codes = if blocking_debt_ids.is_empty() {
914 vec!["regression-debt-ledger-non-blocking".into()]
915 } else {
916 vec!["regression-debt-ledger-blocking".into()]
917 };
918 Self {
919 ledger_id: display_only_unstable_id("regression-debt-ledger"),
920 kind: ArtifactKindV1::RegressionDebtLedger,
921 items,
922 blocking_debt_ids,
923 reason_codes,
924 generated_at: Utc::now(),
925 }
926 }
927
928 pub fn blocks_completion(&self) -> bool {
929 !self.blocking_debt_ids.is_empty()
930 }
931}
932
933#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
934pub struct CompletionAuditReportV1 {
935 pub report_id: ArtifactId,
936 pub kind: ArtifactKindV1,
937 pub completion_state: CompletionAuditStateV1,
938 pub release_bar_passed: bool,
939 pub source_basis: String,
940 pub gate_results: Vec<GateCommandResultV1>,
941 pub release_readiness: ReleaseReadinessReportV1,
942 pub traceability_matrix: CrossPassTraceabilityMatrixV1,
943 pub release_artifact_manifest: ReleaseArtifactManifestV1,
944 pub known_limitations: KnownLimitationsRegisterV1,
945 pub regression_debt: RegressionDebtLedgerV1,
946 pub partial_surfaces: Vec<String>,
947 pub deferred_surfaces: Vec<String>,
948 pub blocked_surfaces: Vec<String>,
949 #[serde(default, skip_serializing_if = "Vec::is_empty")]
950 pub waiver_receipt_ids: Vec<String>,
951 #[serde(default, skip_serializing_if = "Vec::is_empty")]
952 pub reason_codes: Vec<String>,
953 pub generated_at: DateTime<Utc>,
954}
955
956impl CompletionAuditReportV1 {
957 pub fn new(
958 source_basis: impl Into<String>,
959 gate_results: Vec<GateCommandResultV1>,
960 release_readiness: ReleaseReadinessReportV1,
961 traceability_matrix: CrossPassTraceabilityMatrixV1,
962 release_artifact_manifest: ReleaseArtifactManifestV1,
963 known_limitations: KnownLimitationsRegisterV1,
964 regression_debt: RegressionDebtLedgerV1,
965 ) -> Self {
966 let source_basis = source_basis.into();
967 let required_common_gate_commands = [
968 "cargo fmt --all --check",
969 "cargo check --workspace --all-targets --all-features",
970 "cargo test --workspace --all-targets --all-features",
971 "cargo clippy --workspace --all-targets --all-features -- -D warnings",
972 ];
973 let verifier_gate_commands = [
974 "bash scripts/p24_verify.sh",
975 "P24_REQUIRE_CARGO=1 bash scripts/p24_verify.sh",
976 "P24_PACKAGE_SELF_REPLAY=target/p24/package/AiDENs-p24-codex-context.zip bash scripts/p24_verify.sh",
977 "P22_REQUIRE_CARGO=1 bash scripts/p22_verify.sh",
978 ];
979 let gate_map = gate_results
980 .iter()
981 .map(|gate| (gate.command.as_str(), gate.is_satisfied()))
982 .collect::<BTreeMap<_, _>>();
983 let gates_satisfied = required_common_gate_commands
984 .iter()
985 .all(|command| gate_map.get(command).copied().unwrap_or(false))
986 && verifier_gate_commands
987 .iter()
988 .any(|command| gate_map.get(command).copied().unwrap_or(false));
989 let partial_surfaces = release_readiness
990 .surfaces
991 .iter()
992 .filter(|surface| surface.state == ReleaseSurfaceStateV1::Partial)
993 .map(|surface| surface.surface_id.clone())
994 .chain(
995 known_limitations
996 .limitations
997 .iter()
998 .filter(|limitation| limitation.state == ReleaseSurfaceStateV1::Partial)
999 .map(|limitation| limitation.surface_id.clone()),
1000 )
1001 .collect::<BTreeSet<_>>()
1002 .into_iter()
1003 .collect::<Vec<_>>();
1004 let deferred_surfaces = release_readiness
1005 .surfaces
1006 .iter()
1007 .filter(|surface| surface.state == ReleaseSurfaceStateV1::Deferred)
1008 .map(|surface| surface.surface_id.clone())
1009 .chain(
1010 known_limitations
1011 .limitations
1012 .iter()
1013 .filter(|limitation| limitation.state == ReleaseSurfaceStateV1::Deferred)
1014 .map(|limitation| limitation.surface_id.clone()),
1015 )
1016 .collect::<BTreeSet<_>>()
1017 .into_iter()
1018 .collect::<Vec<_>>();
1019 let blocked_surfaces = release_readiness
1020 .surfaces
1021 .iter()
1022 .filter(|surface| surface.state == ReleaseSurfaceStateV1::Blocked)
1023 .map(|surface| surface.surface_id.clone())
1024 .chain(
1025 known_limitations
1026 .limitations
1027 .iter()
1028 .filter(|limitation| limitation.state == ReleaseSurfaceStateV1::Blocked)
1029 .map(|limitation| limitation.surface_id.clone()),
1030 )
1031 .collect::<BTreeSet<_>>()
1032 .into_iter()
1033 .collect::<Vec<_>>();
1034 let hard_release_bar_clear = gates_satisfied
1035 && !release_readiness.blocks_release()
1036 && !traceability_matrix.blocks_completion()
1037 && !release_artifact_manifest.blocks_release()
1038 && !known_limitations.blocks_completion()
1039 && !regression_debt.blocks_completion()
1040 && blocked_surfaces.is_empty();
1041 let completion_state = if !hard_release_bar_clear {
1042 CompletionAuditStateV1::Blocked
1043 } else if !deferred_surfaces.is_empty() {
1044 CompletionAuditStateV1::DeferredHorizon
1045 } else if !partial_surfaces.is_empty() {
1046 CompletionAuditStateV1::NearComplete
1047 } else {
1048 CompletionAuditStateV1::Complete
1049 };
1050 let release_bar_passed = completion_state == CompletionAuditStateV1::Complete;
1051 let reason_codes = if release_bar_passed {
1052 vec!["release-bar-passed".into()]
1053 } else if completion_state == CompletionAuditStateV1::DeferredHorizon {
1054 vec![
1055 "release-bar-blocked".into(),
1056 "deferred-horizon-surfaces-disclosed".into(),
1057 ]
1058 } else {
1059 vec!["release-bar-blocked".into()]
1060 };
1061 Self {
1062 report_id: display_only_unstable_id("completion-audit-report"),
1063 kind: ArtifactKindV1::CompletionAuditReport,
1064 completion_state,
1065 release_bar_passed,
1066 source_basis,
1067 gate_results,
1068 release_readiness,
1069 traceability_matrix,
1070 release_artifact_manifest,
1071 known_limitations,
1072 regression_debt,
1073 partial_surfaces,
1074 deferred_surfaces,
1075 blocked_surfaces,
1076 waiver_receipt_ids: Vec::new(),
1077 reason_codes,
1078 generated_at: Utc::now(),
1079 }
1080 }
1081
1082 pub fn blocks_completion(&self) -> bool {
1083 !self.release_bar_passed
1084 }
1085}