use std::collections::BTreeSet;
use crate::links_format::format_lino_record;
const RECIPE_LINO: &str = include_str!("../data/meta/recursive-core-recipe.lino");
const PIPELINE_SRC: &str = include_str!("meta_core.rs");
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SelfImprovementMode {
#[default]
Off,
Propose,
}
impl SelfImprovementMode {
#[must_use]
pub const fn proposes(self) -> bool {
matches!(self, Self::Propose)
}
#[must_use]
pub const fn slug(self) -> &'static str {
match self {
Self::Off => "off",
Self::Propose => "propose",
}
}
#[must_use]
pub fn from_slug(slug: &str) -> Option<Self> {
match slug.trim().to_ascii_lowercase().as_str() {
"off" => Some(Self::Off),
"propose" => Some(Self::Propose),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct PipelineStage {
pub module: String,
pub function: String,
}
impl PipelineStage {
#[must_use]
pub fn source_file(&self) -> String {
format!("src/{}.rs", self.module)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetaRecipeProposal {
pub undescribed_stages: Vec<PipelineStage>,
pub stale_citations: Vec<String>,
}
impl MetaRecipeProposal {
#[must_use]
pub const fn is_self_consistent(&self) -> bool {
self.undescribed_stages.is_empty() && self.stale_citations.is_empty()
}
#[must_use]
pub const fn change_count(&self) -> usize {
self.undescribed_stages.len() + self.stale_citations.len()
}
#[must_use]
pub fn summary(&self) -> String {
if self.is_self_consistent() {
return "The recipe already describes every pipeline stage; no update proposed."
.to_owned();
}
let add = self
.undescribed_stages
.iter()
.map(|stage| stage.function.as_str())
.collect::<Vec<_>>()
.join(", ");
let drop = self.stale_citations.join(", ");
match (add.is_empty(), drop.is_empty()) {
(false, true) => format!("Propose citing undescribed pipeline stage(s): {add}."),
(true, false) => format!("Propose dropping stale recipe citation(s): {drop}."),
_ => format!("Propose citing {add}; propose dropping stale citation(s): {drop}."),
}
}
#[must_use]
pub fn to_links_notation(&self, mode: SelfImprovementMode) -> String {
let pairs: Vec<(&str, String)> = vec![
("record_type", "meta_recipe_proposal".to_owned()),
("mode", mode.slug().to_owned()),
("self_consistent", self.is_self_consistent().to_string()),
("change_count", self.change_count().to_string()),
];
let mut out = format_lino_record("meta_recipe_proposal", &pairs);
for stage in &self.undescribed_stages {
out.push('\n');
out.push_str(&format_lino_record(
&format!("add_{}", stage.function),
&[
("record_type", "proposed_meta_function".to_owned()),
("function", stage.function.clone()),
("source_file", stage.source_file()),
],
));
}
for stale in &self.stale_citations {
out.push('\n');
out.push_str(&format_lino_record(
&format!("drop_{stale}"),
&[
("record_type", "stale_meta_function".to_owned()),
("function", stale.clone()),
],
));
}
out
}
}
#[derive(Debug, Clone)]
pub struct MetaSelfImprovement {
recipe_record_functions: BTreeSet<String>,
pipeline_stages: Vec<PipelineStage>,
}
impl MetaSelfImprovement {
#[must_use]
pub fn from_sources(recipe_lino: &str, pipeline_src: &str) -> Self {
Self {
recipe_record_functions: recipe_record_functions(recipe_lino),
pipeline_stages: pipeline_stages(pipeline_src),
}
}
#[must_use]
pub fn from_repo() -> Self {
Self::from_sources(RECIPE_LINO, PIPELINE_SRC)
}
#[must_use]
pub fn pipeline_stages(&self) -> &[PipelineStage] {
&self.pipeline_stages
}
#[must_use]
pub fn propose(&self) -> MetaRecipeProposal {
let undescribed_stages = self
.pipeline_stages
.iter()
.filter(|stage| !self.recipe_record_functions.contains(&stage.function))
.cloned()
.collect();
let pipeline_functions: BTreeSet<&str> = self
.pipeline_stages
.iter()
.map(|stage| stage.function.as_str())
.collect();
let stale_citations = self
.recipe_record_functions
.iter()
.filter(|function| !pipeline_functions.contains(function.as_str()))
.cloned()
.collect();
MetaRecipeProposal {
undescribed_stages,
stale_citations,
}
}
}
#[must_use]
pub fn propose_recipe_update(
recipe_lino: &str,
pipeline_src: &str,
mode: SelfImprovementMode,
) -> Option<MetaRecipeProposal> {
mode.proposes()
.then(|| MetaSelfImprovement::from_sources(recipe_lino, pipeline_src).propose())
}
fn recipe_record_functions(recipe_lino: &str) -> BTreeSet<String> {
let mut functions = BTreeSet::new();
let mut in_meta_function = false;
for line in recipe_lino.lines() {
let trimmed = line.trim();
if !line.starts_with(char::is_whitespace) {
in_meta_function = false;
continue;
}
if let Some(value) = field_value(trimmed, "record_type") {
in_meta_function = value == "meta_function";
continue;
}
if in_meta_function {
if let Some(name) = field_value(trimmed, "function") {
if name.starts_with("record_") {
functions.insert(name);
}
}
}
}
functions
}
fn pipeline_stages(pipeline_src: &str) -> Vec<PipelineStage> {
let mut stages = Vec::new();
let mut seen = BTreeSet::new();
for fragment in pipeline_src.split("crate::").skip(1) {
let Some(call) = fragment.split('(').next() else {
continue;
};
let parts: Vec<&str> = call.split("::").collect();
if parts.len() != 2 {
continue;
}
let module = parts[0].trim();
let function = parts[1].trim();
if !function.starts_with("record_")
|| !is_ident(module)
|| !is_ident(function)
|| !seen.insert(function.to_owned())
{
continue;
}
stages.push(PipelineStage {
module: module.to_owned(),
function: function.to_owned(),
});
}
stages
}
fn is_ident(value: &str) -> bool {
!value.is_empty()
&& value
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
}
fn field_value(line: &str, key: &str) -> Option<String> {
let rest = line.strip_prefix(key)?;
if !rest.starts_with(char::is_whitespace) {
return None;
}
let raw = rest.trim();
let unquoted = raw
.strip_prefix('"')
.and_then(|value| value.strip_suffix('"'))
.unwrap_or(raw);
Some(unquoted.to_owned())
}