#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub async fn handle_spec_score(
spec_path: &Path,
format: SpecOutputFormat,
output: Option<&Path>,
verbose: bool,
) -> anyhow::Result<()> {
let parser = SpecParser::new();
let spec = parser.parse_file(spec_path)?;
let score = calculate_spec_score(&spec);
let output_text = match format {
SpecOutputFormat::Text => format_spec_score_text(&spec, score, verbose),
SpecOutputFormat::Json => format_spec_score_json(&spec, score)?,
SpecOutputFormat::Markdown => format_spec_score_markdown(&spec, score),
};
if let Some(output_path) = output {
use crate::cli::colors as c;
fs::write(output_path, &output_text)?;
println!("{}", c::pass(&format!("Spec score written to {}", c::path(&output_path.display().to_string()))));
} else {
println!("{}", output_text);
}
if score < 95.0 {
use crate::cli::colors as c;
println!(
"\n{}",
c::warn(&format!(
"Spec score {:.1} is below 95 threshold. Run `pmat spec comply` to fix.",
score
))
);
std::process::exit(1);
}
Ok(())
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub async fn handle_spec_comply(
spec_path: &Path,
dry_run: bool,
_format: SpecOutputFormat,
) -> anyhow::Result<()> {
let parser = SpecParser::new();
let spec = parser.parse_file(spec_path)?;
let mut fixes: Vec<String> = Vec::new();
if spec.issue_refs.is_empty() {
fixes.push(
"- Add issue_refs in YAML frontmatter (e.g., issue_refs: [\"#123\"])".to_string(),
);
}
if spec.code_examples.len() < 5 {
fixes.push(format!(
"- Add {} more code examples (minimum 5 required)",
5 - spec.code_examples.len()
));
}
if spec.acceptance_criteria.len() < 10 {
fixes.push(format!(
"- Add {} more acceptance criteria (minimum 10 required)",
10 - spec.acceptance_criteria.len()
));
}
let citation_count = {
let mut seen = std::collections::HashSet::new();
let re = regex::Regex::new(r"\[(\d+)\]").expect("internal error");
for caps in re.captures_iter(&spec.raw_content) {
if let Some(m) = caps.get(1) {
seen.insert(m.as_str().to_string());
}
}
seen.len()
};
if citation_count < 5 {
fixes.push(format!(
"- Add {} more peer-reviewed citations (found {}, minimum 5 required)",
5 - citation_count,
citation_count
));
}
if fixes.is_empty() {
use crate::cli::colors as c;
println!("{}", c::pass("Spec already meets all requirements!"));
return Ok(());
}
{
use crate::cli::colors as c;
println!("{}\n", c::header("Spec Compliance Issues Found:"));
for fix in &fixes {
println!("{}", fix);
}
if dry_run {
println!("\n{}", c::dim("(Dry run - no changes made)"));
} else {
println!("\n{}", c::warn("Auto-fix not yet implemented. Please apply fixes manually."));
}
}
Ok(())
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub async fn handle_spec_create(
name: &str,
issue: Option<&str>,
epic: Option<&str>,
output: Option<&Path>,
) -> anyhow::Result<()> {
let slug = name.to_lowercase().replace(' ', "-");
let output_dir = output
.map(|p| p.to_path_buf())
.unwrap_or_else(|| Path::new("docs/specifications").to_path_buf());
let file_path = output_dir.join(format!("{}.md", slug));
let issue_ref = issue.unwrap_or("#TODO");
let epic_name = epic.unwrap_or("PMAT-TODO");
let date = chrono::Local::now().format("%Y-%m-%d");
let template = format!(
r#"---
title: "{name}"
version: "1.0.0"
status: "Draft"
created: "{date}"
updated: "{date}"
issue_refs: ["{issue_ref}"]
epic: "{epic_name}"
---
# {name}
## Executive Summary
[Brief description of what this specification defines]
## Scientific Foundation
[Cite minimum 5 peer-reviewed sources]
1. [Author et al., Year. Title. Journal/Conference.]
2. [...]
## Requirements
### Functional Requirements
- [ ] FR-001: [Requirement description]
- [ ] FR-002: [Requirement description]
### Non-Functional Requirements
- [ ] NFR-001: [Performance requirement]
- [ ] NFR-002: [Security requirement]
## Acceptance Criteria
### Category 1 (AC-001 to AC-005)
- [ ] AC-001: [Testable criterion]
- [ ] AC-002: [Testable criterion]
- [ ] AC-003: [Testable criterion]
- [ ] AC-004: [Testable criterion]
- [ ] AC-005: [Testable criterion]
### Category 2 (AC-006 to AC-010)
- [ ] AC-006: [Testable criterion]
- [ ] AC-007: [Testable criterion]
- [ ] AC-008: [Testable criterion]
- [ ] AC-009: [Testable criterion]
- [ ] AC-010: [Testable criterion]
## Code Examples
### Example 1: Basic Usage
```rust
// Example code here
```
### Example 2: Advanced Usage
```rust
// Example code here
```
### Example 3: Error Handling
```rust
// Example code here
```
### Example 4: Integration
```rust
// Example code here
```
### Example 5: Performance
```rust
// Example code here
```
## Testing Strategy
- Unit tests: [Coverage target]
- Integration tests: [Scope]
- Property tests: [Properties to verify]
## References
[1] [Citation]
[2] [Citation]
[3] [Citation]
[4] [Citation]
[5] [Citation]
"#
);
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&file_path, template)?;
{
use crate::cli::colors as c;
println!("{}", c::pass(&format!("Created specification: {}", c::path(&file_path.display().to_string()))));
println!("\n{}", c::label("Next steps:"));
println!(" 1. Edit the specification with your requirements");
println!(
" 2. Run `{}` to validate",
c::label(&format!("pmat spec score {}", file_path.display()))
);
println!(
" 3. Run `{}` to fix issues",
c::label(&format!("pmat spec comply {}", file_path.display()))
);
}
Ok(())
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub async fn handle_spec_list(
path: &Path,
min_score: Option<u8>,
failing_only: bool,
format: SpecOutputFormat,
) -> anyhow::Result<()> {
let parser = SpecParser::new();
let specs = parser.find_specs(path)?;
let mut results = Vec::new();
for spec_path in specs {
if let Ok(spec) = parser.parse_file(&spec_path) {
let score = calculate_spec_score(&spec);
let passing = score >= 95.0;
if let Some(min) = min_score {
if score < f64::from(min) {
continue;
}
}
if failing_only && passing {
continue;
}
results.push((spec_path, spec.title.clone(), score, passing));
}
}
print_spec_list(&results, path, format)?;
Ok(())
}
fn spec_display_name<'a>(path: &'a Path, title: &'a str) -> &'a str {
if title.is_empty() {
path.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
} else {
title
}
}
fn print_spec_list(
results: &[(std::path::PathBuf, String, f64, bool)],
dir: &Path,
format: SpecOutputFormat,
) -> anyhow::Result<()> {
match format {
SpecOutputFormat::Text => {
use crate::cli::colors as c;
println!("{}\n", c::header(&format!("Specifications in {}", c::path(&dir.display().to_string()))));
println!(
"{}{:<50}{} {:>8} {:>8}",
c::BOLD, "SPECIFICATION", c::RESET, "SCORE", "STATUS"
);
println!("{}", c::separator());
for (path, title, score, passing) in results {
let status = if *passing {
c::pass("PASS")
} else {
c::fail("FAIL")
};
let score_str = c::pct(*score, 95.0, 80.0);
println!("{:<50} {:>15} {}", spec_display_name(path, title), score_str, status);
}
println!(
"\n{} {} specs, {} passing",
c::label("Total:"),
c::number(&results.len().to_string()),
c::number(&results.iter().filter(|(_, _, _, p)| *p).count().to_string())
);
}
SpecOutputFormat::Json => {
let json_results: Vec<_> = results
.iter()
.map(|(path, title, score, passing)| {
serde_json::json!({
"path": path.display().to_string(),
"title": title,
"score": score,
"passing": passing,
})
})
.collect();
println!("{}", serde_json::to_string_pretty(&json_results)?);
}
SpecOutputFormat::Markdown => {
println!("# Specification Status Report\n");
println!("| Specification | Score | Status |");
println!("|---------------|-------|--------|");
for (path, title, score, passing) in results {
let status = if *passing { "✅ PASS" } else { "❌ FAIL" };
println!("| {} | {:.1} | {} |", spec_display_name(path, title), score, status);
}
}
}
Ok(())
}