use crate::engine::stable_id;
use crate::event_log::EventLog;
use crate::intent_formalization::{formalize_intent, IntentFormalization, IntentKind};
use crate::links_format::format_lino_record;
use crate::translation::formalize_prompt;
#[must_use]
fn formalize_span(span: &str, language: &str) -> IntentFormalization {
let candidate = formalize_prompt(span, language);
formalize_intent(span, language, Some(&candidate))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NeedStatus {
Pending,
Satisfied,
Deferred,
Blocked,
Rejected,
}
impl NeedStatus {
#[must_use]
pub const fn slug(self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Satisfied => "satisfied",
Self::Deferred => "deferred",
Self::Blocked => "blocked",
Self::Rejected => "rejected",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Need {
pub need_id: String,
pub source_span: String,
pub kind: IntentKind,
pub route: Option<String>,
pub status: NeedStatus,
}
impl Need {
#[must_use]
fn to_links_notation(&self) -> String {
let mut pairs: Vec<(&str, String)> = vec![
("record_type", "problem_need".to_owned()),
("need_id", self.need_id.clone()),
("source_span", self.source_span.clone()),
("kind", self.kind.slug().to_owned()),
("status", self.status.slug().to_owned()),
];
if let Some(route) = &self.route {
pairs.push(("route", route.clone()));
}
format_lino_record(&self.need_id, &pairs)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProblemFrame {
pub frame_id: String,
pub impulse_id: String,
pub language: String,
pub kind: IntentKind,
pub route: Option<String>,
pub needs: Vec<Need>,
}
impl ProblemFrame {
#[must_use]
pub fn from_formalization(formalization: &IntentFormalization) -> Self {
let frame_id = stable_id("problem_frame", &formalization.impulse_id);
let segments = segment_needs(&formalization.source_text);
let needs = if segments.len() <= 1 {
vec![Need {
need_id: need_id_for(&frame_id, 0, &formalization.source_text),
source_span: formalization.source_text.clone(),
kind: formalization.kind,
route: formalization.route.clone(),
status: NeedStatus::Pending,
}]
} else {
segments
.iter()
.enumerate()
.map(|(index, span)| {
let segment = formalize_span(span, &formalization.language);
Need {
need_id: need_id_for(&frame_id, index, span),
source_span: span.clone(),
kind: segment.kind,
route: segment.route,
status: NeedStatus::Pending,
}
})
.collect()
};
Self {
frame_id,
impulse_id: formalization.impulse_id.clone(),
language: formalization.language.clone(),
kind: formalization.kind,
route: formalization.route.clone(),
needs,
}
}
#[must_use]
pub const fn need_count(&self) -> usize {
self.needs.len()
}
#[must_use]
pub fn to_links_notation(&self) -> String {
let mut pairs: Vec<(&str, String)> = vec![
("record_type", "problem_frame".to_owned()),
("frame_id", self.frame_id.clone()),
("impulse_id", self.impulse_id.clone()),
("language", self.language.clone()),
("kind", self.kind.slug().to_owned()),
("need_count", self.needs.len().to_string()),
];
if let Some(route) = &self.route {
pairs.push(("route", route.clone()));
}
for need in &self.needs {
pairs.push(("need", need.need_id.clone()));
}
let mut out = format_lino_record(&self.frame_id, &pairs);
for need in &self.needs {
out.push('\n');
out.push_str(&need.to_links_notation());
}
out
}
}
#[must_use]
fn need_id_for(frame_id: &str, index: usize, span: &str) -> String {
stable_id("problem_need", &format!("{frame_id}:{index}:{span}"))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AtomicityReason {
DirectMethod,
SingleNeed,
DepthBound,
NotAtomic,
}
impl AtomicityReason {
#[must_use]
pub const fn slug(self) -> &'static str {
match self {
Self::DirectMethod => "direct_method",
Self::SingleNeed => "single_need",
Self::DepthBound => "depth_bound",
Self::NotAtomic => "not_atomic",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkUnit {
pub unit_id: String,
pub parent: Option<String>,
pub source_span: String,
pub depth: u8,
pub atomic: bool,
pub reason: AtomicityReason,
pub route: Option<String>,
pub children: Vec<Self>,
}
impl WorkUnit {
#[must_use]
pub fn from_formalization(formalization: &IntentFormalization, max_depth: u8) -> Self {
Self::build(
&formalization.source_text,
None,
&formalization.language,
0,
max_depth,
)
}
fn build(span: &str, parent: Option<String>, language: &str, depth: u8, max_depth: u8) -> Self {
let unit_id = stable_id("work_unit", &format!("{parent:?}:{depth}:{span}"));
let route = formalize_span(span, language).route;
if depth >= max_depth {
return Self::leaf(
unit_id,
parent,
span,
depth,
AtomicityReason::DepthBound,
route,
);
}
let segments = decompose_once(span);
if segments.len() <= 1 {
let reason = if route.is_some() {
AtomicityReason::DirectMethod
} else {
AtomicityReason::SingleNeed
};
return Self::leaf(unit_id, parent, span, depth, reason, route);
}
let children = segments
.iter()
.map(|child_span| {
Self::build(
child_span,
Some(unit_id.clone()),
language,
depth + 1,
max_depth,
)
})
.collect();
Self {
unit_id,
parent,
source_span: span.to_owned(),
depth,
atomic: false,
reason: AtomicityReason::NotAtomic,
route,
children,
}
}
fn leaf(
unit_id: String,
parent: Option<String>,
span: &str,
depth: u8,
reason: AtomicityReason,
route: Option<String>,
) -> Self {
Self {
unit_id,
parent,
source_span: span.to_owned(),
depth,
atomic: true,
reason,
route,
children: Vec::new(),
}
}
#[must_use]
pub fn unit_count(&self) -> usize {
1 + self.children.iter().map(Self::unit_count).sum::<usize>()
}
#[must_use]
pub fn leaf_count(&self) -> usize {
if self.atomic {
1
} else {
self.children.iter().map(Self::leaf_count).sum()
}
}
pub fn collect_leaves<'a>(&'a self, out: &mut Vec<&'a Self>) {
if self.atomic {
out.push(self);
} else {
for child in &self.children {
child.collect_leaves(out);
}
}
}
#[must_use]
pub fn to_links_notation(&self) -> String {
let mut pairs: Vec<(&str, String)> = vec![
("record_type", "work_unit".to_owned()),
("unit_id", self.unit_id.clone()),
("source_span", self.source_span.clone()),
("depth", self.depth.to_string()),
("atomic", self.atomic.to_string()),
("atomicity_reason", self.reason.slug().to_owned()),
];
if let Some(parent) = &self.parent {
pairs.push(("parent", parent.clone()));
}
if let Some(route) = &self.route {
pairs.push(("route", route.clone()));
}
for child in &self.children {
pairs.push(("child", child.unit_id.clone()));
}
let mut out = format_lino_record(&self.unit_id, &pairs);
for child in &self.children {
out.push('\n');
out.push_str(&child.to_links_notation());
}
out
}
fn emit_events(&self, log: &mut EventLog) {
log.append(
"work_unit:enter",
format!("{} {}", self.depth, self.source_span),
);
for child in &self.children {
child.emit_events(log);
}
log.append(
"work_unit:exit",
format!("{} {}", self.depth, self.reason.slug()),
);
}
}
#[must_use]
fn decompose_once(span: &str) -> Vec<String> {
let sentences = split_sentences(span);
if sentences.len() > 1 {
return sentences;
}
split_clauses(span)
}
#[must_use]
fn segment_needs(text: &str) -> Vec<String> {
let mut segments = Vec::new();
for sentence in split_sentences(text) {
for clause in split_clauses(&sentence) {
let trimmed = clause.trim();
if !trimmed.is_empty() {
segments.push(trimmed.to_owned());
}
}
}
segments
}
#[must_use]
fn split_sentences(text: &str) -> Vec<String> {
let chars: Vec<char> = text.chars().collect();
let mut sentences = Vec::new();
let mut current = String::new();
for (index, &ch) in chars.iter().enumerate() {
current.push(ch);
let strong_terminator = matches!(ch, '?' | '!' | '。' | '!' | '?');
let period_boundary =
ch == '.' && chars.get(index + 1).is_none_or(|next| next.is_whitespace());
if strong_terminator || period_boundary {
push_trimmed(&mut sentences, ¤t);
current.clear();
}
}
push_trimmed(&mut sentences, ¤t);
sentences
}
#[must_use]
fn split_clauses(sentence: &str) -> Vec<String> {
sentence
.split([',', ';'])
.flat_map(|chunk| chunk.split(" and "))
.flat_map(|chunk| chunk.split(" with "))
.map(|chunk| chunk.trim().to_owned())
.filter(|chunk| !chunk.is_empty())
.collect()
}
fn push_trimmed(out: &mut Vec<String>, candidate: &str) {
let trimmed = candidate.trim();
if !trimmed.is_empty() {
out.push(trimmed.to_owned());
}
}
pub(crate) fn record_problem_frame(
log: &mut EventLog,
formalization: &IntentFormalization,
) -> ProblemFrame {
let frame = ProblemFrame::from_formalization(formalization);
log.append("problem_frame", frame.to_links_notation());
log.append("problem_frame:need_count", frame.needs.len().to_string());
for need in &frame.needs {
log.append(
"problem_frame:need",
format!("{} {}", need.kind.slug(), need.source_span),
);
}
frame
}
pub(crate) fn record_work_units(
log: &mut EventLog,
formalization: &IntentFormalization,
max_depth: u8,
) -> WorkUnit {
let root = WorkUnit::from_formalization(formalization, max_depth);
log.append("work_unit", root.to_links_notation());
log.append("work_unit:count", root.unit_count().to_string());
log.append("work_unit:leaf_count", root.leaf_count().to_string());
root.emit_events(log);
root
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LedgerRow {
pub need_id: String,
pub source_span: String,
pub status: NeedStatus,
pub leaf_reason: Option<AtomicityReason>,
pub unit_id: Option<String>,
pub route: Option<String>,
}
impl LedgerRow {
#[must_use]
fn to_links_notation(&self) -> String {
let row_id = stable_id(
"need_ledger_row",
&format!("{}:{}", self.need_id, self.status.slug()),
);
let mut pairs: Vec<(&str, String)> = vec![
("record_type", "need_ledger_row".to_owned()),
("need_id", self.need_id.clone()),
("source_span", self.source_span.clone()),
("status", self.status.slug().to_owned()),
];
if let Some(reason) = self.leaf_reason {
pairs.push(("leaf_reason", reason.slug().to_owned()));
}
if let Some(unit_id) = &self.unit_id {
pairs.push(("unit_id", unit_id.clone()));
}
if let Some(route) = &self.route {
pairs.push(("route", route.clone()));
}
format_lino_record(&row_id, &pairs)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NeedLedger {
pub frame_id: String,
pub rows: Vec<LedgerRow>,
}
impl NeedLedger {
#[must_use]
pub fn resolve(frame: &ProblemFrame, root: &WorkUnit) -> Self {
let mut leaves = Vec::new();
root.collect_leaves(&mut leaves);
let rows = frame
.needs
.iter()
.map(|need| {
let leaf = best_leaf_for(&leaves, &need.source_span);
let status = leaf.map_or(NeedStatus::Blocked, |leaf| {
if leaf.route.is_some() {
NeedStatus::Satisfied
} else {
NeedStatus::Blocked
}
});
LedgerRow {
need_id: need.need_id.clone(),
source_span: need.source_span.clone(),
status,
leaf_reason: leaf.map(|leaf| leaf.reason),
unit_id: leaf.map(|leaf| leaf.unit_id.clone()),
route: leaf.and_then(|leaf| leaf.route.clone()),
}
})
.collect();
Self {
frame_id: frame.frame_id.clone(),
rows,
}
}
#[must_use]
pub fn count_with(&self, status: NeedStatus) -> usize {
self.rows.iter().filter(|row| row.status == status).count()
}
#[must_use]
pub fn every_need_accounted_for(&self) -> bool {
!self.rows.is_empty()
&& self
.rows
.iter()
.all(|row| row.status != NeedStatus::Pending)
}
#[must_use]
pub fn to_links_notation(&self) -> String {
let ledger_id = stable_id("need_ledger", &self.frame_id);
let mut pairs: Vec<(&str, String)> = vec![
("record_type", "need_ledger".to_owned()),
("frame_id", self.frame_id.clone()),
("row_count", self.rows.len().to_string()),
(
"satisfied",
self.count_with(NeedStatus::Satisfied).to_string(),
),
("blocked", self.count_with(NeedStatus::Blocked).to_string()),
];
for row in &self.rows {
pairs.push(("row", row.need_id.clone()));
}
let mut out = format_lino_record(&ledger_id, &pairs);
for row in &self.rows {
out.push('\n');
out.push_str(&row.to_links_notation());
}
out
}
}
#[must_use]
fn best_leaf_for<'a>(leaves: &'a [&'a WorkUnit], span: &str) -> Option<&'a WorkUnit> {
if let Some(exact) = leaves.iter().find(|leaf| leaf.source_span == span) {
return Some(exact);
}
leaves
.iter()
.filter(|leaf| span.contains(leaf.source_span.as_str()) || leaf.source_span.contains(span))
.max_by_key(|leaf| leaf.source_span.len())
.copied()
}
pub(crate) fn record_need_ledger(
log: &mut EventLog,
frame: &ProblemFrame,
root: &WorkUnit,
) -> NeedLedger {
let ledger = NeedLedger::resolve(frame, root);
log.append("need_ledger", ledger.to_links_notation());
log.append(
"need_ledger:accounted_for",
ledger.every_need_accounted_for().to_string(),
);
for row in &ledger.rows {
log.append(
"need:status",
format!("{} {}", row.status.slug(), row.source_span),
);
}
ledger
}