Skip to main content

perspt_coding/
prompts.rs

1//! The coding domain's prompt section library (PSP-10 systems 23 and 26).
2//!
3//! `branch_correct` renders a typed `CorrectionPacket` for the model:
4//! paths, symbols, spans, and rationale all survive — the correction
5//! channel no longer flattens to one instruction string.
6
7use perspt_sdk::error::Result;
8use perspt_sdk::prompt::{BoundedList, BoundedText, PromptSection};
9use perspt_sdk::CorrectionPacket;
10
11mod generated {
12    include!(concat!(env!("OUT_DIR"), "/prompt_sections.rs"));
13}
14
15pub use generated::branch_correct;
16
17fn bounded_items(
18    items: impl IntoIterator<Item = String>,
19    count: usize,
20    bytes: usize,
21) -> Vec<String> {
22    items
23        .into_iter()
24        .map(|item| {
25            let mut item = item;
26            if item.len() > bytes {
27                let mut end = bytes.saturating_sub(1);
28                while end > 0 && !item.is_char_boundary(end) {
29                    end -= 1;
30                }
31                item.truncate(end);
32                item.push('…');
33            }
34            item
35        })
36        .take(count)
37        .collect()
38}
39
40/// Render a correction packet through the `branch_correct` section program.
41pub fn render_correction(packet: &CorrectionPacket) -> Result<String> {
42    let diagnostics = bounded_items(
43        packet.diagnostics.iter().map(|diagnostic| {
44            let location = match (&diagnostic.path, diagnostic.line) {
45                (Some(path), Some(line)) => format!("{path}:{line}: "),
46                (Some(path), None) => format!("{path}: "),
47                _ => String::new(),
48            };
49            let code = diagnostic
50                .code
51                .as_deref()
52                .map(|code| format!("[{code}] "))
53                .unwrap_or_default();
54            format!("{location}{code}{}", diagnostic.message)
55        }),
56        32,
57        500,
58    );
59    let operators = bounded_items(
60        packet.operators.iter().map(|operator| {
61            if operator.rationale.is_empty() {
62                operator.instruction.clone()
63            } else {
64                format!("{} ({})", operator.instruction, operator.rationale)
65            }
66        }),
67        8,
68        1000,
69    );
70    let mut summary = packet.dominant_cluster.root_cause.clone();
71    summary.truncate(1000);
72    let sections = [
73        branch_correct::Role {}.render()?,
74        branch_correct::Correction {
75            cluster_summary: BoundedText::new(summary)?,
76            diagnostics: BoundedList::new(diagnostics)?,
77            paths: BoundedList::new(bounded_items(
78                packet.affected.paths.iter().cloned(),
79                32,
80                250,
81            ))?,
82            symbols: BoundedList::new(bounded_items(
83                packet
84                    .affected
85                    .symbols
86                    .iter()
87                    .map(|symbol| symbol.name.clone()),
88                32,
89                120,
90            ))?,
91            operators: BoundedList::new(operators)?,
92        }
93        .render()?,
94        branch_correct::OutputContract {}.render()?,
95    ];
96    Ok(sections
97        .iter()
98        .map(|section| section.content.as_str())
99        .collect::<Vec<_>>()
100        .join(branch_correct::SEPARATOR))
101}