ref:main
use crate::client::Client;
use crate::config;
use crate::output;
use clap::{Args, Subcommand};
use serde::Deserialize;
#[derive(Args)]
pub struct RequirementArgs {
#[command(subcommand)]
pub command: RequirementCommand,
}
#[derive(Subcommand)]
pub enum RequirementCommand {
/// List requirements for a repository
List {
/// Filter by status (draft, active, deprecated)
#[arg(long)]
status: Option<String>,
/// Filter by category (functional, security, performance, reliability, usability, integration)
#[arg(long)]
category: Option<String>,
/// Repository (org/repo)
#[arg(long)]
repo: Option<String>,
},
/// View a specific requirement
View {
/// Requirement ID (e.g. REQ-ACCT-001)
id: String,
/// Repository (org/repo)
#[arg(long)]
repo: Option<String>,
},
/// Create a new requirement
Create {
/// Requirement ID (e.g. REQ-ACCT-001)
#[arg(long)]
requirement_id: String,
/// Title
#[arg(long)]
title: String,
/// Description
#[arg(long)]
description: Option<String>,
/// Category
#[arg(long)]
category: Option<String>,
/// Priority (critical, high, medium, low)
#[arg(long)]
priority: Option<String>,
/// Status (draft, active, deprecated)
#[arg(long)]
status: Option<String>,
/// Parent requirement UUID (for hierarchy)
#[arg(long)]
parent_id: Option<String>,
/// Repository (org/repo)
#[arg(long)]
repo: Option<String>,
},
/// Update an existing requirement
Update {
/// Requirement ID (e.g. REQ-ACCT-001)
id: String,
/// New title
#[arg(long)]
title: Option<String>,
/// New description
#[arg(long)]
description: Option<String>,
/// New category
#[arg(long)]
category: Option<String>,
/// New priority
#[arg(long)]
priority: Option<String>,
/// New status
#[arg(long)]
status: Option<String>,
/// Repository (org/repo)
#[arg(long)]
repo: Option<String>,
},
/// Show the traceability matrix
Matrix {
/// Repository (org/repo)
#[arg(long)]
repo: Option<String>,
},
/// Check requirement coverage status (exit 1 if uncovered requirements exist)
Status(StatusArgs),
/// Seed all Anvil platform requirements (204 requirements across 16 domains)
Seed {
/// Repository (org/repo)
#[arg(long)]
repo: Option<String>,
/// Clear existing requirements before seeding
#[arg(long)]
clear: bool,
},
}
#[derive(Args)]
pub struct StatusArgs {
/// Repository (org/repo)
#[arg(long)]
repo: Option<String>,
/// Fail on partial coverage too (default: only fail on uncovered)
#[arg(long)]
strict: bool,
}
#[derive(Debug, Deserialize)]
struct Requirement {
#[allow(dead_code)]
id: Option<String>,
requirement_id: Option<String>,
title: Option<String>,
category: Option<String>,
priority: Option<String>,
status: Option<String>,
test_count: Option<u32>,
}
#[derive(Debug, Deserialize)]
struct RequirementDetail {
id: Option<String>,
requirement_id: Option<String>,
title: Option<String>,
description: Option<String>,
category: Option<String>,
priority: Option<String>,
status: Option<String>,
version: Option<String>,
parent_id: Option<String>,
children: Option<Vec<serde_json::Value>>,
test_links: Option<Vec<serde_json::Value>>,
}
pub async fn run(args: RequirementArgs) -> Result<(), Box<dyn std::error::Error>> {
match args.command {
RequirementCommand::List {
status,
category,
repo,
} => list(repo.as_deref(), status.as_deref(), category.as_deref()).await,
RequirementCommand::View { id, repo } => view(repo.as_deref(), &id).await,
RequirementCommand::Create {
requirement_id,
title,
description,
category,
priority,
status,
parent_id,
repo,
} => {
create(
repo.as_deref(),
&requirement_id,
&title,
description.as_deref(),
category.as_deref(),
priority.as_deref(),
status.as_deref(),
parent_id.as_deref(),
)
.await
}
RequirementCommand::Update {
id,
title,
description,
category,
priority,
status,
repo,
} => {
update(
repo.as_deref(),
&id,
title.as_deref(),
description.as_deref(),
category.as_deref(),
priority.as_deref(),
status.as_deref(),
)
.await
}
RequirementCommand::Matrix { repo } => matrix(repo.as_deref()).await,
RequirementCommand::Status(args) => status(args).await,
RequirementCommand::Seed { repo, clear } => seed(repo.as_deref(), clear).await,
}
}
async fn list(
repo: Option<&str>,
status: Option<&str>,
category: Option<&str>,
) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
let mut query = Vec::new();
if let Some(s) = status {
query.push(("status", s));
}
if let Some(c) = category {
query.push(("category", c));
}
let resp: serde_json::Value = client
.get_with_query(&format!("/{org}/{name}/requirements"), &query)
.await?;
let reqs: Vec<Requirement> = resp
.get("requirements")
.and_then(|v| serde_json::from_value(v.clone()).ok())
.unwrap_or_default();
output::header(&format!("Requirements ({org}/{name})"));
let rows: Vec<Vec<String>> = reqs
.iter()
.map(|r| {
vec![
r.requirement_id.clone().unwrap_or_default(),
r.title.clone().unwrap_or_default(),
r.category.clone().unwrap_or_default(),
r.priority.clone().unwrap_or_default(),
r.status.clone().unwrap_or_default(),
r.test_count.map(|c| c.to_string()).unwrap_or_default(),
]
})
.collect();
output::print_table(
&["ID", "TITLE", "CATEGORY", "PRIORITY", "STATUS", "TESTS"],
&rows,
);
println!("\n{} requirements", rows.len());
Ok(())
}
async fn view(repo: Option<&str>, req_id: &str) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
let req: RequirementDetail = client
.get(&format!("/{org}/{name}/requirements/{req_id}"))
.await?;
output::header(&format!(
"Requirement {} — {}",
req.requirement_id.as_deref().unwrap_or("?"),
req.title.as_deref().unwrap_or("?")
));
output::detail("ID", req.requirement_id.as_deref().unwrap_or("-"));
output::detail("Title", req.title.as_deref().unwrap_or("-"));
output::detail(
"Description",
req.description.as_deref().unwrap_or("(none)"),
);
output::detail("Category", req.category.as_deref().unwrap_or("-"));
output::detail("Priority", req.priority.as_deref().unwrap_or("-"));
output::detail("Status", req.status.as_deref().unwrap_or("-"));
output::detail("Version", req.version.as_deref().unwrap_or("-"));
output::detail("Parent", req.parent_id.as_deref().unwrap_or("(top-level)"));
let children = req.children.as_ref().map(|c| c.len()).unwrap_or(0);
output::detail("Children", &children.to_string());
let tests = req.test_links.as_ref().map(|t| t.len()).unwrap_or(0);
output::detail("Linked Tests", &tests.to_string());
// Show UUID for use as parent_id
if let Some(id) = &req.id {
output::detail("UUID", id);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn create(
repo: Option<&str>,
requirement_id: &str,
title: &str,
description: Option<&str>,
category: Option<&str>,
priority: Option<&str>,
status: Option<&str>,
parent_id: Option<&str>,
) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
let mut payload = serde_json::json!({
"requirement_id": requirement_id,
"title": title,
});
if let Some(d) = description {
payload["description"] = serde_json::json!(d);
}
if let Some(c) = category {
payload["category"] = serde_json::json!(c);
}
if let Some(p) = priority {
payload["priority"] = serde_json::json!(p);
}
if let Some(s) = status {
payload["status"] = serde_json::json!(s);
}
if let Some(pid) = parent_id {
payload["parent_id"] = serde_json::json!(pid);
}
let resp: RequirementDetail = client
.post(&format!("/{org}/{name}/requirements"), &payload)
.await?;
output::success(&format!(
"Created requirement '{}' — {}",
resp.requirement_id.as_deref().unwrap_or(requirement_id),
resp.title.as_deref().unwrap_or(title)
));
if let Some(id) = &resp.id {
output::detail("UUID", id);
}
Ok(())
}
async fn update(
repo: Option<&str>,
req_id: &str,
title: Option<&str>,
description: Option<&str>,
category: Option<&str>,
priority: Option<&str>,
status: Option<&str>,
) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
let mut payload = serde_json::json!({});
if let Some(t) = title {
payload["title"] = serde_json::json!(t);
}
if let Some(d) = description {
payload["description"] = serde_json::json!(d);
}
if let Some(c) = category {
payload["category"] = serde_json::json!(c);
}
if let Some(p) = priority {
payload["priority"] = serde_json::json!(p);
}
if let Some(s) = status {
payload["status"] = serde_json::json!(s);
}
let resp: RequirementDetail = client
.put(&format!("/{org}/{name}/requirements/{req_id}"), &payload)
.await?;
output::success(&format!(
"Updated requirement '{}' — {}",
resp.requirement_id.as_deref().unwrap_or(req_id),
resp.title.as_deref().unwrap_or("?")
));
Ok(())
}
async fn matrix(repo: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
let resp: serde_json::Value = client
.get(&format!("/{org}/{name}/requirements/matrix"))
.await?;
let entries = resp
.get("matrix")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
output::header(&format!("Traceability Matrix ({org}/{name})"));
let rows: Vec<Vec<String>> = entries
.iter()
.map(|entry| {
let req = &entry["requirement"];
let coverage = entry["coverage_status"]
.as_str()
.unwrap_or("unknown")
.to_string();
let tests = entry["tests"].as_array().map(|t| t.len()).unwrap_or(0);
vec![
req["requirement_id"].as_str().unwrap_or("-").to_string(),
req["title"].as_str().unwrap_or("-").to_string(),
coverage,
tests.to_string(),
]
})
.collect();
output::print_table(&["ID", "TITLE", "COVERAGE", "TESTS"], &rows);
println!("\n{} requirements in matrix", rows.len());
Ok(())
}
async fn status(args: StatusArgs) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(args.repo.as_deref())?;
let resp: serde_json::Value = client
.get(&format!("/{org}/{name}/requirements/matrix"))
.await?;
let entries = resp
.get("matrix")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
if entries.is_empty() {
output::warn("No requirements found. Create requirements first.");
return Ok(());
}
let mut covered = 0usize;
let mut partial = 0usize;
let mut uncovered = 0usize;
let mut no_tests = 0usize;
for entry in &entries {
match entry["coverage_status"].as_str().unwrap_or("unknown") {
"covered" => covered += 1,
"partial" => partial += 1,
"uncovered" => uncovered += 1,
"no_tests" => no_tests += 1,
_ => {}
}
}
let total = entries.len();
output::header("Requirement Coverage Status");
println!(" Covered: {} ✓", covered);
println!(" Partial: {} ◐", partial);
println!(" Uncovered: {} ✗", uncovered);
println!(" No tests: {} ○", no_tests);
println!(" Total: {}", total);
println!();
let fail = if args.strict {
uncovered > 0 || partial > 0 || no_tests > 0
} else {
uncovered > 0
};
if fail {
output::error("Requirement coverage check FAILED");
println!();
for entry in &entries {
let status = entry["coverage_status"].as_str().unwrap_or("");
let req_id = entry["requirement"]["requirement_id"]
.as_str()
.unwrap_or("?");
let title = entry["requirement"]["title"].as_str().unwrap_or("?");
let should_list = status == "uncovered"
|| (args.strict && (status == "partial" || status == "no_tests"));
if should_list {
output::detail(req_id, &format!("{} — {}", title, status));
}
}
Err(format!("{} uncovered requirement(s)", uncovered).into())
} else {
output::success("Requirement coverage check passed");
Ok(())
}
}
// ── Seed ─────────────────────────────────────────────────────────
struct ReqDef {
id: &'static str,
category: &'static str,
title: &'static str,
priority: &'static str,
}
struct DomainDef {
id: &'static str,
title: &'static str,
description: &'static str,
children: Vec<ReqDef>,
}
async fn seed(repo: Option<&str>, clear: bool) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
if clear {
output::warn("Clearing existing requirements...");
// List and delete all existing requirements (children first, then parents)
let resp: serde_json::Value = client.get(&format!("/{org}/{name}/requirements")).await?;
let existing: Vec<serde_json::Value> = resp
.get("requirements")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
// We can't easily delete via API since there's no delete endpoint exposed.
// Instead, we'll just create and let unique constraints tell us what already exists.
if !existing.is_empty() {
output::warn(&format!(
"Found {} existing requirements. Duplicates will be skipped.",
existing.len()
));
}
}
let domains = all_domains();
let mut created = 0;
let mut skipped = 0;
for domain in &domains {
// Create parent
let parent_payload = serde_json::json!({
"requirement_id": domain.id,
"title": domain.title,
"description": domain.description,
"category": "functional",
"priority": "high",
"status": "active",
});
let parent_result: Result<RequirementDetail, _> = client
.post(&format!("/{org}/{name}/requirements"), &parent_payload)
.await;
let parent_uuid = match parent_result {
Ok(detail) => {
output::success(&format!("Created parent: {} — {}", domain.id, domain.title));
created += 1;
detail.id
}
Err(e) => {
let err_msg = format!("{e}");
if err_msg.contains("422")
|| err_msg.contains("unique")
|| err_msg.contains("already")
{
// Already exists — fetch it to get the UUID
let existing: RequirementDetail = client
.get(&format!("/{org}/{name}/requirements/{}", domain.id))
.await?;
output::info(&format!("Exists: {} — {}", domain.id, domain.title));
skipped += 1;
existing.id
} else {
return Err(e.into());
}
}
};
// Create children
for child in &domain.children {
let mut payload = serde_json::json!({
"requirement_id": child.id,
"title": child.title,
"category": child.category,
"priority": child.priority,
"status": "active",
});
if let Some(pid) = &parent_uuid {
payload["parent_id"] = serde_json::json!(pid);
}
let result: Result<RequirementDetail, _> = client
.post(&format!("/{org}/{name}/requirements"), &payload)
.await;
match result {
Ok(_) => {
created += 1;
eprint!(".");
}
Err(e) => {
let err_msg = format!("{e}");
if err_msg.contains("422")
|| err_msg.contains("unique")
|| err_msg.contains("already")
{
skipped += 1;
eprint!("s");
} else {
eprintln!("\nError creating {}: {e}", child.id);
return Err(e.into());
}
}
}
}
eprintln!();
}
println!();
output::success(&format!(
"Done! Created: {created}, Skipped (existing): {skipped}, Total: {}",
created + skipped
));
Ok(())
}
fn all_domains() -> Vec<DomainDef> {
vec![
DomainDef {
id: "REQ-ACCT",
title: "Accounts",
description: "User registration, authentication, organizations, teams, memberships, SSH keys, API tokens, roles",
children: vec![
ReqDef { id: "REQ-ACCT-001", category: "functional", title: "Users can register with email and password", priority: "high" },
ReqDef { id: "REQ-ACCT-002", category: "functional", title: "Users can log in with email and password", priority: "high" },
ReqDef { id: "REQ-ACCT-003", category: "functional", title: "Users can log in via Google OAuth", priority: "medium" },
ReqDef { id: "REQ-ACCT-004", category: "functional", title: "Users can log in via magic link", priority: "medium" },
ReqDef { id: "REQ-ACCT-005", category: "functional", title: "Users can update their profile (email, username, password)", priority: "medium" },
ReqDef { id: "REQ-ACCT-006", category: "functional", title: "Users can create organizations", priority: "high" },
ReqDef { id: "REQ-ACCT-007", category: "functional", title: "Organizations can have multiple members with roles (owner, admin, member)", priority: "high" },
ReqDef { id: "REQ-ACCT-008", category: "functional", title: "Organization owners can add and remove members", priority: "high" },
ReqDef { id: "REQ-ACCT-009", category: "functional", title: "Users can manage SSH keys (add, list, delete)", priority: "high" },
ReqDef { id: "REQ-ACCT-010", category: "functional", title: "Users can create and revoke personal access tokens", priority: "high" },
ReqDef { id: "REQ-ACCT-011", category: "functional", title: "Admins can list, search, enable, and disable users", priority: "medium" },
ReqDef { id: "REQ-ACCT-012", category: "functional", title: "Admins can grant and revoke site admin status", priority: "medium" },
ReqDef { id: "REQ-ACCT-013", category: "security", title: "Passwords are hashed (not stored in plaintext)", priority: "critical" },
ReqDef { id: "REQ-ACCT-014", category: "security", title: "Session tokens have configurable expiration", priority: "critical" },
ReqDef { id: "REQ-ACCT-015", category: "security", title: "Personal access tokens are scoped to specific permissions", priority: "critical" },
ReqDef { id: "REQ-ACCT-016", category: "security", title: "Sudo mode requires re-authentication within 20-minute window", priority: "high" },
ReqDef { id: "REQ-ACCT-017", category: "security", title: "Disabling a user revokes all active sessions", priority: "critical" },
ReqDef { id: "REQ-ACCT-018", category: "security", title: "SSH key fingerprints are unique per user", priority: "high" },
ReqDef { id: "REQ-ACCT-019", category: "usability", title: "SSH key last-used timestamp is tracked and displayed", priority: "low" },
ReqDef { id: "REQ-ACCT-020", category: "integration", title: "SSH keys API supports list, add, and delete operations", priority: "medium" },
ReqDef { id: "REQ-ACCT-021", category: "integration", title: "PAT authentication works for API and git HTTPS access", priority: "high" },
ReqDef { id: "REQ-ACCT-022", category: "functional", title: "Teams can be created within organizations", priority: "high" },
ReqDef { id: "REQ-ACCT-023", category: "functional", title: "Team members can be added and removed", priority: "high" },
ReqDef { id: "REQ-ACCT-024", category: "functional", title: "Teams can be granted repository access with a permission level", priority: "high" },
ReqDef { id: "REQ-ACCT-025", category: "integration", title: "Team API supports CRUD operations", priority: "medium" },
ReqDef { id: "REQ-ACCT-026", category: "integration", title: "Git clone/push over HTTPS authenticates via personal access token", priority: "high" },
],
},
DomainDef {
id: "REQ-GIT",
title: "Git & Repositories",
description: "Repository CRUD, branches, tags, commits, file browsing, forks, visibility, archival",
children: vec![
ReqDef { id: "REQ-GIT-001", category: "functional", title: "Users can create repositories (public or private)", priority: "high" },
ReqDef { id: "REQ-GIT-002", category: "functional", title: "Repositories are initialized as bare git repos on disk", priority: "high" },
ReqDef { id: "REQ-GIT-003", category: "functional", title: "Users can list repositories (all, by org, with search and pagination)", priority: "medium" },
ReqDef { id: "REQ-GIT-004", category: "functional", title: "Users can update repository name, description, and visibility", priority: "medium" },
ReqDef { id: "REQ-GIT-005", category: "functional", title: "Default branch is auto-detected on first push", priority: "high" },
ReqDef { id: "REQ-GIT-006", category: "functional", title: "Repositories can be archived and soft-deleted", priority: "medium" },
ReqDef { id: "REQ-GIT-007", category: "functional", title: "Users can fork repositories", priority: "medium" },
ReqDef { id: "REQ-GIT-008", category: "functional", title: "Users can browse repository file tree", priority: "high" },
ReqDef { id: "REQ-GIT-009", category: "functional", title: "Users can view file contents (blobs)", priority: "high" },
ReqDef { id: "REQ-GIT-010", category: "functional", title: "Users can view commit history with pagination", priority: "high" },
ReqDef { id: "REQ-GIT-011", category: "functional", title: "Users can view commit details with diffs", priority: "high" },
ReqDef { id: "REQ-GIT-012", category: "functional", title: "Users can list, create, and delete branches", priority: "high" },
ReqDef { id: "REQ-GIT-013", category: "functional", title: "Users can list and create tags", priority: "medium" },
ReqDef { id: "REQ-GIT-014", category: "functional", title: "Diff computation shows files changed, insertions, and deletions", priority: "high" },
ReqDef { id: "REQ-GIT-015", category: "security", title: "Repository paths are never exposed to the web layer", priority: "critical" },
ReqDef { id: "REQ-GIT-016", category: "security", title: "Private repositories are only visible to members", priority: "critical" },
ReqDef { id: "REQ-GIT-017", category: "reliability", title: "Git operations use NIF (libgit2), never shell out to git CLI", priority: "high" },
ReqDef { id: "REQ-GIT-018", category: "integration", title: "Git operations are traced via OpenTelemetry", priority: "low" },
ReqDef { id: "REQ-GIT-019", category: "integration", title: "Repository API supports create and get operations", priority: "medium" },
ReqDef { id: "REQ-GIT-020", category: "integration", title: "Branch API supports list, create, and delete operations", priority: "medium" },
ReqDef { id: "REQ-GIT-021", category: "integration", title: "Commit API supports list (paginated, by branch) and get operations", priority: "medium" },
],
},
DomainDef {
id: "REQ-SSH",
title: "SSH Transport",
description: "SSH daemon, key authentication, git push/pull protocol, channel lifecycle",
children: vec![
ReqDef { id: "REQ-SSH-001", category: "functional", title: "SSH daemon accepts git push and pull operations", priority: "high" },
ReqDef { id: "REQ-SSH-002", category: "functional", title: "SSH authenticates users by public key fingerprint", priority: "high" },
ReqDef { id: "REQ-SSH-003", category: "functional", title: "Push operations update repository refs", priority: "high" },
ReqDef { id: "REQ-SSH-004", category: "security", title: "Only authorized users can push to repositories", priority: "critical" },
ReqDef { id: "REQ-SSH-005", category: "reliability", title: "Channel closes only after both git process exit and client EOF", priority: "high" },
ReqDef { id: "REQ-SSH-006", category: "reliability", title: "Safety timeout (10s) handles missing client EOF", priority: "medium" },
ReqDef { id: "REQ-SSH-007", category: "reliability", title: "Port exit_status is handled without duplicate processing", priority: "medium" },
ReqDef { id: "REQ-SSH-008", category: "integration", title: "SSH daemon runs on port 22, system SSH on port 2200", priority: "high" },
],
},
DomainDef {
id: "REQ-PR",
title: "Pull Requests & Code Review",
description: "PR lifecycle, merge strategies, reviews, inline comments, anchor engine, merge queue",
children: vec![
ReqDef { id: "REQ-PR-001", category: "functional", title: "Users can create pull requests between branches", priority: "high" },
ReqDef { id: "REQ-PR-002", category: "functional", title: "PRs have title, body, and state (open/closed/merged)", priority: "high" },
ReqDef { id: "REQ-PR-003", category: "functional", title: "PRs can be listed with state filtering", priority: "medium" },
ReqDef { id: "REQ-PR-004", category: "functional", title: "PR title and body can be updated", priority: "medium" },
ReqDef { id: "REQ-PR-005", category: "functional", title: "PRs can be closed and reopened", priority: "high" },
ReqDef { id: "REQ-PR-006", category: "functional", title: "PRs support three merge strategies: merge commit, squash, rebase", priority: "high" },
ReqDef { id: "REQ-PR-007", category: "functional", title: "Mergeable status is computed and displayed", priority: "high" },
ReqDef { id: "REQ-PR-008", category: "functional", title: "Reviewers can submit reviews (approved, changes_requested, commented)", priority: "high" },
ReqDef { id: "REQ-PR-009", category: "functional", title: "Reviewers can leave inline comments on specific lines", priority: "high" },
ReqDef { id: "REQ-PR-010", category: "functional", title: "Inline comments can be resolved", priority: "medium" },
ReqDef { id: "REQ-PR-011", category: "functional", title: "Inline comments survive rebases via content-addressed anchoring", priority: "high" },
ReqDef { id: "REQ-PR-012", category: "functional", title: "Merge queue supports priority ordering and batch mode", priority: "medium" },
ReqDef { id: "REQ-PR-013", category: "security", title: "Branch protection rules are enforced before merge", priority: "critical" },
ReqDef { id: "REQ-PR-014", category: "reliability", title: "Concurrent merge attempts are prevented via advisory locks", priority: "high" },
ReqDef { id: "REQ-PR-015", category: "reliability", title: "Head SHA is updated after each push to the head branch", priority: "high" },
ReqDef { id: "REQ-PR-016", category: "integration", title: "PR API supports list, get, create, update, and merge operations", priority: "medium" },
ReqDef { id: "REQ-PR-017", category: "integration", title: "Review API supports list and submit operations", priority: "medium" },
ReqDef { id: "REQ-PR-018", category: "integration", title: "PR events are emitted (opened, merged, closed, review submitted)", priority: "medium" },
ReqDef { id: "REQ-PR-019", category: "usability", title: "PR diff view shows syntax-highlighted file diffs", priority: "medium" },
ReqDef { id: "REQ-PR-020", category: "usability", title: "PR stack view shows related PRs", priority: "low" },
],
},
DomainDef {
id: "REQ-CI",
title: "Continuous Integration",
description: "Pipelines, jobs, runs, logs, artifacts, secrets, YAML parsing, workers, scheduler, health",
children: vec![
ReqDef { id: "REQ-CI-001", category: "functional", title: "Pipelines are defined via .anvil.yml in the repository", priority: "high" },
ReqDef { id: "REQ-CI-002", category: "functional", title: "Pipelines can be created, updated, enabled, and disabled", priority: "medium" },
ReqDef { id: "REQ-CI-003", category: "functional", title: "Pipeline runs are triggered automatically on push", priority: "high" },
ReqDef { id: "REQ-CI-004", category: "functional", title: "Runs progress through states: queued, running, passed, failed, cancelled", priority: "high" },
ReqDef { id: "REQ-CI-005", category: "functional", title: "Runs can be cancelled with job cleanup", priority: "medium" },
ReqDef { id: "REQ-CI-006", category: "functional", title: "Jobs are created from pipeline step definitions", priority: "high" },
ReqDef { id: "REQ-CI-007", category: "functional", title: "Job status and exit codes are tracked", priority: "high" },
ReqDef { id: "REQ-CI-008", category: "functional", title: "Build logs are streamed in real-time via PubSub", priority: "high" },
ReqDef { id: "REQ-CI-009", category: "functional", title: "Build artifacts can be stored, retrieved, and deleted", priority: "medium" },
ReqDef { id: "REQ-CI-010", category: "functional", title: "Artifact storage is tracked against per-repo size limits", priority: "medium" },
ReqDef { id: "REQ-CI-011", category: "functional", title: "Test results are parsed from JUnit XML format", priority: "medium" },
ReqDef { id: "REQ-CI-012", category: "functional", title: "CI parses JUnit XML requirement annotations and forwards them for linking", priority: "medium" },
ReqDef { id: "REQ-CI-013", category: "security", title: "CI secrets are encrypted at rest with AES-256-GCM", priority: "critical" },
ReqDef { id: "REQ-CI-014", category: "security", title: "Secrets are masked in build log output", priority: "critical" },
ReqDef { id: "REQ-CI-015", category: "security", title: "Clone tokens provide temporary git HTTP access during CI runs", priority: "high" },
ReqDef { id: "REQ-CI-016", category: "reliability", title: "Workers are supervised by OTP and recover after crashes", priority: "high" },
ReqDef { id: "REQ-CI-017", category: "reliability", title: "Stateful workers persist build artifacts between runs", priority: "high" },
ReqDef { id: "REQ-CI-018", category: "reliability", title: "Health monitor tracks disk usage and evicts stale workspaces", priority: "medium" },
ReqDef { id: "REQ-CI-019", category: "reliability", title: "Log archiver compresses and retains build logs", priority: "medium" },
ReqDef { id: "REQ-CI-020", category: "performance", title: "Log streaming delivers lines within 100ms of generation", priority: "medium" },
ReqDef { id: "REQ-CI-021", category: "integration", title: "Pipeline run API supports list and get operations", priority: "medium" },
ReqDef { id: "REQ-CI-022", category: "integration", title: "CI events are emitted (triggered, started, completed, job started/completed)", priority: "medium" },
ReqDef { id: "REQ-CI-023", category: "integration", title: "Pipeline hooks execute on CI start and complete", priority: "medium" },
ReqDef { id: "REQ-CI-024", category: "usability", title: "CI dashboard shows live build output with streaming", priority: "high" },
ReqDef { id: "REQ-CI-025", category: "usability", title: "Pipeline settings include YAML editor", priority: "low" },
],
},
DomainDef {
id: "REQ-ISS",
title: "Issues & Tracking",
description: "Issues, labels, milestones, comments, assignees, kanban board",
children: vec![
ReqDef { id: "REQ-ISS-001", category: "functional", title: "Users can create issues with auto-incrementing numbers", priority: "high" },
ReqDef { id: "REQ-ISS-002", category: "functional", title: "Issues have title, body, and state (open/closed)", priority: "high" },
ReqDef { id: "REQ-ISS-003", category: "functional", title: "Issues can be listed with state filtering", priority: "medium" },
ReqDef { id: "REQ-ISS-004", category: "functional", title: "Issue title and body can be updated", priority: "medium" },
ReqDef { id: "REQ-ISS-005", category: "functional", title: "Issues can be closed and reopened", priority: "high" },
ReqDef { id: "REQ-ISS-006", category: "functional", title: "Users can add, update, and delete comments on issues", priority: "medium" },
ReqDef { id: "REQ-ISS-007", category: "functional", title: "Labels can be created, updated, deleted, and assigned to issues", priority: "medium" },
ReqDef { id: "REQ-ISS-008", category: "functional", title: "Issues can have assignees (add, remove)", priority: "medium" },
ReqDef { id: "REQ-ISS-009", category: "functional", title: "Milestones can be created and managed (open/closed)", priority: "medium" },
ReqDef { id: "REQ-ISS-010", category: "functional", title: "Milestones track progress as percentage complete", priority: "medium" },
ReqDef { id: "REQ-ISS-011", category: "functional", title: "Issues can be assigned to milestones", priority: "medium" },
ReqDef { id: "REQ-ISS-012", category: "functional", title: "Kanban board with configurable columns", priority: "medium" },
ReqDef { id: "REQ-ISS-013", category: "functional", title: "Default board columns initialize automatically (Backlog, Todo, In Progress, Review, Done)", priority: "medium" },
ReqDef { id: "REQ-ISS-014", category: "functional", title: "Issues can be moved between board columns with drag-drop", priority: "medium" },
ReqDef { id: "REQ-ISS-015", category: "functional", title: "Moving an issue to a closed column auto-closes it", priority: "medium" },
ReqDef { id: "REQ-ISS-016", category: "integration", title: "Issue API supports list, get, create, and update operations", priority: "medium" },
ReqDef { id: "REQ-ISS-017", category: "integration", title: "Issue comment API supports list, create, and delete", priority: "medium" },
ReqDef { id: "REQ-ISS-018", category: "integration", title: "Label API supports list and create", priority: "medium" },
ReqDef { id: "REQ-ISS-019", category: "integration", title: "Milestone API supports list and create", priority: "medium" },
ReqDef { id: "REQ-ISS-020", category: "integration", title: "Assignee API supports add and remove", priority: "medium" },
ReqDef { id: "REQ-ISS-021", category: "integration", title: "Issue events are emitted (opened, closed)", priority: "medium" },
ReqDef { id: "REQ-ISS-022", category: "usability", title: "Kanban board supports drag-and-drop via SortableJS", priority: "low" },
],
},
DomainDef {
id: "REQ-NOTIF",
title: "Notifications",
description: "In-app notifications, preferences, real-time delivery, PubSub",
children: vec![
ReqDef { id: "REQ-NOTIF-001", category: "functional", title: "Notifications are created for relevant events", priority: "medium" },
ReqDef { id: "REQ-NOTIF-002", category: "functional", title: "Users can list notifications (filtered by read status)", priority: "medium" },
ReqDef { id: "REQ-NOTIF-003", category: "functional", title: "Users can mark notifications as read (single or all)", priority: "medium" },
ReqDef { id: "REQ-NOTIF-004", category: "functional", title: "Unread notification count is available", priority: "medium" },
ReqDef { id: "REQ-NOTIF-005", category: "functional", title: "Users can configure notification preferences per event type", priority: "medium" },
ReqDef { id: "REQ-NOTIF-006", category: "functional", title: "Notifications respect user preferences (suppressed if disabled)", priority: "medium" },
ReqDef { id: "REQ-NOTIF-007", category: "usability", title: "Notifications are delivered in real-time via PubSub/LiveView", priority: "medium" },
],
},
DomainDef {
id: "REQ-FLAG",
title: "Feature Flags",
description: "Flag registry, states, percentage rollouts, scope overrides, ETS cache",
children: vec![
ReqDef { id: "REQ-FLAG-001", category: "functional", title: "Flags are registered with defaults in source code", priority: "medium" },
ReqDef { id: "REQ-FLAG-002", category: "functional", title: "Flag states: on, off, staff, beta", priority: "medium" },
ReqDef { id: "REQ-FLAG-003", category: "functional", title: "Admins can override flags via database", priority: "medium" },
ReqDef { id: "REQ-FLAG-004", category: "functional", title: "Flags support percentage-based rollouts", priority: "medium" },
ReqDef { id: "REQ-FLAG-005", category: "functional", title: "Flags support scope-based overrides (user, org, repo)", priority: "medium" },
ReqDef { id: "REQ-FLAG-006", category: "performance", title: "Flag lookups use ETS cache, not database queries", priority: "high" },
ReqDef { id: "REQ-FLAG-007", category: "reliability", title: "Cache refreshes on flag changes via PubSub", priority: "high" },
ReqDef { id: "REQ-FLAG-008", category: "security", title: "Staff-only flags require admin status", priority: "high" },
ReqDef { id: "REQ-FLAG-009", category: "usability", title: "Admin UI allows flag configuration", priority: "low" },
],
},
DomainDef {
id: "REQ-EVT",
title: "Event Bus",
description: "Event emission, PubSub broadcast, DB persistence, filtering",
children: vec![
ReqDef { id: "REQ-EVT-001", category: "functional", title: "Events are emitted for all significant actions", priority: "high" },
ReqDef { id: "REQ-EVT-002", category: "functional", title: "Events are persisted to the database", priority: "high" },
ReqDef { id: "REQ-EVT-003", category: "functional", title: "Events are broadcast via PubSub (global, org, repo scoped)", priority: "high" },
ReqDef { id: "REQ-EVT-004", category: "functional", title: "Events can be listed with filtering (type, org, repo, since)", priority: "medium" },
ReqDef { id: "REQ-EVT-005", category: "functional", title: "Events track the actor who triggered them", priority: "medium" },
ReqDef { id: "REQ-EVT-006", category: "reliability", title: "Event payloads are JSON-serialized for durability", priority: "medium" },
ReqDef { id: "REQ-EVT-007", category: "integration", title: "Events implement a common Event behaviour", priority: "medium" },
],
},
DomainDef {
id: "REQ-PLUG",
title: "Plugins",
description: "Plugin lifecycle, hook points, config validation, encrypted config, secret grants",
children: vec![
ReqDef { id: "REQ-PLUG-001", category: "functional", title: "Plugins can be created with metadata (name, slug, version)", priority: "medium" },
ReqDef { id: "REQ-PLUG-002", category: "functional", title: "Plugins can be enabled and disabled", priority: "medium" },
ReqDef { id: "REQ-PLUG-003", category: "functional", title: "Plugins register for hook points (pre_push, post_push, pre_merge, post_merge, on_ci_start, on_ci_complete, secrets_resolution)", priority: "high" },
ReqDef { id: "REQ-PLUG-004", category: "functional", title: "Hooks execute at registered points and collect results", priority: "high" },
ReqDef { id: "REQ-PLUG-005", category: "functional", title: "Pre-push hook failures block the push", priority: "high" },
ReqDef { id: "REQ-PLUG-006", category: "functional", title: "Plugin execution history is recorded", priority: "medium" },
ReqDef { id: "REQ-PLUG-007", category: "security", title: "Plugin config is encrypted at rest (AES-256-GCM)", priority: "critical" },
ReqDef { id: "REQ-PLUG-008", category: "security", title: "Secret grants control which env vars plugins can access", priority: "critical" },
ReqDef { id: "REQ-PLUG-009", category: "reliability", title: "Plugin crashes are caught and logged without affecting the host", priority: "high" },
ReqDef { id: "REQ-PLUG-010", category: "integration", title: "Plugins are filterable by org, enabled status, hook point, and capability", priority: "medium" },
],
},
DomainDef {
id: "REQ-MKT",
title: "Marketplace",
description: "Listings, publishing, search, categories, stars, downloads, installations",
children: vec![
ReqDef { id: "REQ-MKT-001", category: "functional", title: "Users can create marketplace listings", priority: "medium" },
ReqDef { id: "REQ-MKT-002", category: "functional", title: "Listings can be published and unpublished", priority: "medium" },
ReqDef { id: "REQ-MKT-003", category: "functional", title: "Listings are searchable by name/description (case-insensitive)", priority: "medium" },
ReqDef { id: "REQ-MKT-004", category: "functional", title: "Listings are filterable by category (reviewer, fixer, triage, security, documentation, custom)", priority: "medium" },
ReqDef { id: "REQ-MKT-005", category: "functional", title: "Listings are sortable by downloads, stars, or newest", priority: "low" },
ReqDef { id: "REQ-MKT-006", category: "functional", title: "Users can star listings", priority: "low" },
ReqDef { id: "REQ-MKT-007", category: "functional", title: "Download counts are tracked", priority: "low" },
ReqDef { id: "REQ-MKT-008", category: "functional", title: "Listings can be installed to repositories", priority: "medium" },
ReqDef { id: "REQ-MKT-009", category: "functional", title: "Installations can be listed per repository", priority: "medium" },
ReqDef { id: "REQ-MKT-010", category: "functional", title: "Listings can be uninstalled from repositories", priority: "medium" },
ReqDef { id: "REQ-MKT-011", category: "reliability", title: "Star count cannot go below zero", priority: "low" },
],
},
DomainDef {
id: "REQ-METRICS",
title: "Analytics & DORA",
description: "Deployment frequency, lead time, MTTR, change failure rate, classification",
children: vec![
ReqDef { id: "REQ-METRICS-001", category: "functional", title: "Deployment frequency is calculated (count and per-day rate)", priority: "medium" },
ReqDef { id: "REQ-METRICS-002", category: "functional", title: "Lead time for changes is calculated (median/mean hours)", priority: "medium" },
ReqDef { id: "REQ-METRICS-003", category: "functional", title: "Mean time to recovery (MTTR) is calculated", priority: "medium" },
ReqDef { id: "REQ-METRICS-004", category: "functional", title: "Change failure rate is calculated as a percentage", priority: "medium" },
ReqDef { id: "REQ-METRICS-005", category: "functional", title: "DORA summary classifies each metric (Elite, High, Medium, Low)", priority: "medium" },
ReqDef { id: "REQ-METRICS-006", category: "functional", title: "Metrics snapshots are saved with period tracking", priority: "low" },
ReqDef { id: "REQ-METRICS-007", category: "usability", title: "DORA metrics dashboard visualizes all four metrics", priority: "low" },
],
},
DomainDef {
id: "REQ-REQ",
title: "Requirements Traceability",
description: "Requirements CRUD, test linking, traceability matrix, coverage status",
children: vec![
ReqDef { id: "REQ-REQ-001", category: "functional", title: "Requirements can be created with ID, title, description, category, priority, status", priority: "high" },
ReqDef { id: "REQ-REQ-002", category: "functional", title: "Requirements support hierarchical parent-child relationships", priority: "high" },
ReqDef { id: "REQ-REQ-003", category: "functional", title: "Requirements can be listed with status and category filtering", priority: "medium" },
ReqDef { id: "REQ-REQ-004", category: "functional", title: "Tests can be linked and unlinked from requirements", priority: "high" },
ReqDef { id: "REQ-REQ-005", category: "functional", title: "Traceability matrix shows requirements mapped to test results", priority: "high" },
ReqDef { id: "REQ-REQ-006", category: "functional", title: "Coverage status is computed (covered, partial, uncovered, no_tests)", priority: "high" },
ReqDef { id: "REQ-REQ-007", category: "integration", title: "Requirement links are created from JUnit XML property annotations parsed by CI", priority: "medium" },
ReqDef { id: "REQ-REQ-008", category: "integration", title: "Requirements API supports CRUD and matrix operations", priority: "medium" },
ReqDef { id: "REQ-REQ-009", category: "usability", title: "Requirements LiveView shows index, detail, and matrix views", priority: "medium" },
ReqDef { id: "REQ-REQ-010", category: "security", title: "Requirements are gated behind feature flag with repo scope", priority: "medium" },
],
},
DomainDef {
id: "REQ-SITES",
title: "Published Sites",
description: "Static content serving from repositories",
children: vec![
ReqDef { id: "REQ-SITES-001", category: "functional", title: "Static content can be served from repository branches", priority: "medium" },
ReqDef { id: "REQ-SITES-002", category: "functional", title: "Site settings are configurable per repository", priority: "medium" },
ReqDef { id: "REQ-SITES-003", category: "security", title: "Published sites respect repository visibility", priority: "high" },
],
},
DomainDef {
id: "REQ-DEPLOY",
title: "Deployments",
description: "Environments, deployment lifecycle, active deployment tracking",
children: vec![
ReqDef { id: "REQ-DEPLOY-001", category: "functional", title: "Deployment environments can be created per repository", priority: "medium" },
ReqDef { id: "REQ-DEPLOY-002", category: "functional", title: "Deployments track commit SHA, deployer, and status", priority: "high" },
ReqDef { id: "REQ-DEPLOY-003", category: "functional", title: "Deployment status progresses: started, active, inactive, failed", priority: "high" },
ReqDef { id: "REQ-DEPLOY-004", category: "functional", title: "Previous active deployment is auto-deactivated on new deployment", priority: "high" },
ReqDef { id: "REQ-DEPLOY-005", category: "functional", title: "Active deployment per environment is retrievable", priority: "medium" },
ReqDef { id: "REQ-DEPLOY-006", category: "functional", title: "Deployment status summary shows all environments with active deployments", priority: "medium" },
ReqDef { id: "REQ-DEPLOY-007", category: "security", title: "Only users with write or admin access can create deployments", priority: "critical" },
ReqDef { id: "REQ-DEPLOY-008", category: "integration", title: "Deployment events are emitted (DeploymentStarted)", priority: "medium" },
ReqDef { id: "REQ-DEPLOY-009", category: "usability", title: "Deployment history and status are viewable in the UI", priority: "low" },
],
},
DomainDef {
id: "REQ-AUTH",
title: "Authorization",
description: "Permission model, role hierarchy, action authorization, team permissions",
children: vec![
ReqDef { id: "REQ-AUTH-001", category: "security", title: "Repository actions require authorization (read, write, admin, delete)", priority: "critical" },
ReqDef { id: "REQ-AUTH-002", category: "security", title: "Organization settings require owner or admin role", priority: "critical" },
ReqDef { id: "REQ-AUTH-003", category: "security", title: "Permission hierarchy: read < write < admin < delete", priority: "critical" },
ReqDef { id: "REQ-AUTH-004", category: "security", title: "Public repositories allow unauthenticated read access", priority: "high" },
ReqDef { id: "REQ-AUTH-005", category: "security", title: "Effective permission is the highest of org membership and team grants", priority: "high" },
ReqDef { id: "REQ-AUTH-006", category: "security", title: "Organization admins cannot delete repositories (owner only)", priority: "high" },
ReqDef { id: "REQ-AUTH-007", category: "security", title: "All API endpoints verify authentication and authorization", priority: "critical" },
ReqDef { id: "REQ-AUTH-008", category: "security", title: "All LiveView pages verify authentication via on_mount", priority: "critical" },
],
},
]
}