Skip to main content

sim_lib_view_agent/
change_capsule.rs

1//! Change Capsule Scene for reviewable Atelier edits.
2
3use sim_kernel::{Expr, Symbol};
4use sim_lib_scene::{data_map, node, sym};
5use sim_value::build::uint;
6
7/// Change Capsule lens id.
8pub const CHANGE_CAPSULE_LENS: &str = "view:agent-change-capsule";
9
10/// Full state rendered by the Change Capsule view.
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct ChangeCapsuleViewState {
13    /// Capsule id.
14    pub id: Symbol,
15    /// Diff summaries grouped by repository.
16    pub diffs: Vec<CapsuleDiff>,
17    /// Validation and docs logs.
18    pub logs: Vec<CapsuleLog>,
19    /// Generated-docs summary.
20    pub generated_docs: Vec<GeneratedDocsSummary>,
21    /// Planned pin updates.
22    pub pin_plan: Vec<PinPlanView>,
23    /// F6 fairness and attribution facet.
24    pub fairness: CapsuleFairnessFacet,
25    /// Replayable Dev Cassette summary.
26    pub replay: CapsuleReplaySummary,
27}
28
29/// One diff row.
30#[derive(Clone, Debug, PartialEq, Eq)]
31pub struct CapsuleDiff {
32    /// Repository name.
33    pub repo: String,
34    /// Changed path.
35    pub path: String,
36    /// Diff summary.
37    pub summary: String,
38}
39
40/// One validation or docs log row.
41#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct CapsuleLog {
43    /// Log kind.
44    pub kind: String,
45    /// Command label.
46    pub label: String,
47    /// Outcome token.
48    pub outcome: String,
49    /// Evidence log path.
50    pub log_path: String,
51}
52
53/// Generated-docs summary for one artifact.
54#[derive(Clone, Debug, PartialEq, Eq)]
55pub struct GeneratedDocsSummary {
56    /// Repository name.
57    pub repo: String,
58    /// Generated path.
59    pub path: String,
60    /// Generator command.
61    pub generator: String,
62    /// Whether hand-edit policy is satisfied.
63    pub regenerated: bool,
64}
65
66/// One pin update entry.
67#[derive(Clone, Debug, PartialEq, Eq)]
68pub struct PinPlanView {
69    /// Repository name.
70    pub repo: String,
71    /// Current pinned commit.
72    pub current_commit: String,
73    /// New pinned commit.
74    pub new_commit: String,
75    /// Whether the new commit exists on the upstream remote before pinning.
76    pub pushed_commit_exists: bool,
77}
78
79/// F6 fairness and attribution facet.
80#[derive(Clone, Debug, PartialEq, Eq)]
81pub struct CapsuleFairnessFacet {
82    /// Facet label.
83    pub label: String,
84    /// Evidence summary.
85    pub evidence: String,
86    /// Confidence token.
87    pub confidence: String,
88}
89
90/// Dev Cassette replay summary.
91#[derive(Clone, Debug, PartialEq, Eq)]
92pub struct CapsuleReplaySummary {
93    /// Recorded content hash.
94    pub content_hash: String,
95    /// Replay-computed content hash.
96    pub replay_content_hash: String,
97    /// Replay events.
98    pub events: Vec<CapsuleReplayEvent>,
99}
100
101/// One replay event.
102#[derive(Clone, Debug, PartialEq, Eq)]
103pub struct CapsuleReplayEvent {
104    /// Sequence number.
105    pub sequence: u64,
106    /// Event kind.
107    pub kind: String,
108    /// Event summary.
109    pub summary: String,
110}
111
112/// Renders a Change Capsule into a Scene value.
113pub fn change_capsule_view(state: &ChangeCapsuleViewState) -> Expr {
114    node(
115        "stack",
116        vec![
117            ("role", sym("change-capsule")),
118            ("capsule", Expr::Symbol(state.id.clone())),
119            ("dir", sym("column")),
120            (
121                "children",
122                Expr::List(vec![
123                    capsule_summary(state),
124                    diff_panel(&state.diffs),
125                    logs_panel(&state.logs),
126                    generated_docs_panel(&state.generated_docs),
127                    pin_plan_panel(&state.pin_plan),
128                    fairness_panel(&state.fairness),
129                    replay_panel(&state.replay),
130                ]),
131            ),
132        ],
133    )
134}
135
136/// Builds one replay frame per cassette event plus the initial frame.
137pub fn change_capsule_replay_frames(state: &ChangeCapsuleViewState) -> Vec<Expr> {
138    (0..=state.replay.events.len())
139        .map(|count| {
140            let mut frame = state.clone();
141            frame.replay.events.truncate(count);
142            change_capsule_view(&frame)
143        })
144        .collect()
145}
146
147/// Deterministic fake state for UI and replay tests.
148pub fn fake_change_capsule_state() -> ChangeCapsuleViewState {
149    ChangeCapsuleViewState {
150        id: Symbol::qualified("atelier/capsule", "fixture"),
151        diffs: vec![
152            CapsuleDiff {
153                repo: "sim-agent-net".to_owned(),
154                path: "crates/sim-lib-agent/src/atelier/capsule.rs".to_owned(),
155                summary: "agent-side capsule review model".to_owned(),
156            },
157            CapsuleDiff {
158                repo: "sim-tooling".to_owned(),
159                path: "src/atelier/capsule.rs".to_owned(),
160                summary: "generated capsule cache".to_owned(),
161            },
162        ],
163        logs: vec![
164            CapsuleLog {
165                kind: "validation".to_owned(),
166                label: "agent-capsule-tests".to_owned(),
167                outcome: "passed".to_owned(),
168                log_path: ".sim/atelier/logs/agent-capsule-tests.log".to_owned(),
169            },
170            CapsuleLog {
171                kind: "docs".to_owned(),
172                label: "simdoc-check".to_owned(),
173                outcome: "passed".to_owned(),
174                log_path: ".sim/atelier/logs/simdoc-check.log".to_owned(),
175            },
176        ],
177        generated_docs: vec![GeneratedDocsSummary {
178            repo: "repo-docs".to_owned(),
179            path: "docs/site/repos.md".to_owned(),
180            generator: "simctl site".to_owned(),
181            regenerated: true,
182        }],
183        pin_plan: vec![PinPlanView {
184            repo: "sim-agent-net".to_owned(),
185            current_commit: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_owned(),
186            new_commit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned(),
187            pushed_commit_exists: true,
188        }],
189        fairness: CapsuleFairnessFacet {
190            label: "F6 trade-off".to_owned(),
191            evidence: "diff, validation, docs, pin, replay, risk, and rollback evidence".to_owned(),
192            confidence: "0.92".to_owned(),
193        },
194        replay: CapsuleReplaySummary {
195            content_hash: "fnv1a64:1111111111111111".to_owned(),
196            replay_content_hash: "fnv1a64:1111111111111111".to_owned(),
197            events: vec![
198                CapsuleReplayEvent {
199                    sequence: 0,
200                    kind: "edit".to_owned(),
201                    summary: "Patch capsule model".to_owned(),
202                },
203                CapsuleReplayEvent {
204                    sequence: 1,
205                    kind: "validate".to_owned(),
206                    summary: "Validation passed".to_owned(),
207                },
208                CapsuleReplayEvent {
209                    sequence: 2,
210                    kind: "pin".to_owned(),
211                    summary: "Pushed commit exists before pin".to_owned(),
212                },
213            ],
214        },
215    }
216}
217
218fn capsule_summary(state: &ChangeCapsuleViewState) -> Expr {
219    node(
220        "box",
221        vec![
222            ("role", sym("capsule-summary")),
223            (
224                "children",
225                Expr::List(vec![
226                    text(format!("diffs: {}", state.diffs.len())),
227                    text(format!("logs: {}", state.logs.len())),
228                    text(format!("pin updates: {}", state.pin_plan.len())),
229                    text(format!("replay hash: {}", state.replay.replay_content_hash)),
230                ]),
231            ),
232        ],
233    )
234}
235
236fn diff_panel(diffs: &[CapsuleDiff]) -> Expr {
237    panel(
238        "capsule-diff",
239        diffs
240            .iter()
241            .map(|diff| format!("{}:{} {}", diff.repo, diff.path, diff.summary)),
242    )
243}
244
245fn logs_panel(logs: &[CapsuleLog]) -> Expr {
246    panel(
247        "capsule-logs",
248        logs.iter().map(|log| {
249            format!(
250                "{} {} {} {}",
251                log.kind, log.label, log.outcome, log.log_path
252            )
253        }),
254    )
255}
256
257fn generated_docs_panel(docs: &[GeneratedDocsSummary]) -> Expr {
258    panel(
259        "generated-docs",
260        docs.iter().map(|doc| {
261            format!(
262                "{}:{} {} regenerated={}",
263                doc.repo, doc.path, doc.generator, doc.regenerated
264            )
265        }),
266    )
267}
268
269fn pin_plan_panel(pins: &[PinPlanView]) -> Expr {
270    let rows = pins
271        .iter()
272        .map(|pin| {
273            data_map(vec![
274                ("repo", Expr::String(pin.repo.clone())),
275                ("current", Expr::String(pin.current_commit.clone())),
276                ("new", Expr::String(pin.new_commit.clone())),
277                ("pushed", Expr::Bool(pin.pushed_commit_exists)),
278            ])
279        })
280        .collect();
281    node(
282        "box",
283        vec![
284            ("role", sym("pin-plan")),
285            (
286                "children",
287                Expr::List(vec![node("table", vec![("rows", Expr::List(rows))])]),
288            ),
289        ],
290    )
291}
292
293fn fairness_panel(facet: &CapsuleFairnessFacet) -> Expr {
294    node(
295        "box",
296        vec![
297            ("role", sym("fairness-facet")),
298            (
299                "children",
300                Expr::List(vec![
301                    text(facet.label.clone()),
302                    text(facet.evidence.clone()),
303                    text(format!("confidence: {}", facet.confidence)),
304                ]),
305            ),
306        ],
307    )
308}
309
310fn replay_panel(replay: &CapsuleReplaySummary) -> Expr {
311    let events = replay
312        .events
313        .iter()
314        .map(|event| {
315            data_map(vec![
316                ("at", uint(event.sequence)),
317                (
318                    "event",
319                    Expr::Symbol(Symbol::qualified("ide/event", event.kind.as_str())),
320                ),
321                ("label", Expr::String(event.summary.clone())),
322            ])
323        })
324        .collect();
325    node(
326        "box",
327        vec![
328            ("role", sym("replay-cassette")),
329            (
330                "children",
331                Expr::List(vec![
332                    text(format!("content hash: {}", replay.content_hash)),
333                    text(format!("replay hash: {}", replay.replay_content_hash)),
334                    node(
335                        "timeline",
336                        vec![
337                            ("lane", sym("dev-cassette")),
338                            ("events", Expr::List(events)),
339                        ],
340                    ),
341                    node(
342                        "slider",
343                        vec![
344                            ("target", sym("replay-cassette")),
345                            ("value", uint(replay.events.len() as u64)),
346                            ("max", uint(replay.events.len() as u64)),
347                        ],
348                    ),
349                ]),
350            ),
351        ],
352    )
353}
354
355fn panel(role: &str, rows: impl Iterator<Item = String>) -> Expr {
356    node(
357        "box",
358        vec![
359            ("role", sym(role)),
360            ("children", Expr::List(rows.map(text).collect())),
361        ],
362    )
363}
364
365fn text(value: String) -> Expr {
366    node("text", vec![("text", Expr::String(value))])
367}