use crate::engine::stable_id;
use crate::event_log::EventLog;
use crate::links_format::format_lino_record;
use crate::meta_frame::NeedStatus;
use crate::solution_evidence::SolutionEvidence;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SkillMode {
#[default]
Off,
Accumulate,
}
impl SkillMode {
#[must_use]
pub const fn emits_ledger(self) -> bool {
matches!(self, Self::Accumulate)
}
#[must_use]
pub const fn slug(self) -> &'static str {
match self {
Self::Off => "off",
Self::Accumulate => "accumulate",
}
}
#[must_use]
pub fn from_slug(slug: &str) -> Option<Self> {
match slug.trim().to_ascii_lowercase().as_str() {
"off" => Some(Self::Off),
"accumulate" => Some(Self::Accumulate),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SkillStatus {
Proposed,
Stable,
Deprecated,
Retired,
}
impl SkillStatus {
#[must_use]
pub const fn slug(self) -> &'static str {
match self {
Self::Proposed => "proposed",
Self::Stable => "stable",
Self::Deprecated => "deprecated",
Self::Retired => "retired",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct PromotionGate {
pub has_tests: bool,
pub has_benchmark_delta: bool,
}
impl PromotionGate {
#[must_use]
pub const fn satisfied(self) -> bool {
self.has_tests && self.has_benchmark_delta
}
#[must_use]
const fn status(self) -> SkillStatus {
if self.satisfied() {
SkillStatus::Stable
} else {
SkillStatus::Proposed
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CandidateSkill {
pub skill_id: String,
pub method: String,
pub route: Option<String>,
pub source_span: String,
pub work_unit_id: Option<String>,
pub gate: PromotionGate,
pub status: SkillStatus,
}
impl CandidateSkill {
#[must_use]
pub const fn promotable(&self) -> bool {
self.gate.satisfied()
}
#[must_use]
fn to_links_notation(&self) -> String {
let mut pairs: Vec<(&str, String)> = vec![
("record_type", "candidate_skill".to_owned()),
("skill_id", self.skill_id.clone()),
("method", self.method.clone()),
("source_span", self.source_span.clone()),
("status", self.status.slug().to_owned()),
("has_tests", self.gate.has_tests.to_string()),
(
"has_benchmark_delta",
self.gate.has_benchmark_delta.to_string(),
),
("promotable", self.promotable().to_string()),
];
if let Some(route) = &self.route {
pairs.push(("route", route.clone()));
}
if let Some(unit_id) = &self.work_unit_id {
pairs.push(("work_unit", unit_id.clone()));
}
format_lino_record(&self.skill_id, &pairs)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CurriculumItem {
pub item_id: String,
pub need_id: String,
pub source_span: String,
pub status: NeedStatus,
pub reason: String,
}
impl CurriculumItem {
#[must_use]
fn to_links_notation(&self) -> String {
format_lino_record(
&self.item_id,
&[
("record_type", "curriculum_item".to_owned()),
("item_id", self.item_id.clone()),
("need_id", self.need_id.clone()),
("source_span", self.source_span.clone()),
("status", self.status.slug().to_owned()),
("reason", self.reason.clone()),
],
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkillLedger {
pub frame_id: String,
pub skills: Vec<CandidateSkill>,
pub curriculum: Vec<CurriculumItem>,
}
impl SkillLedger {
#[must_use]
pub fn from_evidence(evidence: &SolutionEvidence) -> Self {
let mut skills = Vec::new();
let mut curriculum = Vec::new();
for trail in &evidence.trails {
let demonstrated = trail.connected && trail.status == NeedStatus::Satisfied;
if let (true, Some(method)) = (demonstrated, &trail.method) {
let gate = PromotionGate::default();
skills.push(CandidateSkill {
skill_id: stable_id(
"candidate_skill",
&format!("{method}:{}", trail.source_span),
),
method: method.clone(),
route: trail.route.clone(),
source_span: trail.source_span.clone(),
work_unit_id: trail.work_unit_id.clone(),
gate,
status: gate.status(),
});
} else {
let reason = curriculum_reason(trail.method.is_some(), trail.connected);
curriculum.push(CurriculumItem {
item_id: stable_id("curriculum_item", &trail.need_id),
need_id: trail.need_id.clone(),
source_span: trail.source_span.clone(),
status: trail.status,
reason,
});
}
}
Self {
frame_id: evidence.frame_id.clone(),
skills,
curriculum,
}
}
#[must_use]
pub fn proposed_count(&self) -> usize {
self.count_with(SkillStatus::Proposed)
}
#[must_use]
pub fn stable_count(&self) -> usize {
self.count_with(SkillStatus::Stable)
}
#[must_use]
pub const fn curriculum_count(&self) -> usize {
self.curriculum.len()
}
#[must_use]
pub fn promotable_count(&self) -> usize {
self.skills
.iter()
.filter(|skill| skill.promotable())
.count()
}
fn count_with(&self, status: SkillStatus) -> usize {
self.skills
.iter()
.filter(|skill| skill.status == status)
.count()
}
#[must_use]
pub fn to_links_notation(&self) -> String {
let ledger_id = stable_id("skill_ledger", &self.frame_id);
let mut pairs: Vec<(&str, String)> = vec![
("record_type", "skill_ledger".to_owned()),
("frame_id", self.frame_id.clone()),
("skill_count", self.skills.len().to_string()),
("proposed", self.proposed_count().to_string()),
("stable", self.stable_count().to_string()),
("promotable", self.promotable_count().to_string()),
("curriculum_count", self.curriculum_count().to_string()),
];
for skill in &self.skills {
pairs.push(("skill", skill.skill_id.clone()));
}
for item in &self.curriculum {
pairs.push(("curriculum", item.item_id.clone()));
}
let mut out = format_lino_record(&ledger_id, &pairs);
for skill in &self.skills {
out.push('\n');
out.push_str(&skill.to_links_notation());
}
for item in &self.curriculum {
out.push('\n');
out.push_str(&item.to_links_notation());
}
out
}
}
fn curriculum_reason(has_method: bool, connected: bool) -> String {
match (has_method, connected) {
(false, _) => {
"No catalogued method resolves this need; recorded as a gap to close.".to_owned()
}
(true, false) => "A method exists but the need's chain did not connect end to end; \
recorded as a gap to close."
.to_owned(),
(true, true) => "The need resolved to a method but was not satisfied; recorded as a gap \
to close."
.to_owned(),
}
}
pub(crate) fn record_skill_ledger(
log: &mut EventLog,
evidence: &SolutionEvidence,
mode: SkillMode,
) -> Option<SkillLedger> {
if !mode.emits_ledger() {
return None;
}
let ledger = SkillLedger::from_evidence(evidence);
log.append("skill_ledger", ledger.to_links_notation());
log.append(
"skill_ledger:promotable",
ledger.promotable_count().to_string(),
);
Some(ledger)
}