Skip to main content

plan_issue/lifecycle_vnext/
render.rs

1//! Registry-driven lifecycle comment renderer.
2//!
3//! Task 1.1 introduces the boundary; Tasks 3.2 and 6.3 migrate the real
4//! Markdown assembly from [`crate::lifecycle_record`] onto this surface so
5//! `record post`, `record open`, `record attach`, and `tracking checkpoint`
6//! all share one rendering engine per role.
7
8use crate::lifecycle_record::{self, CommentInput, PayloadRole};
9use crate::lifecycle_vnext::registry::{self, RoleSpec};
10
11/// Outcome of rendering one lifecycle comment.
12#[derive(Debug, Clone)]
13pub struct RenderedComment {
14    pub role: PayloadRole,
15    pub spec: &'static RoleSpec,
16    pub body: String,
17}
18
19/// Render a lifecycle comment for the given [`CommentInput`].
20///
21/// Until Task 6.3 migrates the renderer wholesale, this adapter delegates to
22/// the existing [`crate::lifecycle_record::render_comment`] implementation
23/// while exposing the vNext-shaped result. Errors are surfaced verbatim so
24/// upstream callers see the same stable strings.
25pub fn render(input: CommentInput) -> Result<RenderedComment, String> {
26    let role = derive_role(&input);
27    let spec = registry::role(role);
28    let body = lifecycle_record::render_comment(input)?;
29    Ok(RenderedComment { role, spec, body })
30}
31
32fn derive_role(input: &CommentInput) -> PayloadRole {
33    use crate::commands::record::LifecycleCommentKind;
34    match input.kind {
35        LifecycleCommentKind::Source => PayloadRole::Source,
36        LifecycleCommentKind::Plan => PayloadRole::Plan,
37        LifecycleCommentKind::State => PayloadRole::State,
38        LifecycleCommentKind::Session => PayloadRole::Session,
39        LifecycleCommentKind::Validation => PayloadRole::Validation,
40        LifecycleCommentKind::Review => PayloadRole::Review,
41        LifecycleCommentKind::Closeout => PayloadRole::Closeout,
42    }
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48    use crate::commands::record::{LifecycleCommentKind, RecordProfile};
49
50    fn input(kind: LifecycleCommentKind) -> CommentInput {
51        CommentInput {
52            profile: RecordProfile::Tracking,
53            kind,
54            path: Some("plans/demo.md".to_string()),
55            commit: Some("abc123".to_string()),
56            content: Some("visible body".to_string()),
57            title: None,
58            details_summary: None,
59        }
60    }
61
62    #[test]
63    fn render_maps_every_comment_kind_to_registry_role() {
64        let cases = [
65            (LifecycleCommentKind::Source, PayloadRole::Source, "source"),
66            (LifecycleCommentKind::Plan, PayloadRole::Plan, "plan"),
67            (LifecycleCommentKind::State, PayloadRole::State, "state"),
68            (
69                LifecycleCommentKind::Session,
70                PayloadRole::Session,
71                "session",
72            ),
73            (
74                LifecycleCommentKind::Validation,
75                PayloadRole::Validation,
76                "validation",
77            ),
78            (LifecycleCommentKind::Review, PayloadRole::Review, "review"),
79            (
80                LifecycleCommentKind::Closeout,
81                PayloadRole::Closeout,
82                "closeout",
83            ),
84        ];
85
86        for (kind, role, marker_role) in cases {
87            let rendered = render(input(kind)).expect("comment renders");
88            assert_eq!(rendered.role, role);
89            assert_eq!(rendered.spec.role, role);
90            assert_eq!(rendered.spec.marker_role, marker_role);
91            assert!(rendered.body.contains("- Profile: tracking"));
92        }
93    }
94}