diff --git a/.ai/architecture.md b/.ai/architecture.md new file mode 100644 index 00000000..e423b97b --- /dev/null +++ b/.ai/architecture.md @@ -0,0 +1,11 @@ +This repository runs the Rust open source "project goals" program. + +It is structured as an mdbook combined with custom rust code to manage the contents of that mdbook and perform other chores. + +The mdbook sources can be found in `src` and in `book.toml`. + +The `src/admin` directory in particular contains instructions targeting the people who maintain and run the project goal program. It describes the processes for tasks like beginning a new goals program, authoring goal updates, and so forth. + +Many of the tasks in `src/admin` are automated via a utility called `cargo rpg`. + +The sources for `cargo rpg` as well as the plugin for the mdbook processor are found in crates located in the `crates` directory. The `crates/README.md` summarizes the role of each crate. \ No newline at end of file diff --git a/book.toml b/book.toml index 68c87933..c25635a4 100644 --- a/book.toml +++ b/book.toml @@ -7,6 +7,9 @@ title = "Rust Project Goals" [preprocessor.goals] command = "cargo run -p mdbook-goals --" +ignore_users = [ + "@triagebot", +] [preprocessor.goals.links] "Help wanted" = "https://img.shields.io/badge/Help%20wanted-yellow" @@ -23,10 +26,6 @@ command = "cargo run -p mdbook-goals --" [preprocessor.goals.users] "@Nadrieril" = "@Nadrieril" -preprocessor.goals.ignore_users = [ - "@triagebot", -] - [output.html] git-repository-url = "https://github.com/rust-lang/rust-project-goals" diff --git a/crates/README.md b/crates/README.md new file mode 100644 index 00000000..309f2fc0 --- /dev/null +++ b/crates/README.md @@ -0,0 +1,8 @@ +The support crates for running the rust-project-goals program. + +The main crates of interest are: + +* `mdbook-goals`, which is an mdbook plugin that processes the project goal markdown files found in `../src` to populate indices and other content dynamically. +* `rust-project-goals-cli`, which contains the main helper tool, invoked with `cargo rpg` (there is an alias defined in the `.cargo` directory). + * The `rust-project-goals-*` crates are dependencies of `rust-project-goals-cli` for other more specialized tasks. +* The `rust-project-goals` crate is a library used by `mdbook-goals` and by the CLI tool to encapsulate common tasks like scanning for goal documents. diff --git a/crates/rust-project-goals-cli/src/cfp.rs b/crates/rust-project-goals-cli/src/cfp.rs new file mode 100644 index 00000000..c4eda632 --- /dev/null +++ b/crates/rust-project-goals-cli/src/cfp.rs @@ -0,0 +1,533 @@ +use std::fs::{self, File}; +use std::io::Write; +use std::path::{Path, PathBuf}; +use anyhow::{Context, Result}; +use regex::Regex; + +/// Module containing pure functions for text processing +pub mod text_processing { + use regex::Regex; + + /// Process template content by replacing placeholders and removing notes + pub fn process_template_content(content: &str, timeframe: &str, lowercase_timeframe: &str) -> String { + // Remove note sections (starting with '> **NOTE:**' and continuing until a non-'>' line) + // But preserve other content starting with '>' + let lines: Vec<&str> = content.lines().collect(); + let mut filtered_lines = Vec::new(); + let mut in_note_section = false; + + for line in lines.iter() { + if line.trim().starts_with("> **NOTE:**") { + in_note_section = true; + continue; + } + + if in_note_section { + if line.trim().starts_with(">") { + continue; // Skip this line as it's part of the note section + } else { + in_note_section = false; // End of note section + } + } + + filtered_lines.push(*line); + } + + // Join the filtered lines and clean up consecutive empty lines + let mut cleaned_lines = Vec::new(); + let mut last_was_empty = false; + + for line in filtered_lines { + let is_empty = line.trim().is_empty(); + if !is_empty || !last_was_empty { + cleaned_lines.push(line); + } + last_was_empty = is_empty; + } + + // Join and process placeholders + cleaned_lines.join("\n") + .replace("YYYYHN", timeframe) + .replace("YYYY", &timeframe[0..4]) + .replace("HN", &timeframe[4..]) + // Also add lowercase versions for consistency in file paths + .replace("yyyyhn", lowercase_timeframe) + .replace("yyyy", &lowercase_timeframe[0..4]) + .replace("hn", &lowercase_timeframe[4..]) + } + + /// Process SUMMARY.md content to add or update a timeframe section + pub fn process_summary_content(content: &str, timeframe: &str, lowercase_timeframe: &str) -> String { + let mut new_content = content.to_string(); + + // Create the new section content with capitalized H + let capitalized_timeframe = format!("{}H{}", &timeframe[0..4], &timeframe[5..]); + let new_section_content = format!( + "# ⏳ {} goal process\n\n\ + - [Overview](./{}/README.md)\n\ + - [Proposed goals](./{}/goals.md)\n\ + - [Goals not accepted](./{}/not_accepted.md)\n", + capitalized_timeframe, lowercase_timeframe, lowercase_timeframe, lowercase_timeframe + ); + + // Check if the timeframe is already in the SUMMARY.md + let section_header = format!("# ⏳ {} goal process", capitalized_timeframe); + + if content.contains(§ion_header) { + // The section exists, but it might have placeholder content + // Find the section header and its content, but be careful not to include the next section header + let section_header_pattern = format!(r"# ⏳ {} goal process", regex::escape(&capitalized_timeframe)); + let re = Regex::new(§ion_header_pattern).unwrap(); + + if let Some(section_match) = re.find(&content) { + // Find the end of the section header line + if let Some(header_end) = content[section_match.end()..].find('\n') { + let content_start = section_match.end() + header_end + 1; + + // Find the start of the next section header (if any) + let next_section_start = content[content_start..].find("\n# ") + .map(|pos| content_start + pos) + .unwrap_or(content.len()); + + // Extract the section content + let section_content = &content[content_start..next_section_start]; + + // Check if it contains placeholder content like "- [Not yet started]()" + if section_content.contains("[Not yet started]") || section_content.trim().is_empty() { + // Format the replacement content (just the links, not the section header) + let replacement_content = format!( + "\n- [Overview](./{}/README.md)\n\ + - [Proposed goals](./{}/goals.md)\n\ + - [Goals not accepted](./{}/not_accepted.md)\n", + lowercase_timeframe, lowercase_timeframe, lowercase_timeframe + ); + + // Replace just the section content, preserving the section header and any following sections + new_content.replace_range(content_start..next_section_start, &replacement_content); + return new_content; + } else { + // Section already has non-placeholder content, don't modify it + return content.to_string(); + } + } + } + } + + // If we get here, the section doesn't exist, so we need to add it + let new_section = format!("\n{}", new_section_content); + + // Find a good place to insert the new section + // Look for the last timeframe section or insert at the beginning + // Match both lowercase and uppercase H + let re = Regex::new(r"# ⏳ \d{4}[hH][12] goal process").unwrap(); + + if let Some(last_match) = re.find_iter(&content).last() { + // Find the end of this section (next section or end of file) + if let Some(next_section_pos) = content[last_match.end()..].find("\n# ") { + let insert_pos = last_match.end() + next_section_pos; + new_content.insert_str(insert_pos, &new_section); + } else { + // No next section, append to the end + new_content.push_str(&new_section); + } + } else { + // No existing timeframe sections, insert at the beginning + new_content = new_section + &content; + } + + new_content + } + + /// Process README.md content to add or update a timeframe section + pub fn process_readme_content(content: &str, timeframe: &str, lowercase_timeframe: &str) -> String { + let mut new_content = content.to_string(); + + // Extract year and half from timeframe + let _year = &timeframe[0..4]; + let half = &timeframe[4..].to_lowercase(); + + // Determine the months based on the half + let (start_month, end_month) = if half == "h1" { + ("January", "June") + } else { + ("July", "December") + }; + + // Create the new section to add with capitalized H + let capitalized_timeframe = format!("{}H{}", &timeframe[0..4], &timeframe[5..]); + let new_section = format!( + "\n## Next goal period ({})\n\n\ + The next goal period will be {}, running from the start of {} to the end of {}. \ + We are currently in the process of assembling goals. \ + [Click here](./{}/goals.md) to see the current list. \ + If you'd like to propose a goal, [instructions can be found here](./how_to/propose_a_goal.md).\n", + capitalized_timeframe, capitalized_timeframe, start_month, end_month, lowercase_timeframe + ); + + // First check for an existing entry for this specific timeframe + let this_period_pattern = Regex::new(&format!(r"## Next goal period(?:\s*\({}\))?\s*\n", regex::escape(&capitalized_timeframe))).unwrap(); + + // If not found, look for any "Next goal period" section + let next_period_pattern = Regex::new(r"## Next goal period(?:\s*\([^\)]*\))?\s*\n").unwrap(); + + // Also look for "Current goal period" to place after it if no "Next goal period" exists + let current_period_pattern = Regex::new(r"## Current goal period(?:\s*\([^\)]*\))?\s*\n").unwrap(); + + // First try to find and replace an existing entry for this specific timeframe + if let Some(this_period_match) = this_period_pattern.find(&content) { + // Found an existing section for this specific timeframe + // Find the end of this section (next section or end of file) + if let Some(next_section_pos) = content[this_period_match.end()..].find("\n## ") { + let end_pos = this_period_match.start() + next_section_pos + this_period_match.end() - this_period_match.start(); + new_content.replace_range(this_period_match.start()..end_pos, &new_section); + } else { + // No next section, replace until the end + new_content.replace_range(this_period_match.start().., &new_section); + } + } else if let Some(next_period_match) = next_period_pattern.find(&content) { + // Found an existing "Next goal period" section + // Find the end of this section (next section or end of file) + if let Some(next_section_pos) = content[next_period_match.end()..].find("\n## ") { + let end_pos = next_period_match.start() + next_section_pos + next_period_match.end() - next_period_match.start(); + new_content.replace_range(next_period_match.start()..end_pos, &new_section); + } else { + // No next section, replace until the end + new_content.replace_range(next_period_match.start().., &new_section); + } + } else { + // No existing "Next goal period" section, try to add after "Current goal period" section + if let Some(current_period_match) = current_period_pattern.find(&content) { + // Find the end of the current period section + if let Some(next_section_pos) = content[current_period_match.end()..].find("\n## ") { + let insert_pos = current_period_match.end() + next_section_pos; + new_content.insert_str(insert_pos, &new_section); + } else { + // No next section after current period, append to the end + new_content.push_str(&new_section); + } + } else { + // No "Current goal period" section either, add after the introduction + if let Some(pos) = content.find("\n## ") { + new_content.insert_str(pos, &new_section); + } else { + // Fallback: append to the end + new_content.push_str(&new_section); + } + } + } + + new_content + } +} + +/// Creates the directory structure and files for a new Call For Proposals (CFP) period. +pub fn create_cfp(timeframe: &str, force: bool, dry_run: bool) -> Result<()> { + if dry_run { + println!("Dry run mode - no changes will be made"); + } + // Validate the timeframe format + validate_timeframe(timeframe)?; + + // Ensure the timeframe is lowercase for directory and file paths + let lowercase_timeframe = timeframe.to_lowercase(); + + // Create the directory for the new timeframe (always lowercase) + let dir_path = PathBuf::from("src").join(&lowercase_timeframe); + if dir_path.exists() { + if !force && !dry_run { + println!("Directory {} already exists. Continuing will overwrite files.", dir_path.display()); + println!("Do you want to continue? [y/N]"); + + let mut input = String::new(); + std::io::stdin().read_line(&mut input)?; + if !input.trim().eq_ignore_ascii_case("y") { + println!("Operation cancelled."); + return Ok(()); + } + } else if force { + println!("Directory {} already exists. Force flag set, continuing without confirmation.", dir_path.display()); + } else { + // dry_run mode + println!("Would create/overwrite directory: {}", dir_path.display()); + } + } else if !dry_run { + fs::create_dir_all(&dir_path).with_context(|| format!("Failed to create directory {}", dir_path.display()))?; + println!("Created directory: {}", dir_path.display()); + } else { + println!("Would create directory: {}", dir_path.display()); + } + + // Copy and process template files + copy_and_process_template("src/admin/samples/rfc.md", &dir_path.join("README.md"), timeframe, &lowercase_timeframe, dry_run)?; + copy_and_process_template("src/admin/samples/goals.md", &dir_path.join("goals.md"), timeframe, &lowercase_timeframe, dry_run)?; + copy_and_process_template("src/admin/samples/not_accepted.md", &dir_path.join("not_accepted.md"), timeframe, &lowercase_timeframe, dry_run)?; + + // Update SUMMARY.md + update_summary_md(timeframe, &lowercase_timeframe, dry_run)?; + + // Update main README.md with the new timeframe section + update_main_readme(timeframe, &lowercase_timeframe, dry_run)?; + + println!("\nCFP setup for {} completed successfully!", timeframe); + println!("\nNext steps:"); + println!("1. Review and customize the generated files in src/{}/", lowercase_timeframe); + println!("2. Prepare a Call For Proposals blog post based on the sample in src/admin/samples/cfp.md"); + println!("3. Run 'mdbook build' to generate the book with the new content"); + + Ok(()) +} + +/// Validates that the timeframe is in the correct format (e.g., "2025h1" or "2025H1") +fn validate_timeframe(timeframe: &str) -> Result<()> { + let re = Regex::new(r"^\d{4}[hH][12]$").unwrap(); + if !re.is_match(timeframe) { + anyhow::bail!("Invalid timeframe format. Expected format: YYYYhN or YYYYHN (e.g., 2025h1, 2025H1, 2025h2, or 2025H2)"); + } + Ok(()) +} + +/// Copies a template file to the destination and replaces placeholders +fn copy_and_process_template(template_path: &str, dest_path: &Path, timeframe: &str, lowercase_timeframe: &str, dry_run: bool) -> Result<()> { + // Read the template file + let template_content = fs::read_to_string(template_path) + .with_context(|| format!("Failed to read template file: {}", template_path))?; + + // Use the pure function to process the content + let processed_content = text_processing::process_template_content(&template_content, timeframe, lowercase_timeframe); + + // Write to destination file + if !dry_run { + File::create(dest_path) + .with_context(|| format!("Failed to create file: {}", dest_path.display()))? + .write_all(processed_content.as_bytes()) + .with_context(|| format!("Failed to write to file: {}", dest_path.display()))?; + + println!("Created file: {}", dest_path.display()); + } else { + println!("Would create file: {}", dest_path.display()); + } + Ok(()) +} + +/// Updates the SUMMARY.md file to include the new timeframe section +fn update_summary_md(timeframe: &str, lowercase_timeframe: &str, dry_run: bool) -> Result<()> { + let summary_path = "src/SUMMARY.md"; + let content = fs::read_to_string(summary_path) + .with_context(|| format!("Failed to read SUMMARY.md"))?; + + // Use the pure function to process the content + let new_content = text_processing::process_summary_content(&content, timeframe, lowercase_timeframe); + + // Check if the content was modified + if new_content == content { + if !dry_run { + println!("SUMMARY.md already contains a non-placeholder entry for {}. Skipping update.", timeframe); + } else { + println!("Would skip updating SUMMARY.md (already contains a non-placeholder entry for {})", timeframe); + } + return Ok(()); + } + + // Write the updated content back to SUMMARY.md + if !dry_run { + fs::write(summary_path, new_content) + .with_context(|| format!("Failed to write to SUMMARY.md"))?; + + println!("Updated SUMMARY.md with {} section", timeframe); + } else { + println!("Would update SUMMARY.md with {} section", timeframe); + } + + Ok(()) +} + +/// Updates the src/README.md with information about the new timeframe +fn update_main_readme(timeframe: &str, lowercase_timeframe: &str, dry_run: bool) -> Result<()> { + let readme_path = "src/README.md"; + let content = fs::read_to_string(readme_path) + .with_context(|| format!("Failed to read README.md"))?; + + // Use the pure function to process the content + let new_content = text_processing::process_readme_content(&content, timeframe, lowercase_timeframe); + + // Check if the content was modified + if new_content == content { + if !dry_run { + println!("README.md already contains up-to-date information for {}. Skipping update.", timeframe); + } else { + println!("Would skip updating README.md (already contains up-to-date information for {})", timeframe); + } + return Ok(()); + } + + // Determine what kind of update was made for better logging + let capitalized_timeframe = format!("{}H{}", &timeframe[0..4], &timeframe[5..]); + let specific_timeframe_pattern = format!(r"## Next goal period(?:\s*\({}\))", regex::escape(&capitalized_timeframe)); + let specific_re = Regex::new(&specific_timeframe_pattern).unwrap(); + + if specific_re.is_match(&content) && specific_re.is_match(&new_content) { + if !dry_run { + println!("Updated existing 'Next goal period ({})' section in src/README.md", capitalized_timeframe); + } else { + println!("Would update existing 'Next goal period ({})' section in src/README.md", capitalized_timeframe); + } + } else if Regex::new(r"## Next goal period(?:\s*\([^\)]*\))").unwrap().is_match(&content) { + if !dry_run { + println!("Updated existing 'Next goal period' section in src/README.md"); + } else { + println!("Would update existing 'Next goal period' section in src/README.md"); + } + } else if Regex::new(r"## Current goal period(?:\s*\([^\)]*\))").unwrap().is_match(&content) { + if !dry_run { + println!("Added new 'Next goal period' section after 'Current goal period' in src/README.md"); + } else { + println!("Would add new 'Next goal period' section after 'Current goal period' in src/README.md"); + } + } else { + if !dry_run { + println!("Added new 'Next goal period' section to src/README.md"); + } else { + println!("Would add new 'Next goal period' section to src/README.md"); + } + } + + // Write the updated content back to README.md + if !dry_run { + fs::write(readme_path, new_content) + .with_context(|| format!("Failed to write to src/README.md"))?; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::text_processing::*; + + #[test] + fn test_process_template_content() { + // Test basic placeholder replacement + let content = "# YYYYHN Goals\n\nThis is for YYYY and HN."; + let result = process_template_content(content, "2026H1", "2026h1"); + assert_eq!(result, "# 2026H1 Goals\n\nThis is for 2026 and H1."); + + // Test note removal + let content_with_notes = "# YYYYHN Goals\n\n> **NOTE:** This is a note that should be removed.\n> More note content.\n\nThis should stay."; + let result = process_template_content(content_with_notes, "2026H1", "2026h1"); + assert_eq!(result, "# 2026H1 Goals\n\nThis should stay."); + + // Test that other blockquotes are preserved + let content_with_blockquote = "# YYYYHN Goals\n\n> This is a regular blockquote that should stay.\n\n> **NOTE:** This is a note that should be removed.\n> More note content.\n\nThis should stay."; + let result = process_template_content(content_with_blockquote, "2026H1", "2026h1"); + assert_eq!(result, "# 2026H1 Goals\n\n> This is a regular blockquote that should stay.\n\nThis should stay."); + } + + #[test] + fn test_process_summary_content_no_existing_section() { + // Test adding a new section when no timeframe sections exist + let content = "# Summary\n\n[Introduction](./README.md)\n"; + let result = process_summary_content(content, "2026h1", "2026h1"); + + assert!(result.contains("# ⏳ 2026H1 goal process")); + assert!(result.contains("- [Overview](./2026h1/README.md)")); + assert!(result.contains("- [Proposed goals](./2026h1/goals.md)")); + assert!(result.contains("- [Goals not accepted](./2026h1/not_accepted.md)")); + } + + #[test] + fn test_process_summary_content_with_placeholder() { + // Test updating a section that has placeholder content + let content = "# Summary\n\n[Introduction](./README.md)\n\n# ⏳ 2026H1 goal process\n\n- [Not yet started]()\n"; + let result = process_summary_content(content, "2026h1", "2026h1"); + + assert!(result.contains("# ⏳ 2026H1 goal process")); + assert!(result.contains("- [Overview](./2026h1/README.md)")); + assert!(!result.contains("- [Not yet started]()")); + } + + #[test] + fn test_process_summary_content_with_existing_content() { + // Test that existing non-placeholder content is not modified + let content = "# Summary\n\n[Introduction](./README.md)\n\n# ⏳ 2026H1 goal process\n\n- [Already populated](./2026h1/README.md)\n"; + let result = process_summary_content(content, "2026h1", "2026h1"); + + // Should not change existing non-placeholder content + assert_eq!(result, content); + } + + #[test] + fn test_process_summary_content_with_other_timeframes() { + // Test adding a new section when other timeframe sections exist + let content = "# Summary\n\n[Introduction](./README.md)\n\n# ⏳ 2025H1 goal process\n\n- [Overview](./2025h1/README.md)\n"; + let result = process_summary_content(content, "2026h1", "2026h1"); + + assert!(result.contains("# ⏳ 2025H1 goal process")); + assert!(result.contains("# ⏳ 2026H1 goal process")); + assert!(result.contains("- [Overview](./2026h1/README.md)")); + } + + #[test] + fn test_process_readme_content_no_existing_section() { + // Test adding a new section when no next goal period section exists + let content = "# Project goals\n\n## Current goal period (2025H1)\n\nThe 2025H1 goal period runs from Jan 1 to Jun 30."; + let result = process_readme_content(content, "2026h1", "2026h1"); + + assert!(result.contains("## Next goal period (2026H1)")); + assert!(result.contains("running from January 1 to June 30")); + assert!(result.contains("[Click here](./2026h1/goals.md)")); + } + + #[test] + fn test_process_readme_content_with_existing_section() { + // Test updating an existing section for the same timeframe + let content = "# Project goals\n\n## Current goal period (2025H1)\n\nThe 2025H1 goal period runs from Jan 1 to Jun 30.\n\n## Next goal period (2026H1)\n\nOld content."; + let result = process_readme_content(content, "2026h1", "2026h1"); + + assert!(result.contains("## Next goal period (2026H1)")); + assert!(result.contains("running from January 1 to June 30")); + assert!(!result.contains("Old content.")); + } + + #[test] + fn test_process_readme_content_with_existing_section_and_extra() { + // Test updating an existing section while preserving unrelated sections + let content = "# Project goals\n\n## Current goal period (2025H1)\n\nThe 2025H1 goal period runs from Jan 1 to Jun 30.\n\n## Next goal period (2026H1)\n\nOld content.\n\n## Extra section\nsome content"; + let result = process_readme_content(content, "2026h1", "2026h1"); + + assert!(result.contains("## Next goal period (2026H1)")); + assert!(result.contains("running from January 1 to June 30")); + assert!(!result.contains("Old content.")); + assert!(result.contains("## Extra section\nsome content")); + } + + #[test] + fn test_process_readme_content_with_different_timeframe() { + // Test replacing an existing next goal period section with a different timeframe + let content = "# Project goals\n\n## Current goal period (2025H1)\n\nThe 2025H1 goal period runs from Jan 1 to Jun 30.\n\n## Next goal period (2025H2)\n\nOld content."; + let result = process_readme_content(content, "2026h1", "2026h1"); + + assert!(result.contains("## Next goal period (2026H1)")); + assert!(!result.contains("## Next goal period (2025H2)")); + assert!(result.contains("running from January 1 to June 30")); + } + + #[test] + fn test_process_readme_content_second_half() { + // Test that the correct months are used for the second half of the year + let content = "# Project goals\n\n## Current goal period (2025H2)\n\nThe 2025H2 goal period runs from Jul 1 to Dec 31."; + let result = process_readme_content(content, "2026h2", "2026h2"); + + assert!(result.contains("## Next goal period (2026H2)")); + assert!(result.contains("running from the start of July to the end of December")); + } + + #[test] + fn test_process_readme_content_no_current_period() { + // Test adding a section when there's no current goal period section + let content = "# Project goals\n\n## About the process\n\nSome content."; + let result = process_readme_content(content, "2026h1", "2026h1"); + + assert!(result.contains("## Next goal period (2026H1)")); + assert!(result.contains("running from the start of January to the end of June")); + } +} diff --git a/crates/rust-project-goals-cli/src/main.rs b/crates/rust-project-goals-cli/src/main.rs index e44c5c6c..08e93bdc 100644 --- a/crates/rust-project-goals-cli/src/main.rs +++ b/crates/rust-project-goals-cli/src/main.rs @@ -6,6 +6,7 @@ use rust_project_goals_llm::UpdateArgs; use std::path::PathBuf; use walkdir::WalkDir; +mod cfp; mod generate_json; mod rfc; mod team_repo; @@ -29,6 +30,20 @@ enum Command { /// Print the RFC text to stdout RFC { path: PathBuf }, + + /// Set up a new Call For Proposals (CFP) period + CFP { + /// Timeframe for the new CFP period (e.g., 2025h1) + timeframe: String, + + /// Force overwrite without asking for confirmation + #[arg(short = 'f', long = "force")] + force: bool, + + /// Dry run - don't make any changes, just show what would be done + #[arg(short = 'n', long = "dry-run")] + dry_run: bool, + }, /// Use `gh` CLI tool to create issues on the rust-lang/rust-project-goals repository Issues { @@ -86,6 +101,10 @@ fn main() -> anyhow::Result<()> { rfc::generate_comment(&path)?; } + Command::CFP { timeframe, force, dry_run } => { + cfp::create_cfp(timeframe, *force, *dry_run)?; + } + Command::Check {} => { check()?; } diff --git a/src/2025h2/README.md b/src/2025h2/README.md new file mode 100644 index 00000000..2d8cb185 --- /dev/null +++ b/src/2025h2/README.md @@ -0,0 +1,103 @@ +# Rust project goals 2025h2 + + +## Summary + +*![Status: Accepting goal proposals](https://img.shields.io/badge/Status-Accepting%20goal%20proposals-yellow) We are in the process of assembling the goal slate.* + +This is a draft for the eventual RFC proposing the 2025h2 goals. + +## Motivation + +The 2025h2 goal slate consists of project goals, of which we have selected (TBD) as **flagship goals**. Flagship goals represent the goals expected to have the broadest overall impact. + +### How the goal process works + +**Project goals** are proposed bottom-up by a **point of contact**, somebody who is willing to commit resources (time, money, leadership) to seeing the work get done. The point of contact identifies the problem they want to address and sketches the solution of how they want to do so. They also identify the support they will need from the Rust teams (typically things like review bandwidth or feedback on RFCs). Teams then read the goals and provide feedback. If the goal is approved, teams are committing to support the point of contact in their work. + +Project goals can vary in scope from an internal refactoring that affects only one team to a larger cross-cutting initiative. No matter its scope, accepting a goal should never be interpreted as a promise that the team will make any future decision (e.g., accepting an RFC that has yet to be written). Rather, it is a promise that the team are aligned on the contents of the goal thus far (including the design axioms and other notes) and will prioritize giving feedback and support as needed. + +Of the proposed goals, a small subset are selected by the roadmap owner as **flagship goals**. Flagship goals are chosen for their high impact (many Rust users will be impacted) and their shovel-ready nature (the org is well-aligned around a concrete plan). Flagship goals are the ones that will feature most prominently in our public messaging and which should be prioritized by Rust teams where needed. + +### Rust’s mission + +Our goals are selected to further Rust's mission of **empowering everyone to build reliable and efficient software**. Rust targets programs that prioritize + +* reliability and robustness; +* performance, memory usage, and resource consumption; and +* long-term maintenance and extensibility. + +We consider "any two out of the three" as the right heuristic for projects where Rust is a strong contender or possibly the best option. + +### Axioms for selecting goals + +We believe that... + +* **Rust must deliver on its promise of peak performance and high reliability.** Rust’s maximum advantage is in applications that require peak performance or low-level systems capabilities. We must continue to innovate and support those areas above all. +* **Rust's goals require high productivity and ergonomics.** Being attentive to ergonomics broadens Rust impact by making it more appealing for projects that value reliability and maintenance but which don't have strict performance requirements. +* **Slow and steady wins the race.** For this first round of goals, we want a small set that can be completed without undue stress. As the Rust open source org continues to grow, the set of goals can grow in size. + +## Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +### Flagship goals + +The flagship goals proposed for this roadmap are as follows: + +(TBD) + +#### Why these particular flagship goals? + +(TBD--typically one paragraph per goal) + +### Project goals + +The full slate of project goals are as follows. These goals all have identified points of contact who will drive the work forward as well as a viable work plan. The goals include asks from the listed Rust teams, which are cataloged in the [reference-level explanation](#reference-level-explanation) section below. + +**Invited goals.** Some goals of the goals below are "invited goals", meaning that for that goal to happen we need someone to step up and serve as a point of contact. To find the invited goals, look for the ![Help wanted][] badge in the table below. Invited goals have reserved capacity for teams and a mentor, so if you are someone looking to help Rust progress, they are a great way to get involved. + + + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +The following table highlights the asks from each affected team. +The "owner" in the column is the person expecting to do the design/implementation work that the team will be approving. + + + +### Definitions + +Definitions for terms used above: + +* *Author RFC* and *Implementation* means actually writing the code, document, whatever. +* *Design meeting* means holding a synchronous meeting to review a proposal and provide feedback (no decision expected). +* *RFC decisions* means reviewing an RFC and deciding whether to accept. +* *Org decisions* means reaching a decision on an organizational or policy matter. +* *Secondary review* of an RFC means that the team is "tangentially" involved in the RFC and should be expected to briefly review. +* *Stabilizations* means reviewing a stabilization and report and deciding whether to stabilize. +* *Standard reviews* refers to reviews for PRs against the repository; these PRs are not expected to be unduly large or complicated. +* Other kinds of decisions: + * [Lang team experiments](https://lang-team.rust-lang.org/how_to/experiment.html) are used to add nightly features that do not yet have an RFC. They are limited to trusted contributors and are used to resolve design details such that an RFC can be written. + * Compiler [Major Change Proposal (MCP)](https://forge.rust-lang.org/compiler/mcp.html) is used to propose a 'larger than average' change and get feedback from the compiler team. + * Library [API Change Proposal (ACP)](https://std-dev-guide.rust-lang.org/development/feature-lifecycle.html) describes a change to the standard library. + +[AGS]: ./Project-goal-slate.md +[AMF]: ./a-mir-formality.md +[Async]: ./async.md +[ATPIT]: ./ATPIT.md +[CS]: ./cargo-script.md +[CT]: ./const-traits.md +[ERC]: ./ergonomic-rc.md +[MGCA]: ./min_generic_const_arguments.md +[NBNLB]: ./Polonius.md +[NGS]: ./next-solver.md +[PET]: ./Patterns-of-empty-types.md +[PGC]: ./pubgrub-in-cargo.md +[RFL]: ./rfl_stable.md +[SBS]: ./sandboxed-build-script.md +[YKR]: ./yank-crates-with-a-reason.md +[SC]: ./Rust-for-SciComp.md +[OC]: ./optimize-clippy.md + + \ No newline at end of file diff --git a/src/2025h2/goals.md b/src/2025h2/goals.md new file mode 100644 index 00000000..d75fee59 --- /dev/null +++ b/src/2025h2/goals.md @@ -0,0 +1,22 @@ +# Goals + + +This page lists the project goals **proposed** for 2025h2. + +> Just because a goal is listed on this list does not mean the goal has been accepted. +> The owner of the goal process makes the final decisions on which goals to include +> and prepares an RFC to ask approval from the teams. + +## Flagship goals + +Flagship goals represent the goals expected to have the broadest overall impact. + + + +## Other goals + +These are the other proposed goals. + +**Invited goals.** Some goals of the goals below are "invited goals", meaning that for that goal to happen we need someone to step up and serve as a point of contact. To find the invited goals, look for the ![Help wanted][] badge in the table below. Invited goals have reserved capacity for teams and a mentor, so if you are someone looking to help Rust progress, they are a great way to get involved. + + \ No newline at end of file diff --git a/src/2025h2/not_accepted.md b/src/2025h2/not_accepted.md new file mode 100644 index 00000000..6ef16dbc --- /dev/null +++ b/src/2025h2/not_accepted.md @@ -0,0 +1,6 @@ +# Not accepted + + +This section contains goals that were proposed but ultimately not accepted, either for want of resources or consensus. In many cases, narrower versions of these goals were proposed instead. + + \ No newline at end of file diff --git a/src/README.md b/src/README.md index 813ff5c8..f3ac1779 100644 --- a/src/README.md +++ b/src/README.md @@ -22,9 +22,10 @@ The 2025H1 goal period runs from Jan 1 to Jun 30. We have identified three flags [The full list of 2025H1 goals is available here.](./2025h1/goals.md) We author monthly blog posts about our overall status, but you can also follow the tracking issue for a [particular goal](./2025h1/goals.md) to get updates specific to that goal. -## Next goal period (2025h2) -Discussion about goals for the next goal period will begin in May. +## Next goal period (2025H2) + +The next goal period will be 2025H2, running from July 1 to December 30. We are currently in the process of assembling goals. [Click here](./2025h2/goals.md) to see the current list. If you'd like to propose a goal, [instructions can be found here](./how_to/propose_a_goal.md). ## About the process diff --git a/src/SUMMARY.md b/src/SUMMARY.md index def710e3..18aa668a 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -10,7 +10,9 @@ # ⏳ 2025H2 goal process -- [Not yet started]() +- [Overview](./2025h2/README.md) +- [Proposed goals](./2025h2/goals.md) +- [Goals not accepted](./2025h2/not_accepted.md) # 📖 Learn about diff --git a/src/admin/cfp.md b/src/admin/cfp.md index 9996741b..4a8bdb74 100644 --- a/src/admin/cfp.md +++ b/src/admin/cfp.md @@ -5,7 +5,50 @@ Each goal milestone corresponds to six months, designated in the format YYYYhN, * For an H1 season, start around mid October of the year before. * For an H2 season, start around mid April of the year before. -This is the checklist of steps to starting accepting goal proposals: +## Using the automated setup command + +The easiest way to set up a new Call For Proposals (CFP) period is to use the `cargo rpg cfp` command. This command automates the process of creating the necessary directory structure, copying template files, and updating both the SUMMARY.md and README.md files. + +```bash +# Basic usage +cargo rpg cfp 2025h2 + +# Force overwrite without asking for confirmation +cargo rpg cfp 2025h2 --force + +# Dry run - don't make any changes, just show what would be done +cargo rpg cfp 2025h2 --dry-run +``` + +The command will: +1. Create a new directory for the specified timeframe (e.g., `src/2025h2/`) +2. Copy and process template files from `src/admin/samples/` to the new directory +3. Update the `SUMMARY.md` file to include the new timeframe section +4. Update the main `README.md` with information about the new timeframe + +## Manual steps required + +After running the `cargo rpg cfp` command, there are still important manual steps that need to be completed: + +### 1. Prepare and publish a blog post + +You need to prepare a Call For Proposals blog post on the [Inside Rust] blog: +* Use [this sample](./samples/cfp.md) as a starting point +* Copy the sample to the `blog.rust-lang.org` repository as a new post +* Replace placeholders like `YYYYHN` with the actual timeframe (e.g., `2025H2`) +* We use Inside Rust and not the Main blog because the target audience is would-be Rust contributors and maintainers + +### 2. Email the mailing list + +Send an email to the `all@rust-lang.org` mailing list to announce the Call For Proposals: +* Include a link to the blog post +* Summarize the key dates and process +* Encourage team participation and feedback +* This step is crucial for ensuring all Rust team members are aware of the upcoming goal period + +## Manual setup checklist + +If you prefer to set up the CFP manually, or need to customize the process beyond what the automated command provides, here's a checklist of steps: * [ ] Prepare a Call For Proposals blog post on the [Inside Rust] blog based on [this sample](./samples/cfp.md). * We use Inside Rust and not the Main blog because the target audience is would-be Rust contributors and maintainers. diff --git a/src/admin/commands.md b/src/admin/commands.md index 211c0726..cefa4043 100644 --- a/src/admin/commands.md +++ b/src/admin/commands.md @@ -3,3 +3,29 @@ The `cargo rpg` command is a CLI for manipulating and checking project goals. This section provides a reference describing (some of) the ability commands. You can also try `cargo rpg --help` to get a summary. Note that this relies on the [`gh` client](https://github.com/cli/cli), which needs to be installed and configured with a token (for example using `gh auth login`). + +## Available Commands + +### `cargo rpg cfp` + +Sets up a new Call For Proposals (CFP) period. This command automates the process of creating the necessary directory structure, copying template files, and updating both the SUMMARY.md and README.md files. + +```bash +# Basic usage +cargo rpg cfp + +# Options +cargo rpg cfp --force # Force overwrite without asking for confirmation +cargo rpg cfp --dry-run # Don't make any changes, just show what would be done +``` + +Example: +```bash +cargo rpg cfp 2025h2 +``` + +Note that after running this command, you'll still need to manually: +1. Prepare and publish a blog post on the Inside Rust blog +2. Send an email to the `all@rust-lang.org` mailing list + +For more details, see the [Call for proposals](./cfp.md) documentation. diff --git a/src/admin/samples/rfc.md b/src/admin/samples/rfc.md index 676c5b9d..b8dca055 100644 --- a/src/admin/samples/rfc.md +++ b/src/admin/samples/rfc.md @@ -1,4 +1,4 @@ -# Sample RFC +# Rust project goals YYYYHN > **NOTE:** This is a sample RFC you can use as a starting point. > To begin a new goal season (e.g., 2222H1), do the following: