#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProposalKind {
Tool,
Command,
Skill,
}
impl ProposalKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Tool => "tool",
Self::Command => "command",
Self::Skill => "skill",
}
}
}
pub fn required_kind(opportunity: &str) -> Option<ProposalKind> {
let lower = opportunity.to_lowercase();
let first = |needle: &str| lower.find(needle);
let mut found: Vec<(usize, ProposalKind)> = [
("kind=tool", ProposalKind::Tool),
("kind=command", ProposalKind::Command),
("kind=skill", ProposalKind::Skill),
]
.into_iter()
.filter_map(|(needle, kind)| first(needle).map(|at| (at, kind)))
.collect();
found.sort_by_key(|(at, _)| *at);
found.first().map(|(_, kind)| *kind)
}
pub fn capability_count(opportunities: &[String]) -> usize {
opportunities
.iter()
.filter(|o| required_kind(o).is_some())
.count()
}
pub fn required_actions_block(opportunities: &[String]) -> String {
let gaps: Vec<(usize, ProposalKind)> = opportunities
.iter()
.enumerate()
.filter_map(|(i, o)| required_kind(o).map(|k| (i + 1, k)))
.collect();
if gaps.is_empty() {
return String::new();
}
let mut block = String::from(
"\nREQUIRED ACTIONS\n\n\
The opportunities below are capability gaps, not guidance gaps. They \
describe something you cannot currently do. A brain-file rule cannot \
close them: the gap survives, gets re-detected next cycle, and the \
rule becomes prompt bloat that degrades every later cycle.\n\n\
`self_improve` does NOT answer these. `rsi_propose` does. You are not \
installing anything; you are filing a proposal for the user to accept \
or reject.\n\n",
);
for (n, kind) in &gaps {
block.push_str(&format!(
"- Opportunity {n}: call rsi_propose with kind={}\n",
kind.as_str()
));
}
block.push_str(&format!(
"\nThat is {} required rsi_propose call(s) this cycle. If you judge one \
to be a duplicate of an existing proposal or genuinely not worth \
filing, say which and why in your summary. Do not silently skip it.\n",
gaps.len()
));
block
}