Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .cargo/audit.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ ignore = [
"RUSTSEC-2025-0057",
# RUSTSEC-2025-0134: rustls-pemfile unmaintained - dev-only via bollard/testcontainers
# Waiting for upstream bollard fix
"RUSTSEC-2025-0134"
"RUSTSEC-2025-0134",
# RUSTSEC-2026-0066 (astral-tokio-tar PAX extraction) - dev-only via bollard/testcontainers
# Blocked by testcontainers upstream constraints
"RUSTSEC-2026-0066"
]
# Warn about unmaintained crates but don't fail
informational_warnings = ["unmaintained"]
14 changes: 7 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/http-platform/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ serde.workspace = true
serde_json.workspace = true
serde_yaml.workspace = true
tracing.workspace = true
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }

[dev-dependencies]
testing.workspace = true
Expand Down
10 changes: 7 additions & 3 deletions crates/http-platform/src/idp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,13 @@ pub async fn get_idp_snapshot<S>(State(state): State<S>) -> Result<Json<IdpSnaps
where
S: super::PlatformState,
{
let snapshot = generate_snapshot(state.workspace_root()).map_err(|e| {
HttpError::internal_error(format!("Failed to generate IDP snapshot: {}", e))
})?;
let root = state.workspace_root().to_path_buf();
let snapshot = tokio::task::spawn_blocking(move || generate_snapshot(&root))
.await
.map_err(|e| HttpError::internal_error(format!("Blocking task join error: {}", e)))?
.map_err(|e| {
HttpError::internal_error(format!("Failed to generate IDP snapshot: {}", e))
})?;

Ok(Json(snapshot))
}
Expand Down
46 changes: 31 additions & 15 deletions crates/http-platform/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,19 @@ async fn debug_info<S>(State(state): State<S>) -> Json<DebugInfo>
where
S: PlatformState,
{
let root = state.workspace_root();

let template_version = load_service_metadata(&root.join("specs/service_metadata.yaml"))
.ok()
.and_then(|m| m.template_version)
.unwrap_or_else(|| "unknown".to_string());
let root = state.workspace_root().to_path_buf();

let template_version = tokio::task::spawn_blocking(move || {
load_service_metadata(&root.join("specs/service_metadata.yaml"))
.ok()
.and_then(|m| m.template_version)
.unwrap_or_else(|| "unknown".to_string())
})
.await
.unwrap_or_else(|e| {
tracing::error!("Blocking task join error: {}", e);
"unknown".to_string()
});

Json(DebugInfo { kernel_version: env!("CARGO_PKG_VERSION").to_string(), template_version })
}
Expand Down Expand Up @@ -207,10 +214,21 @@ async fn get_status<S>(State(state): State<S>) -> Result<Json<PlatformStatusResp
where
S: PlatformState,
{
let root = state.workspace_root();
let specs = load_all_specs(root)
.map_err(|e| HttpError::internal_error(format!("Failed to load specs: {}", e)))?;
let tasks_spec = spec_runtime::load_tasks(&root.join("specs/tasks.yaml"))
let root = state.workspace_root().to_path_buf();

let root_clone = root.clone();
let (specs, tasks_spec, ac_cov) = tokio::task::spawn_blocking(move || {
let specs_result = load_all_specs(&root_clone);
let tasks_result = spec_runtime::load_tasks(&root_clone.join("specs/tasks.yaml"));
let ac_cov = idp::load_ac_coverage(&root_clone);
(specs_result, tasks_result, ac_cov)
})
Comment on lines +220 to +225
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The refactor in get_status is incomplete. While some calls were moved to spawn_blocking, several other blocking I/O operations remain on the async executor thread: load_question_counts (line 264), load_friction_counts (line 267), load_fork_counts (line 270), and the fs::read_to_string for policy status (line 274). To prevent thread starvation as intended, these synchronous operations should be consolidated into the spawn_blocking block, similar to the implementation in the dashboard handler in src/ui.rs.

.await
.map_err(|e| HttpError::internal_error(format!("Blocking task join error: {}", e)))?;

let specs =
specs.map_err(|e| HttpError::internal_error(format!("Failed to load specs: {}", e)))?;
let tasks_spec = tasks_spec
.map_err(|e| HttpError::internal_error(format!("Failed to load tasks: {}", e)))?;

let ledger_counts = LedgerCounts::new(
Expand Down Expand Up @@ -239,19 +257,17 @@ where
let task_breakdown = calculate_task_breakdown(&tasks_spec);
let task_counts = TaskCounts::new(tasks_spec.tasks.len(), Some(task_breakdown));

// Load AC coverage from idp module
let ac_cov = idp::load_ac_coverage(root);
let ac_coverage =
Some(AcCoverageInfo::new(ac_cov.total, ac_cov.passing, ac_cov.failing, ac_cov.unknown));

// Load question counts
let question_counts = load_question_counts(root);
let question_counts = load_question_counts(&root);

// Load friction counts
let friction_counts = load_friction_counts(root);
let friction_counts = load_friction_counts(&root);

// Load fork counts
let fork_counts = load_fork_counts(root);
let fork_counts = load_fork_counts(&root);

// Read policy status from last policy-test run
let policy_path = root.join("target/policy_status.json");
Expand Down
124 changes: 97 additions & 27 deletions crates/http-platform/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,51 @@ pub async fn dashboard<S>(State(state): State<S>) -> Html<String>
where
S: super::PlatformState,
{
let root = state.workspace_root();
let status_result = load_all_specs(root);
let tasks_result = spec_runtime::load_tasks(&root.join("specs/tasks.yaml"));
let metadata = load_service_metadata(&root.join("specs/service_metadata.yaml")).ok();
let root = state.workspace_root().to_path_buf();
let config = super::config_summary(&state);

let (status_result, tasks_result, metadata, policy_status, feature_status_content) =
tokio::task::spawn_blocking(move || {
let status_result = load_all_specs(&root);
let tasks_result = spec_runtime::load_tasks(&root.join("specs/tasks.yaml"));
let metadata = load_service_metadata(&root.join("specs/service_metadata.yaml")).ok();

// Read policy status
let policy_path = root.join("target/policy_status.json");
let policy_status = std::fs::read_to_string(policy_path)
.ok()
.and_then(|content| serde_json::from_str::<serde_json::Value>(&content).ok())
.and_then(|v| v.get("summary").and_then(|s| s.as_str()).map(String::from))
.unwrap_or_else(|| "unknown".to_string());

// Read AC coverage from feature_status.md
let feature_status_path = root.join("docs/feature_status.md");
let feature_status_content = if feature_status_path.exists() {
std::fs::read_to_string(&feature_status_path).ok()
} else {
None
};

(status_result, tasks_result, metadata, policy_status, feature_status_content)
})
.await
.unwrap_or_else(|e| {
tracing::error!("Blocking task join error: {}", e);
(
Err(spec_runtime::SpecError::io(
std::path::PathBuf::new(),
std::io::Error::other("Join error"),
)),
Err(spec_runtime::SpecError::io(
std::path::PathBuf::new(),
std::io::Error::other("Join error"),
)),
None,
"unknown".to_string(),
None,
)
});

let content = match (status_result, tasks_result) {
(Ok(specs), Ok(tasks_spec)) => {
let _req_count: usize = specs.ledger.stories.iter().map(|s| s.requirements.len()).sum();
Expand All @@ -38,30 +77,18 @@ where
.map(|r| r.acceptance_criteria.len())
.sum();

// Read policy status
let policy_path = root.join("target/policy_status.json");
let policy_status = std::fs::read_to_string(policy_path)
.ok()
.and_then(|content| serde_json::from_str::<serde_json::Value>(&content).ok())
.and_then(|v| v.get("summary").and_then(|s| s.as_str()).map(String::from))
.unwrap_or_else(|| "unknown".to_string());

let status_class = match policy_status.as_str() {
"pass" => "status-pass",
"fail" => "status-fail",
_ => "status-unknown",
};

// Read AC coverage from feature_status.md
let feature_status_path = root.join("docs/feature_status.md");
let mut passing = 0;
let mut failing = 0;
let mut unknown = 0;
let mut coverage_rows = 0;

if feature_status_path.exists()
&& let Ok(content) = std::fs::read_to_string(feature_status_path)
{
if let Some(content) = feature_status_content {
for line in content.lines() {
if !line.starts_with("| AC-") {
continue;
Expand Down Expand Up @@ -119,10 +146,26 @@ pub async fn graph_view<S>(State(state): State<S>) -> Html<String>
where
S: super::PlatformState,
{
let root = state.workspace_root();
let metadata = load_service_metadata(&root.join("specs/service_metadata.yaml")).ok();
let root = state.workspace_root().to_path_buf();

let (metadata, specs_result) = tokio::task::spawn_blocking(move || {
let metadata = load_service_metadata(&root.join("specs/service_metadata.yaml")).ok();
let specs_result = load_all_specs(&root);
(metadata, specs_result)
})
.await
.unwrap_or_else(|e| {
tracing::error!("Blocking task join error: {}", e);
(
None,
Err(spec_runtime::SpecError::io(
std::path::PathBuf::new(),
std::io::Error::other("Join error"),
)),
)
});

let content = match load_all_specs(root) {
let content = match specs_result {
Ok(specs) => match spec_runtime::build_graph(&specs.ledger, &specs.devex, &specs.docs) {
Ok(graph) => {
let mermaid_diagram = graph.to_mermaid();
Expand Down Expand Up @@ -168,11 +211,29 @@ pub async fn flows_view<S>(State(state): State<S>) -> Html<String>
where
S: super::PlatformState,
{
let root = state.workspace_root();
let metadata = load_service_metadata(&root.join("specs/service_metadata.yaml")).ok();

let flows_result = spec_runtime::load_devex_flows(&root.join("specs/devex_flows.yaml"));
let tasks_result = spec_runtime::load_tasks(&root.join("specs/tasks.yaml"));
let root = state.workspace_root().to_path_buf();

let (metadata, flows_result, tasks_result) = tokio::task::spawn_blocking(move || {
let metadata = load_service_metadata(&root.join("specs/service_metadata.yaml")).ok();
let flows_result = spec_runtime::load_devex_flows(&root.join("specs/devex_flows.yaml"));
let tasks_result = spec_runtime::load_tasks(&root.join("specs/tasks.yaml"));
(metadata, flows_result, tasks_result)
})
.await
.unwrap_or_else(|e| {
tracing::error!("Blocking task join error: {}", e);
(
None,
Err(spec_runtime::SpecError::io(
std::path::PathBuf::new(),
std::io::Error::other("Join error"),
)),
Err(spec_runtime::SpecError::io(
std::path::PathBuf::new(),
std::io::Error::other("Join error"),
)),
)
});

let content = match (flows_result, tasks_result) {
(Ok(devex), Ok(tasks_spec)) => {
Expand Down Expand Up @@ -244,8 +305,17 @@ pub async fn coverage_view<S>(State(state): State<S>) -> Html<String>
where
S: super::PlatformState,
{
let metadata =
load_service_metadata(&state.workspace_root().join("specs/service_metadata.yaml")).ok();
let root = state.workspace_root().to_path_buf();

let metadata = tokio::task::spawn_blocking(move || {
load_service_metadata(&root.join("specs/service_metadata.yaml")).ok()
})
.await
.unwrap_or_else(|e| {
tracing::error!("Blocking task join error: {}", e);
None
});

let content = coverage_content();

Html(layout("AC Coverage", "coverage", &metadata, content).into_string())
Expand Down
7 changes: 6 additions & 1 deletion deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ ignore = [
# - Next review: 2026-03-18 (quarterly)
# - Action: Monitor upstream bollard crate for fix
# - Tracking: https://github.com/testcontainers/testcontainers-rs/issues
"RUSTSEC-2025-0134"
"RUSTSEC-2025-0134",
# RUSTSEC-2026-0066 (astral-tokio-tar PAX extraction)
# - Path: astral-tokio-tar → bollard → testcontainers (dev-dependencies only)
# - Risk: Parser differential, requires secondary vulnerability
# - Action: Blocked by testcontainers pinning astral-tokio-tar
"RUSTSEC-2026-0066"
]
yanked = "deny"

Expand Down
Loading