use super::{
add_frontmatter_context, parse_frontmatter, to_markdown, MetadataStore, CRITIQUES_DIR,
PROBLEMS_DIR, SOLUTIONS_DIR,
};
use crate::error::{JjjError, Result};
use crate::models::{Problem, ProblemFrontmatter};
use std::fs;
impl MetadataStore {
pub fn load_problem(&self, problem_id: &str) -> Result<Problem> {
self.ensure_meta_checkout()?;
let problem_path = self
.meta_path
.join(PROBLEMS_DIR)
.join(format!("{}.md", problem_id));
if !problem_path.exists() {
return Err(JjjError::ProblemNotFound(problem_id.to_string()));
}
let content = fs::read_to_string(problem_path)?;
let (frontmatter, body): (ProblemFrontmatter, String) = parse_frontmatter(&content)
.map_err(|e| add_frontmatter_context(e, "problem", problem_id))?;
let problem = Problem {
id: frontmatter.id,
title: frontmatter.title,
parent_id: frontmatter.parent_id,
status: frontmatter.status,
priority: frontmatter.priority,
confidence: frontmatter.confidence,
solution_ids: frontmatter.solution_ids,
child_ids: frontmatter.child_ids,
milestone_id: frontmatter.milestone_id,
assignee: frontmatter.assignee,
created_at: frontmatter.created_at,
updated_at: frontmatter.updated_at,
description: body,
dissolved_reason: frontmatter.dissolved_reason,
github_issue: frontmatter.github_issue,
tags: frontmatter.tags,
};
Ok(problem)
}
pub fn save_problem(&self, problem: &Problem) -> Result<()> {
self.ensure_meta_checkout()?;
let problems_dir = self.meta_path.join(PROBLEMS_DIR);
fs::create_dir_all(&problems_dir)?;
let frontmatter = ProblemFrontmatter::from(problem);
let body = if problem.description.is_empty() {
String::new()
} else {
format!("{}\n", problem.description)
};
let content = to_markdown(&frontmatter, &body)?;
let problem_path = problems_dir.join(format!("{}.md", problem.id));
super::atomic_write(&problem_path, content.as_bytes())?;
let db_path = self.jj_client.repo_root().join(".jj").join("jjj.db");
if db_path.exists() {
if let Ok(db) = crate::db::schema::Database::open(&db_path) {
let body = format!("{}\n{}", problem.description, problem.tags.join(" "));
if let Err(e) = crate::db::sync::update_fts_entry(
db.conn(),
"problem",
&problem.id,
&problem.title,
&body,
) {
eprintln!("Warning: FTS index update failed: {}", e);
}
}
}
Ok(())
}
pub fn delete_problem(&self, problem_id: &str) -> Result<()> {
self.ensure_meta_checkout()?;
let problem_path = self
.meta_path
.join(PROBLEMS_DIR)
.join(format!("{}.md", problem_id));
if !problem_path.exists() {
return Err(JjjError::ProblemNotFound(problem_id.to_string()));
}
let problem = self.load_problem(problem_id)?;
match self.list_subproblems(problem_id) {
Ok(children) => {
for child in children {
match self.load_problem(&child.id) {
Ok(mut c) => {
c.set_parent(None);
if let Err(e) = self.save_problem(&c) {
eprintln!(
"Warning: failed to orphan child problem {}: {}",
child.id, e
);
}
}
Err(e) => {
eprintln!("Warning: failed to load child problem {}: {}", child.id, e)
}
}
}
}
Err(e) => eprintln!(
"Warning: failed to list child problems of {}: {}",
problem_id, e
),
}
match self.list_solutions_for_problem(problem_id) {
Ok(solutions) => {
for solution in solutions {
match self.list_critiques_for_solution(&solution.id) {
Ok(critiques) => {
for critique in critiques {
let path = self
.meta_path
.join(CRITIQUES_DIR)
.join(format!("{}.md", critique.id));
if let Err(e) = fs::remove_file(&path) {
eprintln!(
"Warning: failed to delete critique {}: {}",
critique.id, e
);
}
}
}
Err(e) => eprintln!(
"Warning: failed to list critiques for solution {}: {}",
solution.id, e
),
}
let path = self
.meta_path
.join(SOLUTIONS_DIR)
.join(format!("{}.md", solution.id));
if let Err(e) = fs::remove_file(&path) {
eprintln!("Warning: failed to delete solution {}: {}", solution.id, e);
}
}
}
Err(e) => eprintln!(
"Warning: failed to list solutions for problem {}: {}",
problem_id, e
),
}
if let Some(ref milestone_id) = problem.milestone_id {
match self.load_milestone(milestone_id) {
Ok(mut milestone) => {
milestone.remove_problem(problem_id);
if let Err(e) = self.save_milestone(&milestone) {
eprintln!(
"Warning: failed to update milestone {}: {}",
milestone_id, e
);
}
}
Err(e) => eprintln!("Warning: failed to load milestone {}: {}", milestone_id, e),
}
}
fs::remove_file(problem_path)?;
let db_path = self.jj_client.repo_root().join(".jj").join("jjj.db");
if db_path.exists() {
if let Ok(db) = crate::db::schema::Database::open(&db_path) {
let _ = db.conn().execute("DELETE FROM fts WHERE id = ?1", rusqlite::params![problem_id]);
}
}
Ok(())
}
pub fn list_problems(&self) -> Result<Vec<Problem>> {
self.ensure_meta_checkout()?;
let problems_dir = self.meta_path.join(PROBLEMS_DIR);
if !problems_dir.exists() {
return Ok(Vec::new());
}
let mut problems = Vec::new();
let mut failures = Vec::new();
for entry in fs::read_dir(problems_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("md") {
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
match self.load_problem(stem) {
Ok(problem) => problems.push(problem),
Err(e) => failures.push(format!("{}: {}", stem, e)),
}
}
}
}
if !failures.is_empty() {
eprintln!("Warning: Failed to load {} problem(s):", failures.len());
for failure in &failures {
eprintln!(" {}", failure);
}
}
let mut child_map: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
for p in &problems {
if let Some(ref pid) = p.parent_id {
child_map.entry(pid.clone()).or_default().push(p.id.clone());
}
}
for p in &mut problems {
p.child_ids = child_map.remove(&p.id).unwrap_or_default();
}
Ok(problems)
}
pub fn next_problem_id(&self) -> Result<String> {
Ok(crate::id::generate_id())
}
pub fn list_subproblems(&self, problem_id: &str) -> Result<Vec<Problem>> {
let problems = self.list_problems()?;
Ok(problems
.into_iter()
.filter(|p| p.parent_id.as_deref() == Some(problem_id))
.collect())
}
pub fn list_root_problems(&self) -> Result<Vec<Problem>> {
let problems = self.list_problems()?;
Ok(problems
.into_iter()
.filter(|p| p.parent_id.is_none())
.collect())
}
pub fn parent_chain(&self, problem_id: &str) -> Result<Vec<Problem>> {
let mut chain = Vec::new();
let mut current_id = Some(problem_id.to_string());
let mut visited = std::collections::HashSet::new();
visited.insert(problem_id.to_string());
while let Some(id) = current_id {
if let Ok(problem) = self.load_problem(&id) {
current_id = problem.parent_id.clone();
if let Some(ref next_id) = current_id {
if !visited.insert(next_id.clone()) {
eprintln!(
"Warning: cycle detected in problem parent chain at {}",
next_id
);
break;
}
}
if current_id.is_some() {
chain.push(problem);
}
} else {
break;
}
}
Ok(chain)
}
}