diff --git a/.aphoria/claims.toml b/.aphoria/claims.toml new file mode 100644 index 0000000..6dbecc6 --- /dev/null +++ b/.aphoria/claims.toml @@ -0,0 +1,168 @@ +# Aphoria Claims - version controlled +# +# Human-authored claims with provenance, invariants, and consequences. +# Each claim represents a deliberate architectural decision or safety invariant. +# +# Manage with: aphoria claims create|list|explain|update|supersede|deprecate + +[[claim]] +id = "aphoria-no-unwrap-001" +concept_path = "aphoria/production/error_handling" +predicate = "unwrap_count" +value = 0 +comparison = "equals" +provenance = "CI clippy::unwrap_used lint at deny level" +invariant = "Production code MUST NOT use unwrap() or expect()" +consequence = "Runtime panics in production" +authority_tier = "expert" +evidence = ["CLAUDE.md critical rules", "Cargo.toml clippy config"] +category = "safety" +status = "active" +created_by = "jml" +created_at = "2026-02-08T12:00:00Z" + +[[claim]] +id = "aphoria-bridge-tier-001" +concept_path = "aphoria/bridge/tier_assignment" +predicate = "default_tier" +value = "SourceClass::Community" +comparison = "present" +provenance = "Bridge module design: observations default to Community tier" +invariant = "Observation-to-assertion bridge MUST assign Community tier by default" +consequence = "Incorrect authority ranking in conflict detection" +authority_tier = "expert" +evidence = ["bridge.rs observation_to_assertion function"] +category = "architecture" +status = "active" +created_by = "jml" +created_at = "2026-02-08T12:00:00Z" + +[[claim]] +id = "aphoria-lifecycle-skip-001" +concept_path = "aphoria/bridge/lifecycle" +predicate = "skips_pending" +value = true +comparison = "present" +provenance = "Bridge design: observations skip Pending and go directly to Approved" +invariant = "Observations bypass Pending lifecycle stage" +consequence = "Observations would be invisible to queries if stuck in Pending" +authority_tier = "expert" +evidence = ["bridge.rs observation_to_assertion"] +category = "architecture" +status = "active" +created_by = "jml" +created_at = "2026-02-08T12:00:00Z" + +# --- Dogfood claims for flywheel testing --- + +[[claim]] +id = "aphoria-tls-verify-001" +concept_path = "aphoria/tls/cert_verification" +predicate = "enabled" +value = false +comparison = "absent" +provenance = "RFC 5246 Section 7.4.2 - TLS certificate verification is mandatory" +invariant = "TLS certificate verification MUST NOT be disabled in production code" +consequence = "MITM attacks become trivial; all encrypted traffic can be intercepted" +authority_tier = "regulatory" +evidence = ["RFC 5246", "OWASP TLS Cheat Sheet"] +category = "security" +status = "active" +created_by = "jml" +created_at = "2026-02-08T14:00:00Z" + +[[claim]] +id = "aphoria-no-tokio-core-001" +concept_path = "stemedb_core/imports/tokio" +predicate = "imported" +value = true +comparison = "absent" +provenance = "Architecture decision: stemedb-core must remain runtime-agnostic" +invariant = "stemedb-core MUST NOT import tokio to prevent runtime coupling" +consequence = "Core becomes tied to a specific async runtime, preventing embedding in non-tokio contexts" +authority_tier = "expert" +evidence = ["CLAUDE.md architecture overview", "stemedb-core Cargo.toml"] +category = "architecture" +status = "active" +created_by = "jml" +created_at = "2026-02-08T14:00:00Z" + +[[claim]] +id = "aphoria-no-md5-001" +concept_path = "aphoria/crypto/hashing/algorithm" +predicate = "algorithm" +value = "md5" +comparison = "not_equals" +provenance = "NIST SP 800-131A Rev 2 - MD5 is not approved for any cryptographic use" +invariant = "MD5 MUST NOT be used for hashing in any security context" +consequence = "Collision attacks are practical; signatures and integrity checks become meaningless" +authority_tier = "regulatory" +evidence = ["NIST SP 800-131A", "RFC 6151"] +category = "security" +status = "active" +created_by = "jml" +created_at = "2026-02-08T14:00:00Z" + +[[claim]] +id = "aphoria-no-wildcard-cors-001" +concept_path = "aphoria/cors/allow_origin" +predicate = "config_value" +value = "*" +comparison = "absent" +provenance = "OWASP CORS Misconfiguration - Wildcard origin with credentials is a vulnerability" +invariant = "CORS MUST NOT use wildcard (*) origin in production services" +consequence = "Any origin can make credentialed cross-origin requests, bypassing same-origin policy" +authority_tier = "expert" +evidence = ["OWASP Testing Guide v4 - CORS", "CWE-942"] +category = "security" +status = "active" +created_by = "jml" +created_at = "2026-02-08T14:00:00Z" + +[[claim]] +id = "aphoria-jwt-audience-001" +concept_path = "aphoria/jwt/audience_validation" +predicate = "enabled" +value = false +comparison = "absent" +provenance = "RFC 7519 Section 4.1.3 - The aud claim MUST be validated" +invariant = "JWT audience validation MUST NOT be disabled" +consequence = "Tokens issued for one service can be replayed against another" +authority_tier = "regulatory" +evidence = ["RFC 7519 Section 4.1.3"] +category = "security" +status = "active" +created_by = "jml" +created_at = "2026-02-08T14:00:00Z" + +[[claim]] +id = "aphoria-hsts-enabled-001" +concept_path = "aphoria/security_headers/hsts" +predicate = "header_status" +value = "disabled" +comparison = "absent" +provenance = "RFC 6797 - HTTP Strict Transport Security must be enabled for HTTPS services" +invariant = "HSTS header MUST NOT be disabled on HTTPS-serving endpoints" +consequence = "Users can be downgraded to HTTP via SSL stripping attacks" +authority_tier = "regulatory" +evidence = ["RFC 6797", "OWASP Secure Headers Project"] +category = "security" +status = "active" +created_by = "jml" +created_at = "2026-02-08T14:00:00Z" + +[[claim]] +id = "aphoria-no-hardcoded-secrets-001" +concept_path = "aphoria/secrets/api_key" +predicate = "storage_method" +value = "hardcoded" +comparison = "absent" +provenance = "OWASP Top 10 2021 - A07 Identification and Authentication Failures" +invariant = "API keys MUST NOT be hardcoded in source files" +consequence = "Secrets leak through version control; credential rotation requires code changes" +authority_tier = "expert" +evidence = ["OWASP Top 10 A07:2021", "CWE-798"] +category = "security" +status = "active" +created_by = "jml" +created_at = "2026-02-08T14:00:00Z" diff --git a/.claude/skills/aphoria-claims/SKILL.md b/.claude/skills/aphoria-claims/SKILL.md index f5409f8..7198096 100644 --- a/.claude/skills/aphoria-claims/SKILL.md +++ b/.claude/skills/aphoria-claims/SKILL.md @@ -18,6 +18,19 @@ You are an expert at identifying **architectural decisions, safety invariants, a Observations describe what IS. Claims describe what MUST BE and WHY. +## Primary Workflow: Day-to-Day Claim Authoring (The Actual Use Case) + +This is the real workflow — commit-time claim authoring driven by the skill calling CLI tools: + +1. **Look at the entire diff** — Get the full context of what changed +2. **Identify claimable patterns** — Find things worth encoding as claims (spec constants, ordering, boundaries, derives on wire types) +3. **Look up existing claims** — Call `aphoria claims list` to check what already exists +4. **Align if needed** — If the diff changes something covered by a claim, use `aphoria claims update` or `supersede` +5. **Craft and submit new claims** — For new claimable patterns, draft claim with provenance/invariant/consequence, then call `aphoria claims create` +6. **Create extractors if needed** — For audit coverage, optionally create a paired extractor + +**You drive the CLI.** You call `aphoria claims list|create|update|supersede` commands. The CLI doesn't know about you. You orchestrate the loop. + ## Workflow: Reviewing a Diff for Claims ### Step 1: Get the Diff diff --git a/.gitignore b/.gitignore index 923f5ea..d3e48f0 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,8 @@ credentials.json service-account*.json # Aphoria project data (contains keys) -.aphoria/ +.aphoria/* +!.aphoria/claims.toml # Python virtual environments .venv/ diff --git a/CLAUDE.md b/CLAUDE.md index 7c3daf6..6201eca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,6 +80,55 @@ Two files, strict separation: 3. Update status tables in both files 4. Update "Current Focus" in `roadmap.md` header +## Aphoria: What Is a Claim? + +A **claim** is a human-authored statement about what code MUST do and WHY, with provenance and consequences. + +### Claims vs Observations + +| Type | What it is | Who creates it | Example | +|------|-----------|----------------|---------| +| **Observation** | Grep result: "this code does X" | Extractors (automated) | `imports/tokio: true` | +| **Claim** | Rule: "code MUST do X because Y, or Z breaks" | Humans (via skill) | "Core MUST NOT import tokio because it creates runtime coupling. If tokio appears in core imports, the library becomes async-only and breaks sync users." | + +**Observations are garbage.** They're indexed facts with no meaning. Nobody cares that `imports/format: true` — that's just grep output. + +**Claims are the product.** They encode architectural decisions, safety invariants, and spec compliance with full context: provenance (where the rule came from), invariant (what must stay true), and consequence (what breaks if violated). + +### Structure of a Claim + +```toml +[[claim]] +id = "core-no-tokio-001" +concept_path = "stemedb/core/imports/tokio" +predicate = "imported" +value = false +comparison = "absent" # Code MUST NOT have this +provenance = "Architecture decision by jml 2024-12-15" +invariant = "Core modules MUST remain sync-only" +consequence = "Importing tokio makes core async-only, breaking sync library users" +authority_tier = "expert" +category = "architecture" +evidence = ["ADR-003", "design review notes"] +status = "active" +``` + +### Aphoria Workflows (Primary Use Cases) + +**Day-to-day (commit-time claim authoring):** +1. Look at the entire diff +2. Use `aphoria-claims` skill to identify "claimable" patterns (spec constants, ordering changes, boundary violations, derive changes on wire types) +3. Skill does lookups: `aphoria claims list` to check what exists +4. If alignment needed, skill uses `aphoria claims update` or `supersede` +5. Skill crafts and submits new claims via `aphoria claims create` +6. If needed for audit, create paired extractor + +**Audit (scan-time claim verification):** +1. **Direction 1**: `aphoria scan` runs extractors → observations, compares against authored claims → PASS/CONFLICT/MISSING +2. **Direction 2**: `aphoria verify run` walks all claims, verifies each one's pattern exists in code → PASS/CONFLICT/MISSING + +The skill drives the CLI. The CLI doesn't know about the skill. They connect via skill calling `aphoria claims` commands in a loop. + ## Critical Rules - **Append-Only:** NEVER mutate existing Assertions. Create new ones. diff --git a/applications/aphoria/src/bridge.rs b/applications/aphoria/src/bridge.rs index ffc5398..66c33ed 100644 --- a/applications/aphoria/src/bridge.rs +++ b/applications/aphoria/src/bridge.rs @@ -67,7 +67,10 @@ pub fn observation_to_assertion( /// /// Observations are lower-weight assertions (Tier 4, 0.3 authority weight) that /// record what the code actually does without making authoritative claims. -#[deprecated(since = "0.9.0", note = "Use observation_to_assertion() for confidence-based tier mapping")] +#[deprecated( + since = "0.9.0", + note = "Use observation_to_assertion() for confidence-based tier mapping" +)] #[instrument(skip(signing_key), fields(concept_path = %claim.concept_path, predicate = %claim.predicate))] pub fn claim_to_observation( claim: &Observation, @@ -160,8 +163,7 @@ pub fn authored_claim_to_assertion( let source_hash = compute_authored_claim_hash(&claim.id); // Compute parent hash from superseded claim ID if present - let parent_hash = - claim.supersedes.as_ref().map(|sid| compute_authored_claim_hash(sid)); + let parent_hash = claim.supersedes.as_ref().map(|sid| compute_authored_claim_hash(sid)); // Sign subject:predicate let message = format!("{}:{}", claim.concept_path, claim.predicate); @@ -475,8 +477,7 @@ mod tests { }; let key = generate_signing_key(); - let assertion = - authored_claim_to_assertion(&claim, &key, 1706832000).expect("convert"); + let assertion = authored_claim_to_assertion(&claim, &key, 1706832000).expect("convert"); assert_eq!(assertion.subject, "maxwell/wallet/atomics/ordering"); assert_eq!(assertion.predicate, "required_ordering"); @@ -520,8 +521,7 @@ mod tests { }; let key = generate_signing_key(); - let assertion = - authored_claim_to_assertion(&claim, &key, 1706832000).expect("convert"); + let assertion = authored_claim_to_assertion(&claim, &key, 1706832000).expect("convert"); assert!(assertion.parent_hash.is_some()); } diff --git a/applications/aphoria/src/claims_explain.rs b/applications/aphoria/src/claims_explain.rs index bfdf560..c700a31 100644 --- a/applications/aphoria/src/claims_explain.rs +++ b/applications/aphoria/src/claims_explain.rs @@ -3,7 +3,9 @@ //! Generates `claims-explained.md` style output, grouping claims by category //! and rendering full provenance details. -use crate::types::authored_claim::{format_authority_tier, parse_authority_tier, AuthoredClaim, ClaimStatus}; +use crate::types::authored_claim::{ + format_authority_tier, parse_authority_tier, AuthoredClaim, ClaimStatus, +}; /// Render all claims as a markdown document grouped by category. pub fn render_claims_markdown(claims: &[AuthoredClaim], project_name: &str) -> String { @@ -83,7 +85,10 @@ pub fn render_single_claim(out: &mut String, claim: &AuthoredClaim) { } /// Render a single claim as JSON wrapped in a structured envelope. -pub fn render_claim_json(claim: &AuthoredClaim, project_name: &str) -> Result { +pub fn render_claim_json( + claim: &AuthoredClaim, + project_name: &str, +) -> Result { let envelope = serde_json::json!({ "type": "claim_detail", "project": project_name, @@ -93,7 +98,10 @@ pub fn render_claim_json(claim: &AuthoredClaim, project_name: &str) -> Result Result { +pub fn render_claims_json( + claims: &[AuthoredClaim], + project_name: &str, +) -> Result { let envelope = serde_json::json!({ "type": "claims_explain", "project": project_name, diff --git a/applications/aphoria/src/claims_file.rs b/applications/aphoria/src/claims_file.rs index 620d2a4..10ab9e7 100644 --- a/applications/aphoria/src/claims_file.rs +++ b/applications/aphoria/src/claims_file.rs @@ -276,7 +276,10 @@ mod tests { }) .expect("update claim"); - assert_eq!(file.find_by_id("claim-001").map(|c| c.provenance.as_str()), Some("Updated provenance")); + assert_eq!( + file.find_by_id("claim-001").map(|c| c.provenance.as_str()), + Some("Updated provenance") + ); } #[test] @@ -297,7 +300,10 @@ mod tests { file.supersede("claim-001", new_claim).expect("supersede"); assert_eq!(file.find_by_id("claim-001").map(|c| &c.status), Some(&ClaimStatus::Superseded)); - assert_eq!(file.find_by_id("claim-002").map(|c| c.supersedes.as_deref()), Some(Some("claim-001"))); + assert_eq!( + file.find_by_id("claim-002").map(|c| c.supersedes.as_deref()), + Some(Some("claim-001")) + ); } #[test] diff --git a/applications/aphoria/src/cli/mod.rs b/applications/aphoria/src/cli/mod.rs index ae11ea4..d7b855f 100644 --- a/applications/aphoria/src/cli/mod.rs +++ b/applications/aphoria/src/cli/mod.rs @@ -35,7 +35,9 @@ use clap::{Parser, Subcommand}; #[derive(Parser)] #[command(name = "aphoria")] #[command(version, about, long_about = None)] -#[command(after_help = "Examples:\n aphoria scan Scan current directory\n aphoria scan --format sarif Output for IDE integration\n aphoria scan --strict Stricter conflict thresholds\n aphoria verify run Check code against claims\n aphoria coverage Show claim density per module\n aphoria explain Onboarding summary")] +#[command( + after_help = "Examples:\n aphoria scan Scan current directory\n aphoria scan --format sarif Output for IDE integration\n aphoria scan --strict Stricter conflict thresholds\n aphoria verify run Check code against claims\n aphoria coverage Show claim density per module\n aphoria explain Onboarding summary" +)] pub struct Cli { /// Path to aphoria.toml configuration file #[arg(short, long, global = true)] diff --git a/applications/aphoria/src/config/defaults.rs b/applications/aphoria/src/config/defaults.rs index 380e3dd..7f42e1f 100644 --- a/applications/aphoria/src/config/defaults.rs +++ b/applications/aphoria/src/config/defaults.rs @@ -5,8 +5,8 @@ use std::path::PathBuf; use super::types::{ AliasConfig, AutonomousConfig, CommunityConfig, CorpusConfig, DepVersionConfig, EntropyConfig, EpistemeConfig, ExtractorConfig, HostedConfig, LearningConfig, LlmConfig, OfflineFallback, - PromotionConfig, ScanConfig, SelfAuditConfig, SyncMode, ThresholdConfig, TimeoutExtractorConfig, - DEFAULT_LLM_MODEL, + PromotionConfig, ScanConfig, SelfAuditConfig, SyncMode, ThresholdConfig, + TimeoutExtractorConfig, DEFAULT_LLM_MODEL, }; impl Default for EpistemeConfig { diff --git a/applications/aphoria/src/corpus/owasp/mod.rs b/applications/aphoria/src/corpus/owasp/mod.rs index 809d754..26cc624 100644 --- a/applications/aphoria/src/corpus/owasp/mod.rs +++ b/applications/aphoria/src/corpus/owasp/mod.rs @@ -33,7 +33,7 @@ use tracing::{debug, info, instrument, warn}; use super::CorpusBuilder; use crate::config::CorpusConfig; -use crate::episteme::{create_authoritative_assertion_with_metadata}; +use crate::episteme::create_authoritative_assertion_with_metadata; use crate::AphoriaError; use parsers::parse_cheatsheet; diff --git a/applications/aphoria/src/corpus_build.rs b/applications/aphoria/src/corpus_build.rs index 5d7bca0..545e030 100644 --- a/applications/aphoria/src/corpus_build.rs +++ b/applications/aphoria/src/corpus_build.rs @@ -125,8 +125,12 @@ pub async fn export_corpus_as_pack( let assertion_count = result.assertions.len(); // Include predicate aliases from config - let predicate_aliases: Vec = - config.predicate_aliases.to_alias_sets().iter().map(crate::policy::PackPredicateAliasSet::from).collect(); + let predicate_aliases: Vec = config + .predicate_aliases + .to_alias_sets() + .iter() + .map(crate::policy::PackPredicateAliasSet::from) + .collect(); // Package as Trust Pack let pack = TrustPack::new_with_predicate_aliases( diff --git a/applications/aphoria/src/coverage.rs b/applications/aphoria/src/coverage.rs index bb18c97..d3a43b5 100644 --- a/applications/aphoria/src/coverage.rs +++ b/applications/aphoria/src/coverage.rs @@ -106,10 +106,7 @@ fn derive_module_from_claim(concept_path: &str) -> String { } } else { // Fallback: strip scheme, use what we have - let path = concept_path - .find("://") - .map(|i| &concept_path[i + 3..]) - .unwrap_or(concept_path); + let path = concept_path.find("://").map(|i| &concept_path[i + 3..]).unwrap_or(concept_path); path.to_string() } } @@ -225,23 +222,17 @@ pub fn compute_coverage_from_report( // Count missing claims in this module let missing_in_module = claim_list - .map(|cls| { - cls.iter() - .filter(|c| missing_claim_ids.contains(&c.id)) - .count() - }) + .map(|cls| cls.iter().filter(|c| missing_claim_ids.contains(&c.id)).count()) .unwrap_or(0); - let density = if observation_count > 0 { - claim_count as f32 / observation_count as f32 - } else { - 0.0 - }; + let density = + if observation_count > 0 { claim_count as f32 / observation_count as f32 } else { 0.0 }; // Collect unique files in this module let files: Vec = obs_list .map(|obs| { - let mut file_set: std::collections::BTreeSet = std::collections::BTreeSet::new(); + let mut file_set: std::collections::BTreeSet = + std::collections::BTreeSet::new(); for o in obs { file_set.insert(o.file.clone()); } @@ -271,10 +262,8 @@ pub fn compute_coverage_from_report( }); } - let active_claims = claims - .iter() - .filter(|c| c.status == crate::types::ClaimStatus::Active) - .count(); + let active_claims = + claims.iter().filter(|c| c.status == crate::types::ClaimStatus::Active).count(); let claimed_percentage = if total_observations > 0 { (total_claimed as f32 / total_observations as f32) * 100.0 @@ -309,10 +298,13 @@ pub fn format_coverage_table(report: &CoverageReport, sort_by: &str) -> String { let mut modules = report.modules.clone(); match sort_by { - "unclaimed" => modules.sort_by(|a, b| b.unclaimed_observations.cmp(&a.unclaimed_observations)), + "unclaimed" => { + modules.sort_by(|a, b| b.unclaimed_observations.cmp(&a.unclaimed_observations)) + } "observations" => modules.sort_by(|a, b| b.observation_count.cmp(&a.observation_count)), "density" => modules.sort_by(|a, b| { - b.density.partial_cmp(&a.density) + b.density + .partial_cmp(&a.density) .unwrap_or(std::cmp::Ordering::Equal) .then_with(|| b.observation_count.cmp(&a.observation_count)) }), @@ -403,11 +395,8 @@ pub fn format_coverage_markdown(report: &CoverageReport) -> String { } // Highlight modules with 0 claims - let uncovered: Vec<&ModuleCoverage> = report - .modules - .iter() - .filter(|m| m.claim_count == 0 && m.observation_count > 0) - .collect(); + let uncovered: Vec<&ModuleCoverage> = + report.modules.iter().filter(|m| m.claim_count == 0 && m.observation_count > 0).collect(); if !uncovered.is_empty() { out.push_str("\n## Coverage Gaps\n\n"); @@ -474,14 +463,8 @@ mod tests { #[test] fn test_derive_module_from_claim() { - assert_eq!( - derive_module_from_claim("project/wallet/atomics/ordering"), - "atomics" - ); - assert_eq!( - derive_module_from_claim("code://rust/core/imports/tokio"), - "imports" - ); + assert_eq!(derive_module_from_claim("project/wallet/atomics/ordering"), "atomics"); + assert_eq!(derive_module_from_claim("code://rust/core/imports/tokio"), "imports"); } #[test] @@ -494,11 +477,7 @@ mod tests { #[test] fn test_compute_coverage_with_matches() { - let claims = vec![make_claim( - "c1", - "project/atomics/ordering", - "safety", - )]; + let claims = vec![make_claim("c1", "project/atomics/ordering", "safety")]; let observations = vec![ make_obs("code://rust/project/atomics/ordering", "src/wallet/atomics/sync.rs"), make_obs("code://rust/project/tls/config", "src/tls/config.rs"), @@ -513,10 +492,8 @@ mod tests { #[test] fn test_coverage_table_output() { let claims = vec![make_claim("c1", "project/atomics/ordering", "safety")]; - let observations = vec![make_obs( - "code://rust/project/atomics/ordering", - "src/wallet/atomics/sync.rs", - )]; + let observations = + vec![make_obs("code://rust/project/atomics/ordering", "src/wallet/atomics/sync.rs")]; let report = compute_coverage(&claims, &observations, "myproject"); let table = format_coverage_table(&report, "name"); assert!(table.contains("Aphoria Coverage: myproject")); @@ -527,8 +504,7 @@ mod tests { fn test_coverage_json_output() { let report = compute_coverage(&[], &[], "test"); let json = format_coverage_json(&report); - let parsed: serde_json::Value = - serde_json::from_str(&json).expect("valid json"); + let parsed: serde_json::Value = serde_json::from_str(&json).expect("valid json"); assert_eq!(parsed["project"], "test"); } @@ -551,19 +527,15 @@ mod tests { fn test_claims_map_to_observation_modules() { // Claim concept_path and observation concept_path share tail "atomics/ordering" let claims = vec![make_claim("c1", "project/atomics/ordering", "safety")]; - let observations = vec![ - make_obs("code://rust/project/atomics/ordering", "src/wallet/atomics/sync.rs"), - ]; + let observations = + vec![make_obs("code://rust/project/atomics/ordering", "src/wallet/atomics/sync.rs")]; let report = compute_coverage(&claims, &observations, "test"); // The claim should land in "wallet/atomics" (from observation file path), // NOT "atomics" (from concept_path tail). This means the module should // have both a claim and an observation with non-zero density. - let wallet_mod = report - .modules - .iter() - .find(|m| m.module_path == "wallet/atomics"); + let wallet_mod = report.modules.iter().find(|m| m.module_path == "wallet/atomics"); assert!(wallet_mod.is_some(), "Expected wallet/atomics module"); let Some(wallet_mod) = wallet_mod else { panic!("wallet/atomics module not found"); diff --git a/applications/aphoria/src/episteme/authority_lens.rs b/applications/aphoria/src/episteme/authority_lens.rs index 4a06029..1e41b34 100644 --- a/applications/aphoria/src/episteme/authority_lens.rs +++ b/applications/aphoria/src/episteme/authority_lens.rs @@ -138,10 +138,8 @@ mod tests { #[test] fn test_single_candidate() { let lens = AphoriaAuthorityLens; - let assertion = AssertionBuilder::new() - .source_class(SourceClass::Regulatory) - .confidence(0.95) - .build(); + let assertion = + AssertionBuilder::new().source_class(SourceClass::Regulatory).confidence(0.95).build(); let result = lens.resolve(&[assertion]); assert!(result.winner.is_some()); assert_eq!(result.candidates_count, 1); @@ -171,14 +169,10 @@ mod tests { fn test_lens_scores_match_existing() { // Verify the normalized formula matches conflict.rs expectations // Tier 0 vs code → ~0.95 - let regulatory = AssertionBuilder::new() - .source_class(SourceClass::Regulatory) - .confidence(1.0) - .build(); - let community = AssertionBuilder::new() - .source_class(SourceClass::Community) - .confidence(1.0) - .build(); + let regulatory = + AssertionBuilder::new().source_class(SourceClass::Regulatory).confidence(1.0).build(); + let community = + AssertionBuilder::new().source_class(SourceClass::Community).confidence(1.0).build(); let lens = AphoriaAuthorityLens; let result = lens.resolve(&[regulatory, community]); @@ -194,18 +188,9 @@ mod tests { #[test] fn test_tier_breakdown() { let assertions = vec![ - AssertionBuilder::new() - .source_class(SourceClass::Regulatory) - .confidence(0.95) - .build(), - AssertionBuilder::new() - .source_class(SourceClass::Regulatory) - .confidence(0.9) - .build(), - AssertionBuilder::new() - .source_class(SourceClass::Community) - .confidence(0.7) - .build(), + AssertionBuilder::new().source_class(SourceClass::Regulatory).confidence(0.95).build(), + AssertionBuilder::new().source_class(SourceClass::Regulatory).confidence(0.9).build(), + AssertionBuilder::new().source_class(SourceClass::Community).confidence(0.7).build(), ]; let breakdown = compute_tier_breakdown(&assertions); diff --git a/applications/aphoria/src/episteme/conflict.rs b/applications/aphoria/src/episteme/conflict.rs index 605a82e..cea4395 100644 --- a/applications/aphoria/src/episteme/conflict.rs +++ b/applications/aphoria/src/episteme/conflict.rs @@ -185,8 +185,7 @@ pub fn check_conflicts_with_predicate_aliases( let mut by_tier: BTreeMap = BTreeMap::new(); for source in &conflicts { let tier = source.source_class.tier(); - let entry = - by_tier.entry(tier).or_insert((source.source_class, 0, 0.0)); + let entry = by_tier.entry(tier).or_insert((source.source_class, 0, 0.0)); entry.1 += 1; if source.confidence > entry.2 { entry.2 = source.confidence; @@ -195,13 +194,11 @@ pub fn check_conflicts_with_predicate_aliases( Some( by_tier .into_iter() - .map(|(tier, (sc, count, max_conf))| { - crate::types::TierBreakdown { - tier, - source_class: sc, - assertion_count: count, - max_confidence: max_conf, - } + .map(|(tier, (sc, count, max_conf))| crate::types::TierBreakdown { + tier, + source_class: sc, + assertion_count: count, + max_confidence: max_conf, }) .collect(), ) diff --git a/applications/aphoria/src/episteme/corpus.rs b/applications/aphoria/src/episteme/corpus.rs index 6eaa235..6328868 100644 --- a/applications/aphoria/src/episteme/corpus.rs +++ b/applications/aphoria/src/episteme/corpus.rs @@ -200,7 +200,10 @@ pub fn create_authoritative_assertion_with_metadata( }; if let serde_json::Value::Object(ref mut map) = metadata { map.insert("description".to_string(), serde_json::Value::String(description.to_string())); - map.insert("source".to_string(), serde_json::Value::String("authoritative_corpus".to_string())); + map.insert( + "source".to_string(), + serde_json::Value::String("authoritative_corpus".to_string()), + ); } Assertion { diff --git a/applications/aphoria/src/episteme/local/queries.rs b/applications/aphoria/src/episteme/local/queries.rs index 6a8c52d..b8d691e 100644 --- a/applications/aphoria/src/episteme/local/queries.rs +++ b/applications/aphoria/src/episteme/local/queries.rs @@ -8,8 +8,7 @@ use tracing::{debug, info, instrument, warn}; use crate::config::AphoriaConfig; use crate::types::{ - AcknowledgmentInfo, ConflictResult, ConflictingSource, Observation, PolicySourceInfo, - Verdict, + AcknowledgmentInfo, ConflictResult, ConflictingSource, Observation, PolicySourceInfo, Verdict, }; use crate::AphoriaError; diff --git a/applications/aphoria/src/eval/harness.rs b/applications/aphoria/src/eval/harness.rs index d1fa852..1c3b2b2 100644 --- a/applications/aphoria/src/eval/harness.rs +++ b/applications/aphoria/src/eval/harness.rs @@ -24,7 +24,7 @@ use crate::config::EvalConfig; use crate::error::Result; use crate::llm::ontology::{AuthorityConcept, OntologyVocabulary, ValueType}; use crate::llm::{GeminiClient, LlmCache, LlmExtractor}; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Configuration for an evaluation run. #[derive(Debug, Clone)] diff --git a/applications/aphoria/src/explain.rs b/applications/aphoria/src/explain.rs index 4797e70..1c86a6b 100644 --- a/applications/aphoria/src/explain.rs +++ b/applications/aphoria/src/explain.rs @@ -317,9 +317,9 @@ fn capitalize(s: &str) -> String { #[cfg(test)] mod tests { use super::*; + use crate::coverage::{CoverageSummary, ModuleCoverage}; use crate::types::authored_claim::{AuthoredValue, ComparisonMode}; use crate::verify::{VerifyResult, VerifySummary}; - use crate::coverage::{CoverageSummary, ModuleCoverage}; fn sample_claim(id: &str, category: &str) -> AuthoredClaim { AuthoredClaim { @@ -343,10 +343,7 @@ mod tests { } fn empty_verify_report() -> VerifyReport { - VerifyReport { - results: vec![], - summary: VerifySummary::default(), - } + VerifyReport { results: vec![], summary: VerifySummary::default() } } fn empty_coverage_report() -> CoverageReport { @@ -433,7 +430,13 @@ mod tests { sample_claim("a1", "architecture"), sample_claim("s2", "safety"), ]; - let out = generate_onboarding(&claims, &empty_verify_report(), &empty_coverage_report(), "myproject", "markdown"); + let out = generate_onboarding( + &claims, + &empty_verify_report(), + &empty_coverage_report(), + "myproject", + "markdown", + ); assert!(out.contains("# myproject")); assert!(out.contains("3** active claims")); assert!(out.contains("| Safety")); @@ -461,7 +464,13 @@ mod tests { #[test] fn test_onboarding_pointers() { - let out = generate_onboarding(&[], &empty_verify_report(), &empty_coverage_report(), "proj", "markdown"); + let out = generate_onboarding( + &[], + &empty_verify_report(), + &empty_coverage_report(), + "proj", + "markdown", + ); assert!(out.contains("aphoria claims explain")); assert!(out.contains("aphoria docs generate")); } @@ -469,7 +478,13 @@ mod tests { #[test] fn test_full_docs_includes_claim_details() { let claims = vec![sample_claim("c1", "safety")]; - let out = generate_full_docs(&claims, &empty_verify_report(), &empty_coverage_report(), "proj", "markdown"); + let out = generate_full_docs( + &claims, + &empty_verify_report(), + &empty_coverage_report(), + "proj", + "markdown", + ); // Should contain per-claim fields from claims_explain assert!(out.contains("**Concept:**")); assert!(out.contains("**Invariant:**")); @@ -511,7 +526,13 @@ mod tests { #[test] fn test_onboarding_json() { let claims = vec![sample_claim("c1", "safety")]; - let out = generate_onboarding(&claims, &empty_verify_report(), &empty_coverage_report(), "proj", "json"); + let out = generate_onboarding( + &claims, + &empty_verify_report(), + &empty_coverage_report(), + "proj", + "json", + ); let parsed: serde_json::Value = serde_json::from_str(&out).unwrap_or_default(); assert_eq!(parsed["type"], "onboarding"); assert_eq!(parsed["active_claims"], 1); @@ -520,7 +541,13 @@ mod tests { #[test] fn test_full_docs_json() { let claims = vec![sample_claim("c1", "safety")]; - let out = generate_full_docs(&claims, &empty_verify_report(), &empty_coverage_report(), "proj", "json"); + let out = generate_full_docs( + &claims, + &empty_verify_report(), + &empty_coverage_report(), + "proj", + "json", + ); let parsed: serde_json::Value = serde_json::from_str(&out).unwrap_or_default(); assert_eq!(parsed["type"], "full_docs"); assert!(parsed["claims"].is_array()); diff --git a/applications/aphoria/src/extractors/api_key_security.rs b/applications/aphoria/src/extractors/api_key_security.rs index 874af38..7b58da1 100644 --- a/applications/aphoria/src/extractors/api_key_security.rs +++ b/applications/aphoria/src/extractors/api_key_security.rs @@ -9,7 +9,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for API key security configuration. /// diff --git a/applications/aphoria/src/extractors/aspnet_security.rs b/applications/aphoria/src/extractors/aspnet_security.rs index d665f8a..90d91fc 100644 --- a/applications/aphoria/src/extractors/aspnet_security.rs +++ b/applications/aphoria/src/extractors/aspnet_security.rs @@ -12,7 +12,7 @@ use stemedb_core::types::ObjectValue; use super::traits::build_claim; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for ASP.NET Core security misconfigurations. pub struct AspNetSecurityExtractor { diff --git a/applications/aphoria/src/extractors/auth_bypass.rs b/applications/aphoria/src/extractors/auth_bypass.rs index 43be846..65b58b7 100644 --- a/applications/aphoria/src/extractors/auth_bypass.rs +++ b/applications/aphoria/src/extractors/auth_bypass.rs @@ -12,7 +12,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::{build_claim, Extractor}; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for authentication bypass patterns. /// diff --git a/applications/aphoria/src/extractors/circuit_breaker_config.rs b/applications/aphoria/src/extractors/circuit_breaker_config.rs index 0246182..d3c25ee 100644 --- a/applications/aphoria/src/extractors/circuit_breaker_config.rs +++ b/applications/aphoria/src/extractors/circuit_breaker_config.rs @@ -8,7 +8,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for circuit breaker configuration. /// diff --git a/applications/aphoria/src/extractors/command_injection.rs b/applications/aphoria/src/extractors/command_injection.rs index dcce046..b49587c 100644 --- a/applications/aphoria/src/extractors/command_injection.rs +++ b/applications/aphoria/src/extractors/command_injection.rs @@ -7,7 +7,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for command injection vulnerabilities. /// diff --git a/applications/aphoria/src/extractors/config_security.rs b/applications/aphoria/src/extractors/config_security.rs index b11ac49..dc11f56 100644 --- a/applications/aphoria/src/extractors/config_security.rs +++ b/applications/aphoria/src/extractors/config_security.rs @@ -29,7 +29,7 @@ use stemedb_core::types::ObjectValue; use super::config_parser::{parse_config, walk_config, ConfigValue}; use super::traits::is_test_file; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// A security rule that matches config paths and values. struct SecurityRule { diff --git a/applications/aphoria/src/extractors/const_declarations.rs b/applications/aphoria/src/extractors/const_declarations.rs index c5ce6e1..af509fe 100644 --- a/applications/aphoria/src/extractors/const_declarations.rs +++ b/applications/aphoria/src/extractors/const_declarations.rs @@ -10,7 +10,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for Rust constant declarations. /// diff --git a/applications/aphoria/src/extractors/cors_config.rs b/applications/aphoria/src/extractors/cors_config.rs index 056d372..af0d757 100644 --- a/applications/aphoria/src/extractors/cors_config.rs +++ b/applications/aphoria/src/extractors/cors_config.rs @@ -7,7 +7,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for CORS configuration issues. pub struct CorsConfigExtractor { @@ -125,10 +125,7 @@ impl Extractor for CorsConfigExtractor { } fn verifiable_predicates(&self) -> Vec<(&str, &str)> { - vec![ - ("cors/allow_origin", "config_value"), - ("cors/credentials_with_wildcard", "enabled"), - ] + vec![("cors/allow_origin", "config_value"), ("cors/credentials_with_wildcard", "enabled")] } fn screening_patterns(&self) -> Vec<&str> { diff --git a/applications/aphoria/src/extractors/declarative/executor.rs b/applications/aphoria/src/extractors/declarative/executor.rs index 932882b..b9ce6dc 100644 --- a/applications/aphoria/src/extractors/declarative/executor.rs +++ b/applications/aphoria/src/extractors/declarative/executor.rs @@ -5,7 +5,7 @@ use stemedb_core::types::ObjectValue; use super::parser::DeclarativeExtractor; use super::types::DeclarativeValue; use crate::extractors::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; impl Extractor for DeclarativeExtractor { fn name(&self) -> &str { diff --git a/applications/aphoria/src/extractors/dep_versions.rs b/applications/aphoria/src/extractors/dep_versions.rs index 36e842a..20fd82f 100644 --- a/applications/aphoria/src/extractors/dep_versions.rs +++ b/applications/aphoria/src/extractors/dep_versions.rs @@ -7,7 +7,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for vulnerable dependency versions. /// @@ -113,12 +113,7 @@ impl DepVersionsExtractor { claims } - fn extract_npm( - &self, - path_segments: &[String], - content: &str, - file: &str, - ) -> Vec { + fn extract_npm(&self, path_segments: &[String], content: &str, file: &str) -> Vec { let mut claims = Vec::new(); for (line_idx, line) in content.lines().enumerate() { @@ -166,12 +161,7 @@ impl DepVersionsExtractor { claims } - fn extract_go( - &self, - path_segments: &[String], - content: &str, - file: &str, - ) -> Vec { + fn extract_go(&self, path_segments: &[String], content: &str, file: &str) -> Vec { let mut claims = Vec::new(); let mut in_require = false; @@ -218,12 +208,7 @@ impl DepVersionsExtractor { claims } - fn extract_pip( - &self, - path_segments: &[String], - content: &str, - file: &str, - ) -> Vec { + fn extract_pip(&self, path_segments: &[String], content: &str, file: &str) -> Vec { let mut claims = Vec::new(); for (line_idx, line) in content.lines().enumerate() { diff --git a/applications/aphoria/src/extractors/derive_pattern.rs b/applications/aphoria/src/extractors/derive_pattern.rs index 80ac18e..2394550 100644 --- a/applications/aphoria/src/extractors/derive_pattern.rs +++ b/applications/aphoria/src/extractors/derive_pattern.rs @@ -8,7 +8,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for Rust derive patterns. /// diff --git a/applications/aphoria/src/extractors/django_security.rs b/applications/aphoria/src/extractors/django_security.rs index c8e3cc8..20199c4 100644 --- a/applications/aphoria/src/extractors/django_security.rs +++ b/applications/aphoria/src/extractors/django_security.rs @@ -13,7 +13,7 @@ use stemedb_core::types::ObjectValue; use super::traits::build_claim; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for Django security misconfigurations. pub struct DjangoSecurityExtractor { diff --git a/applications/aphoria/src/extractors/durability_config.rs b/applications/aphoria/src/extractors/durability_config.rs index e99fe63..6f6b32d 100644 --- a/applications/aphoria/src/extractors/durability_config.rs +++ b/applications/aphoria/src/extractors/durability_config.rs @@ -7,7 +7,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for durability configuration. /// diff --git a/applications/aphoria/src/extractors/express_security.rs b/applications/aphoria/src/extractors/express_security.rs index 1134920..562a99d 100644 --- a/applications/aphoria/src/extractors/express_security.rs +++ b/applications/aphoria/src/extractors/express_security.rs @@ -12,7 +12,7 @@ use stemedb_core::types::ObjectValue; use super::traits::build_claim; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for Express.js security misconfigurations. #[allow(dead_code)] diff --git a/applications/aphoria/src/extractors/fastapi_security.rs b/applications/aphoria/src/extractors/fastapi_security.rs index 6bdb7ef..2c4b6bc 100644 --- a/applications/aphoria/src/extractors/fastapi_security.rs +++ b/applications/aphoria/src/extractors/fastapi_security.rs @@ -11,7 +11,7 @@ use stemedb_core::types::ObjectValue; use super::traits::build_claim; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for FastAPI security misconfigurations. #[allow(dead_code)] diff --git a/applications/aphoria/src/extractors/flask_security.rs b/applications/aphoria/src/extractors/flask_security.rs index d6ad7ac..2f91d24 100644 --- a/applications/aphoria/src/extractors/flask_security.rs +++ b/applications/aphoria/src/extractors/flask_security.rs @@ -12,7 +12,7 @@ use stemedb_core::types::ObjectValue; use super::traits::build_claim; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for Flask security misconfigurations. #[allow(dead_code)] diff --git a/applications/aphoria/src/extractors/hardcoded_secrets.rs b/applications/aphoria/src/extractors/hardcoded_secrets.rs index 7e82fd8..03fead4 100644 --- a/applications/aphoria/src/extractors/hardcoded_secrets.rs +++ b/applications/aphoria/src/extractors/hardcoded_secrets.rs @@ -7,7 +7,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for hardcoded secrets in source code. pub struct HardcodedSecretsExtractor { diff --git a/applications/aphoria/src/extractors/high_entropy/mod.rs b/applications/aphoria/src/extractors/high_entropy/mod.rs index 2bb019e..8fad2e1 100644 --- a/applications/aphoria/src/extractors/high_entropy/mod.rs +++ b/applications/aphoria/src/extractors/high_entropy/mod.rs @@ -14,7 +14,7 @@ use stemedb_core::types::ObjectValue; use super::{build_claim, Extractor}; use crate::config::EntropyConfig; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; use entropy::{charset_variety, shannon_entropy}; use patterns::{classify_known_secret, is_likely_not_secret, SecretPatterns}; diff --git a/applications/aphoria/src/extractors/import_graph.rs b/applications/aphoria/src/extractors/import_graph.rs index 46d05d2..bd01f0b 100644 --- a/applications/aphoria/src/extractors/import_graph.rs +++ b/applications/aphoria/src/extractors/import_graph.rs @@ -8,7 +8,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for Rust import patterns. /// diff --git a/applications/aphoria/src/extractors/insecure_cookies/mod.rs b/applications/aphoria/src/extractors/insecure_cookies/mod.rs index b4d5cf8..e16a8a7 100644 --- a/applications/aphoria/src/extractors/insecure_cookies/mod.rs +++ b/applications/aphoria/src/extractors/insecure_cookies/mod.rs @@ -13,7 +13,7 @@ use stemedb_core::types::ObjectValue; use self::patterns::CookiePatterns; use super::{build_claim, Extractor}; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for insecure cookie configuration patterns. /// diff --git a/applications/aphoria/src/extractors/insecure_deserialization.rs b/applications/aphoria/src/extractors/insecure_deserialization.rs index 3aa045d..08c6f36 100644 --- a/applications/aphoria/src/extractors/insecure_deserialization.rs +++ b/applications/aphoria/src/extractors/insecure_deserialization.rs @@ -7,7 +7,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::traits::{build_claim, Extractor}; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for insecure deserialization vulnerabilities. /// diff --git a/applications/aphoria/src/extractors/jwt_config.rs b/applications/aphoria/src/extractors/jwt_config.rs index 60caf05..7c23eab 100644 --- a/applications/aphoria/src/extractors/jwt_config.rs +++ b/applications/aphoria/src/extractors/jwt_config.rs @@ -7,7 +7,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for JWT validation configuration. pub struct JwtConfigExtractor { diff --git a/applications/aphoria/src/extractors/laravel_security.rs b/applications/aphoria/src/extractors/laravel_security.rs index b566e00..73800cf 100644 --- a/applications/aphoria/src/extractors/laravel_security.rs +++ b/applications/aphoria/src/extractors/laravel_security.rs @@ -13,7 +13,7 @@ use stemedb_core::types::ObjectValue; use super::traits::build_claim; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for Laravel security misconfigurations. #[allow(dead_code)] diff --git a/applications/aphoria/src/extractors/nestjs_security.rs b/applications/aphoria/src/extractors/nestjs_security.rs index 0d69dd6..27b18ab 100644 --- a/applications/aphoria/src/extractors/nestjs_security.rs +++ b/applications/aphoria/src/extractors/nestjs_security.rs @@ -12,7 +12,7 @@ use stemedb_core::types::ObjectValue; use super::traits::build_claim; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for NestJS security misconfigurations. #[allow(dead_code)] diff --git a/applications/aphoria/src/extractors/nextjs_security.rs b/applications/aphoria/src/extractors/nextjs_security.rs index d93cb0c..0383bd8 100644 --- a/applications/aphoria/src/extractors/nextjs_security.rs +++ b/applications/aphoria/src/extractors/nextjs_security.rs @@ -12,7 +12,7 @@ use stemedb_core::types::ObjectValue; use super::traits::build_claim; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for Next.js security misconfigurations. #[allow(dead_code)] diff --git a/applications/aphoria/src/extractors/orm_injection.rs b/applications/aphoria/src/extractors/orm_injection.rs index df27be1..6c3f2d9 100644 --- a/applications/aphoria/src/extractors/orm_injection.rs +++ b/applications/aphoria/src/extractors/orm_injection.rs @@ -7,7 +7,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::traits::{build_claim, Extractor}; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for ORM-specific SQL injection vulnerabilities. /// diff --git a/applications/aphoria/src/extractors/path_traversal.rs b/applications/aphoria/src/extractors/path_traversal.rs index 48741a7..6bfc714 100644 --- a/applications/aphoria/src/extractors/path_traversal.rs +++ b/applications/aphoria/src/extractors/path_traversal.rs @@ -7,7 +7,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::traits::{build_claim, Extractor}; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for path traversal vulnerabilities. /// diff --git a/applications/aphoria/src/extractors/rails_security.rs b/applications/aphoria/src/extractors/rails_security.rs index f4e5e11..48134e9 100644 --- a/applications/aphoria/src/extractors/rails_security.rs +++ b/applications/aphoria/src/extractors/rails_security.rs @@ -12,7 +12,7 @@ use stemedb_core::types::ObjectValue; use super::traits::build_claim; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for Rails security misconfigurations. pub struct RailsSecurityExtractor { diff --git a/applications/aphoria/src/extractors/rate_limit.rs b/applications/aphoria/src/extractors/rate_limit.rs index 48edf9f..0bfca3b 100644 --- a/applications/aphoria/src/extractors/rate_limit.rs +++ b/applications/aphoria/src/extractors/rate_limit.rs @@ -7,7 +7,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Configuration for rate limit thresholds. #[derive(Debug, Clone)] diff --git a/applications/aphoria/src/extractors/registry.rs b/applications/aphoria/src/extractors/registry.rs index d80d7a1..5b4251a 100644 --- a/applications/aphoria/src/extractors/registry.rs +++ b/applications/aphoria/src/extractors/registry.rs @@ -6,7 +6,7 @@ use regex::RegexSet; use tracing::instrument; use crate::config::AphoriaConfig; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; use super::api_key_security::ApiKeySecurityExtractor; use super::aspnet_security::AspNetSecurityExtractor; @@ -380,10 +380,8 @@ impl ExtractorRegistry { if !patterns.is_empty() { match RegexSet::new(&patterns) { Ok(regex_set) => { - self.screening.insert(lang, ScreeningSet { - regex_set, - pattern_to_extractor, - }); + self.screening + .insert(lang, ScreeningSet { regex_set, pattern_to_extractor }); } Err(e) => { tracing::warn!( diff --git a/applications/aphoria/src/extractors/security_headers.rs b/applications/aphoria/src/extractors/security_headers.rs index 6a33270..e05b440 100644 --- a/applications/aphoria/src/extractors/security_headers.rs +++ b/applications/aphoria/src/extractors/security_headers.rs @@ -7,7 +7,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::traits::{build_claim, Extractor}; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for missing or disabled security headers. /// diff --git a/applications/aphoria/src/extractors/self_audit.rs b/applications/aphoria/src/extractors/self_audit.rs index c4ab518..f8f79fc 100644 --- a/applications/aphoria/src/extractors/self_audit.rs +++ b/applications/aphoria/src/extractors/self_audit.rs @@ -274,12 +274,7 @@ fn build_assertion() { fn test_no_bridge_obs_for_non_bridge() { let ext = SelfAuditExtractor::new(); let content = "let source_class = SourceClass::Community;\n"; - let obs = ext.extract( - &["rust".to_string()], - content, - Language::Rust, - "src/other.rs", - ); + let obs = ext.extract(&["rust".to_string()], content, Language::Rust, "src/other.rs"); assert!(!obs.iter().any(|o| o.predicate == "default_tier")); } @@ -288,12 +283,8 @@ fn build_assertion() { fn test_skips_test_files_for_unwrap() { let ext = SelfAuditExtractor::new(); let content = "let x = foo().unwrap();\n"; - let obs = ext.extract( - &["rust".to_string()], - content, - Language::Rust, - "src/tests/verify.rs", - ); + let obs = + ext.extract(&["rust".to_string()], content, Language::Rust, "src/tests/verify.rs"); // Test files should not produce unwrap_count observations let unwrap_obs: Vec<_> = obs.iter().filter(|o| o.predicate == "unwrap_count").collect(); diff --git a/applications/aphoria/src/extractors/spring_security.rs b/applications/aphoria/src/extractors/spring_security.rs index 0ab6b56..5beedb2 100644 --- a/applications/aphoria/src/extractors/spring_security.rs +++ b/applications/aphoria/src/extractors/spring_security.rs @@ -13,7 +13,7 @@ use stemedb_core::types::ObjectValue; use super::traits::build_claim; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for Spring Boot security misconfigurations. #[allow(dead_code)] diff --git a/applications/aphoria/src/extractors/sql_injection.rs b/applications/aphoria/src/extractors/sql_injection.rs index 41083d1..e2904bf 100644 --- a/applications/aphoria/src/extractors/sql_injection.rs +++ b/applications/aphoria/src/extractors/sql_injection.rs @@ -7,7 +7,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for SQL injection vulnerabilities. /// diff --git a/applications/aphoria/src/extractors/ssrf.rs b/applications/aphoria/src/extractors/ssrf.rs index ac476d1..5dc62ad 100644 --- a/applications/aphoria/src/extractors/ssrf.rs +++ b/applications/aphoria/src/extractors/ssrf.rs @@ -7,7 +7,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::traits::{build_claim, Extractor}; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for SSRF vulnerabilities. /// diff --git a/applications/aphoria/src/extractors/timeout_config.rs b/applications/aphoria/src/extractors/timeout_config.rs index febee78..74f0130 100644 --- a/applications/aphoria/src/extractors/timeout_config.rs +++ b/applications/aphoria/src/extractors/timeout_config.rs @@ -7,7 +7,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Configuration for timeout extraction thresholds. #[derive(Debug, Clone)] diff --git a/applications/aphoria/src/extractors/tls_verify.rs b/applications/aphoria/src/extractors/tls_verify.rs index b47a538..edaee23 100644 --- a/applications/aphoria/src/extractors/tls_verify.rs +++ b/applications/aphoria/src/extractors/tls_verify.rs @@ -7,7 +7,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for TLS certificate verification settings. pub struct TlsVerifyExtractor { diff --git a/applications/aphoria/src/extractors/tls_version.rs b/applications/aphoria/src/extractors/tls_version.rs index 5841f41..3c4e2ef 100644 --- a/applications/aphoria/src/extractors/tls_version.rs +++ b/applications/aphoria/src/extractors/tls_version.rs @@ -7,7 +7,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for deprecated TLS version usage. /// diff --git a/applications/aphoria/src/extractors/unreal_config.rs b/applications/aphoria/src/extractors/unreal_config.rs index fc149a0..b25d32a 100644 --- a/applications/aphoria/src/extractors/unreal_config.rs +++ b/applications/aphoria/src/extractors/unreal_config.rs @@ -11,7 +11,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for Unreal Engine INI patterns. pub struct UnrealConfigExtractor { diff --git a/applications/aphoria/src/extractors/unreal_cpp.rs b/applications/aphoria/src/extractors/unreal_cpp.rs index 2bee93d..5fcfd10 100644 --- a/applications/aphoria/src/extractors/unreal_cpp.rs +++ b/applications/aphoria/src/extractors/unreal_cpp.rs @@ -11,7 +11,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for Unreal Engine C++ patterns. pub struct UnrealCppExtractor { diff --git a/applications/aphoria/src/extractors/unreal_performance.rs b/applications/aphoria/src/extractors/unreal_performance.rs index df36349..d9a74e4 100644 --- a/applications/aphoria/src/extractors/unreal_performance.rs +++ b/applications/aphoria/src/extractors/unreal_performance.rs @@ -8,7 +8,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for Unreal Engine performance patterns. pub struct UnrealPerformanceExtractor { diff --git a/applications/aphoria/src/extractors/unsafe_atomic.rs b/applications/aphoria/src/extractors/unsafe_atomic.rs index 05346b0..7ad485d 100644 --- a/applications/aphoria/src/extractors/unsafe_atomic.rs +++ b/applications/aphoria/src/extractors/unsafe_atomic.rs @@ -9,7 +9,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for unsafe blocks and atomic ordering patterns. /// @@ -136,10 +136,7 @@ impl Extractor for UnsafeAtomicExtractor { } fn verifiable_predicates(&self) -> Vec<(&str, &str)> { - vec![ - ("atomics/ordering", "pattern"), - ("unsafe/count", "occurrences"), - ] + vec![("atomics/ordering", "pattern"), ("unsafe/count", "occurrences")] } } diff --git a/applications/aphoria/src/extractors/unvalidated_redirects.rs b/applications/aphoria/src/extractors/unvalidated_redirects.rs index 3bcc421..e321ca8 100644 --- a/applications/aphoria/src/extractors/unvalidated_redirects.rs +++ b/applications/aphoria/src/extractors/unvalidated_redirects.rs @@ -7,7 +7,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::traits::{build_claim, Extractor}; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for unvalidated redirect vulnerabilities. /// diff --git a/applications/aphoria/src/extractors/weak_crypto.rs b/applications/aphoria/src/extractors/weak_crypto.rs index b1e6ef7..c318675 100644 --- a/applications/aphoria/src/extractors/weak_crypto.rs +++ b/applications/aphoria/src/extractors/weak_crypto.rs @@ -7,7 +7,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::Extractor; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for weak cryptographic algorithm usage. /// @@ -305,10 +305,7 @@ impl Extractor for WeakCryptoExtractor { } fn verifiable_predicates(&self) -> Vec<(&str, &str)> { - vec![ - ("hashing/algorithm", "algorithm"), - ("encryption/algorithm", "algorithm"), - ] + vec![("hashing/algorithm", "algorithm"), ("encryption/algorithm", "algorithm")] } fn screening_patterns(&self) -> Vec<&str> { diff --git a/applications/aphoria/src/extractors/weak_password.rs b/applications/aphoria/src/extractors/weak_password.rs index 8a93564..8c3d038 100644 --- a/applications/aphoria/src/extractors/weak_password.rs +++ b/applications/aphoria/src/extractors/weak_password.rs @@ -7,7 +7,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::traits::{build_claim, Extractor}; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for weak password requirement configurations. /// diff --git a/applications/aphoria/src/extractors/xxe.rs b/applications/aphoria/src/extractors/xxe.rs index 5c9d110..dcf5b11 100644 --- a/applications/aphoria/src/extractors/xxe.rs +++ b/applications/aphoria/src/extractors/xxe.rs @@ -7,7 +7,7 @@ use regex::Regex; use stemedb_core::types::ObjectValue; use super::traits::{build_claim, Extractor}; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// Extractor for XXE vulnerabilities. /// diff --git a/applications/aphoria/src/handlers/claims.rs b/applications/aphoria/src/handlers/claims.rs index ac93c8c..3fc1711 100644 --- a/applications/aphoria/src/handlers/claims.rs +++ b/applications/aphoria/src/handlers/claims.rs @@ -4,8 +4,8 @@ use std::process::ExitCode; use aphoria::claims_explain; use aphoria::claims_file::ClaimsFile; -use aphoria::{parse_authority_tier, AuthoredClaim, AuthoredValue, ClaimStatus}; use aphoria::AphoriaConfig; +use aphoria::{parse_authority_tier, AuthoredClaim, AuthoredValue, ClaimStatus}; use crate::cli::ClaimsCommands; @@ -74,11 +74,53 @@ pub async fn handle_claims_command(command: ClaimsCommands, config: &AphoriaConf ClaimsCommands::Explain { claim, output, format } => { handle_claims_explain(claim, output, format, config).await } - ClaimsCommands::Update { id, provenance, invariant, consequence, tier, evidence, category, value } => { - handle_claims_update(id, provenance, invariant, consequence, tier, evidence, category, value, config).await + ClaimsCommands::Update { + id, + provenance, + invariant, + consequence, + tier, + evidence, + category, + value, + } => { + handle_claims_update( + id, + provenance, + invariant, + consequence, + tier, + evidence, + category, + value, + config, + ) + .await } - ClaimsCommands::Supersede { id, new_id, value, provenance, invariant, consequence, tier, evidence, by } => { - handle_claims_supersede(id, new_id, value, provenance, invariant, consequence, tier, evidence, by, config).await + ClaimsCommands::Supersede { + id, + new_id, + value, + provenance, + invariant, + consequence, + tier, + evidence, + by, + } => { + handle_claims_supersede( + id, + new_id, + value, + provenance, + invariant, + consequence, + tier, + evidence, + by, + config, + ) + .await } ClaimsCommands::Deprecate { id, reason } => { handle_claims_deprecate(id, reason, config).await diff --git a/applications/aphoria/src/handlers/mod.rs b/applications/aphoria/src/handlers/mod.rs index ae54c80..624da16 100644 --- a/applications/aphoria/src/handlers/mod.rs +++ b/applications/aphoria/src/handlers/mod.rs @@ -185,12 +185,11 @@ pub async fn handle_command(command: Commands, config: &AphoriaConfig) -> ExitCo } }; - let project_name = project_root - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("project"); + let project_name = + project_root.file_name().and_then(|n| n.to_str()).unwrap_or("project"); - let report = aphoria::compute_coverage(&claims_file.claims, &observations, project_name); + let report = + aphoria::compute_coverage(&claims_file.claims, &observations, project_name); let output = match format.as_str() { "json" => aphoria::format_coverage_json(&report), @@ -218,10 +217,17 @@ pub async fn handle_command(command: Commands, config: &AphoriaConfig) -> ExitCo Ok((claims, observations, project_name)) => { let verify_report = aphoria::verify_claims(&claims, &observations); let coverage_report = aphoria::compute_coverage_from_report( - &claims, &observations, &verify_report, &project_name, + &claims, + &observations, + &verify_report, + &project_name, ); let text = aphoria::explain::generate_onboarding( - &claims, &verify_report, &coverage_report, &project_name, &format, + &claims, + &verify_report, + &coverage_report, + &project_name, + &format, ); write_or_print(&text, output.as_deref()) } @@ -229,78 +235,83 @@ pub async fn handle_command(command: Commands, config: &AphoriaConfig) -> ExitCo } } - Commands::Docs { command } => { - match command { - crate::cli::DocsCommands::Generate { path, output, format } => { - let project_root = if path.as_os_str() == "." { - match std::env::current_dir() { - Ok(p) => p, - Err(e) => { - eprintln!("Cannot determine project root: {e}"); - return ExitCode::from(1); - } - } - } else { - path - }; - - match gather_explain_data(&project_root, config).await { - Ok((claims, observations, project_name)) => { - let verify_report = aphoria::verify_claims(&claims, &observations); - let coverage_report = aphoria::compute_coverage_from_report( - &claims, &observations, &verify_report, &project_name, - ); - let text = aphoria::explain::generate_full_docs( - &claims, &verify_report, &coverage_report, &project_name, &format, - ); - write_or_print(&text, output.as_deref()) - } - Err(code) => code, - } - } - } - } - - Commands::TrustPack { command } => { - match command { - crate::cli::TrustPackCommands::Install { name, registry } => { - if let Some(registry_url) = registry { - eprintln!("Custom registry not yet supported: {registry_url}"); - eprintln!("Use a built-in pack name or omit --registry."); - return ExitCode::from(1); - } - match aphoria::trust_pack_registry::lookup(&name) { - Ok(entry) => { - println!("Trust Pack: {}", entry.name); - println!(" {}", entry.description); - println!(" Tier: {}", entry.tier); - println!(" URL: {}", entry.url); - println!(); - println!("Download not yet implemented (requires hosting infrastructure)."); - println!("For now, use `aphoria corpus build` to build assertions locally,"); - println!("or `aphoria policy import ` to import a .pack file."); - ExitCode::SUCCESS - } + Commands::Docs { command } => match command { + crate::cli::DocsCommands::Generate { path, output, format } => { + let project_root = if path.as_os_str() == "." { + match std::env::current_dir() { + Ok(p) => p, Err(e) => { - eprintln!("{e}"); - ExitCode::from(1) + eprintln!("Cannot determine project root: {e}"); + return ExitCode::from(1); } } - } - crate::cli::TrustPackCommands::List => { - let packs = aphoria::trust_pack_registry::list_packs(); - println!("Available Trust Packs:"); - println!(); - for pack in packs { - println!(" {} ({})", pack.name, pack.tier); - println!(" {}", pack.description); + } else { + path + }; + + match gather_explain_data(&project_root, config).await { + Ok((claims, observations, project_name)) => { + let verify_report = aphoria::verify_claims(&claims, &observations); + let coverage_report = aphoria::compute_coverage_from_report( + &claims, + &observations, + &verify_report, + &project_name, + ); + let text = aphoria::explain::generate_full_docs( + &claims, + &verify_report, + &coverage_report, + &project_name, + &format, + ); + write_or_print(&text, output.as_deref()) } - println!(); - println!("Install with: aphoria trust-pack install "); - ExitCode::SUCCESS + Err(code) => code, } } - } + }, + + Commands::TrustPack { command } => match command { + crate::cli::TrustPackCommands::Install { name, registry } => { + if let Some(registry_url) = registry { + eprintln!("Custom registry not yet supported: {registry_url}"); + eprintln!("Use a built-in pack name or omit --registry."); + return ExitCode::from(1); + } + match aphoria::trust_pack_registry::lookup(&name) { + Ok(entry) => { + println!("Trust Pack: {}", entry.name); + println!(" {}", entry.description); + println!(" Tier: {}", entry.tier); + println!(" URL: {}", entry.url); + println!(); + println!("Download not yet implemented (requires hosting infrastructure)."); + println!( + "For now, use `aphoria corpus build` to build assertions locally," + ); + println!("or `aphoria policy import ` to import a .pack file."); + ExitCode::SUCCESS + } + Err(e) => { + eprintln!("{e}"); + ExitCode::from(1) + } + } + } + crate::cli::TrustPackCommands::List => { + let packs = aphoria::trust_pack_registry::list_packs(); + println!("Available Trust Packs:"); + println!(); + for pack in packs { + println!(" {} ({})", pack.name, pack.tier); + println!(" {}", pack.description); + } + println!(); + println!("Install with: aphoria trust-pack install "); + ExitCode::SUCCESS + } + }, } } @@ -337,11 +348,8 @@ async fn gather_explain_data( } }; - let project_name = project_root - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("project") - .to_string(); + let project_name = + project_root.file_name().and_then(|n| n.to_str()).unwrap_or("project").to_string(); Ok((claims_file.claims, observations, project_name)) } diff --git a/applications/aphoria/src/handlers/verify.rs b/applications/aphoria/src/handlers/verify.rs index 34776a6..370e56c 100644 --- a/applications/aphoria/src/handlers/verify.rs +++ b/applications/aphoria/src/handlers/verify.rs @@ -105,12 +105,8 @@ async fn handle_verify_run( Ok(c) => c, Err(_) => continue, }; - let obs = registry.extract_all( - &file.path_segments, - &content, - file.language, - &file.relative_path, - ); + let obs = + registry.extract_all(&file.path_segments, &content, file.language, &file.relative_path); all_observations.extend(obs); } @@ -118,10 +114,7 @@ async fn handle_verify_run( let report = verify::verify_claims(&claims, &all_observations); // 5. Format and output - let project_name = project_root - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("project"); + let project_name = project_root.file_name().and_then(|n| n.to_str()).unwrap_or("project"); let output = match format.as_str() { "json" => format_verify_json(&report, show_unclaimed), @@ -193,10 +186,7 @@ async fn handle_verify_map(path: PathBuf, config: &AphoriaConfig) -> ExitCode { let mut covered = 0usize; for mapping in &map.claim_mappings { if mapping.covering_extractors.is_empty() { - println!( - " {} ({}) -> NO EXTRACTOR", - mapping.claim_id, mapping.claim_tail_path - ); + println!(" {} ({}) -> NO EXTRACTOR", mapping.claim_id, mapping.claim_tail_path); } else { println!( " {} ({}) -> {}", @@ -212,11 +202,7 @@ async fn handle_verify_map(path: PathBuf, config: &AphoriaConfig) -> ExitCode { let total = map.claim_mappings.len(); println!( "Coverage: {covered}/{total} claims have covering extractors ({:.0}%)", - if total > 0 { - (covered as f64 / total as f64) * 100.0 - } else { - 0.0 - } + if total > 0 { (covered as f64 / total as f64) * 100.0 } else { 0.0 } ); // Show extractors that declare predicates but have no matching claims diff --git a/applications/aphoria/src/init.rs b/applications/aphoria/src/init.rs index 161c298..90d23cc 100644 --- a/applications/aphoria/src/init.rs +++ b/applications/aphoria/src/init.rs @@ -42,10 +42,16 @@ pub async fn show_status(config: &AphoriaConfig) -> Result let claims_path = project_root.join(".aphoria/claims.toml"); if claims_path.exists() { if let Ok(claims_file) = crate::claims_file::ClaimsFile::load(&claims_path) { - let active = claims_file.claims.iter() + let active = claims_file + .claims + .iter() .filter(|c| c.status == crate::types::authored_claim::ClaimStatus::Active) .count(); - output.push_str(&format!(" Claims: {} ({} active)\n", claims_file.claims.len(), active)); + output.push_str(&format!( + " Claims: {} ({} active)\n", + claims_file.claims.len(), + active + )); } } else { output.push_str(" Claims: none (run 'aphoria claims create' to add)\n"); diff --git a/applications/aphoria/src/lib.rs b/applications/aphoria/src/lib.rs index 1d2642b..8a0dbff 100644 --- a/applications/aphoria/src/lib.rs +++ b/applications/aphoria/src/lib.rs @@ -57,9 +57,9 @@ pub mod claims_explain; pub mod claims_file; pub mod community; mod config; -pub mod coverage; pub mod corpus; mod corpus_build; +pub mod coverage; mod episteme; pub mod scope; pub use episteme::{ @@ -67,9 +67,9 @@ pub use episteme::{ }; mod error; pub mod eval; -pub mod explain; pub mod evidence; pub mod expiry; +pub mod explain; pub mod extractors; pub mod governance; pub mod hosted; @@ -92,9 +92,8 @@ pub mod walker; // Public re-exports pub use baseline::{set_baseline, show_diff}; -pub use bridge::{ - authored_claim_to_assertion, observation_to_assertion, observation_to_tier, -}; +pub use bridge::{authored_claim_to_assertion, observation_to_assertion, observation_to_tier}; +pub use claim_store::{ClaimFilter, ClaimStore, ImportStats as ClaimImportStats, TomlClaimStore}; pub use community::{ compute_pattern_hash, AnonymizedObservation, CommunityClaimDef, CommunityExtractor, CommunityExtractorLoader, CommunityExtractorProvenance, CommunityObjectValue, PatternAggregate, @@ -105,12 +104,12 @@ pub use config::{ GovernanceConfig, HostedConfig, LearningConfig, LlmConfig, OfflineFallback, PredicateAliasConfig, PromotionConfig, ShadowConfig, SyncMode, }; +pub use corpus::{CorpusBuildResult, CorpusBuilderInfo, CorpusRegistry}; +pub use corpus_build::{build_corpus, export_corpus_as_pack, list_corpus_sources, CorpusBuildArgs}; pub use coverage::{ compute_coverage, compute_coverage_from_report, format_coverage_json, format_coverage_markdown, format_coverage_table, CoverageReport, CoverageSummary, ModuleCoverage, }; -pub use corpus::{CorpusBuildResult, CorpusBuilderInfo, CorpusRegistry}; -pub use corpus_build::{build_corpus, export_corpus_as_pack, list_corpus_sources, CorpusBuildArgs}; pub use error::AphoriaError; pub use eval::{ BaselineComparison, BaselineMetrics, CategoryMetrics, ClaimMatcher, CorpusManifest, @@ -163,13 +162,12 @@ pub use shadow::{ #[allow(deprecated)] pub use types::ExtractedClaim; // Backward compat alias for Observation pub use types::{ - extract_leaf_concept, format_authority_tier, parse_authority_tier, predicates, - AcknowledgeArgs, AuthoredClaim, AuthoredValue, BlessArgs, ClaimStatus, ClaimValue, - ComparisonMode, ConflictResult, ConflictTrace, DeprecatedUsageResult, FileSource, Observation, + extract_leaf_concept, format_authority_tier, parse_authority_tier, predicates, AcknowledgeArgs, + AuthoredClaim, AuthoredValue, BlessArgs, ClaimStatus, ClaimValue, ComparisonMode, + ConflictResult, ConflictTrace, DeprecatedUsageResult, FileSource, Observation, PolicySourceInfo, PredicateAliasSet, ScanArgs, ScanMode, ScanResult, TierBreakdown, UpdateArgs, Verdict, }; -pub use claim_store::{ClaimFilter, ClaimStore, ImportStats as ClaimImportStats, TomlClaimStore}; pub use verify::{ compute_extractor_claim_map, tail_path, verify_claims, AuditVerdict, ExtractorClaimMap, ExtractorClaimMapping, UnmatchedExtractor, VerifyReport, VerifyResult, VerifySummary, diff --git a/applications/aphoria/src/llm/extractor.rs b/applications/aphoria/src/llm/extractor.rs index 07f48ff..c5371fe 100644 --- a/applications/aphoria/src/llm/extractor.rs +++ b/applications/aphoria/src/llm/extractor.rs @@ -26,7 +26,7 @@ use crate::llm::prompts::{ DEFAULT_SYSTEM_PROMPT, }; use crate::llm::types::{LlmClaim, LlmClaimsResponse}; -use crate::types::{Observation, Language}; +use crate::types::{Language, Observation}; /// LLM-based claim extractor with ontology awareness. pub struct LlmExtractor { @@ -240,12 +240,7 @@ impl LlmExtractor { /// /// When vocabulary is available, validates claims against the ontology /// and uses fuzzy matching to correct near-misses. - fn parse_claims( - &self, - json: &str, - concept_prefix: &str, - file_path: &str, - ) -> Vec { + fn parse_claims(&self, json: &str, concept_prefix: &str, file_path: &str) -> Vec { // Try to extract JSON from response (may have markdown code blocks) let json_str = extract_json(json); diff --git a/applications/aphoria/src/policy_ops.rs b/applications/aphoria/src/policy_ops.rs index 76ebf7a..d75be6d 100644 --- a/applications/aphoria/src/policy_ops.rs +++ b/applications/aphoria/src/policy_ops.rs @@ -692,11 +692,8 @@ pub async fn export_claims_as_policy( let claims_file = ClaimsFile::load(&claims_path)?; // Only export active claims - let active_claims: Vec<_> = claims_file - .find_by_status(&ClaimStatus::Active) - .into_iter() - .cloned() - .collect(); + let active_claims: Vec<_> = + claims_file.find_by_status(&ClaimStatus::Active).into_iter().cloned().collect(); if active_claims.is_empty() { return Err(AphoriaError::Claims( @@ -717,12 +714,8 @@ pub async fn export_claims_as_policy( let assertion_count = assertions.len(); // Include predicate aliases from config - let predicate_aliases: Vec = config - .predicate_aliases - .to_alias_sets() - .iter() - .map(PackPredicateAliasSet::from) - .collect(); + let predicate_aliases: Vec = + config.predicate_aliases.to_alias_sets().iter().map(PackPredicateAliasSet::from).collect(); let pack = TrustPack::new_with_predicate_aliases( name, diff --git a/applications/aphoria/src/report/json.rs b/applications/aphoria/src/report/json.rs index 82ca1b1..7011e29 100644 --- a/applications/aphoria/src/report/json.rs +++ b/applications/aphoria/src/report/json.rs @@ -148,27 +148,57 @@ impl ReportFormatter for JsonReport { }) .collect(); + let mut summary = serde_json::json!({ + "files_scanned": result.files_scanned, + "observations_extracted": result.claims_extracted, + "authority_conflicts": result.conflicts.len(), + "blocks": result.count_by_verdict(Verdict::Block), + "flags": result.count_by_verdict(Verdict::Flag), + "acks": result.count_by_verdict(Verdict::Ack), + "passes": result.count_by_verdict(Verdict::Pass), + "drifts": result.drift_count(), + "deprecated_usages": result.deprecated_usage_count(), + "observations_recorded": result.observations_recorded, + }); + + // Add claim verification summary if present + if let Some(ref verify) = result.verify { + summary["claims_total"] = serde_json::json!(verify.summary.total_claims); + summary["claims_pass"] = serde_json::json!(verify.summary.pass); + summary["claims_conflict"] = serde_json::json!(verify.summary.conflict); + summary["claims_missing"] = serde_json::json!(verify.summary.missing); + summary["claims_unclaimed"] = serde_json::json!(verify.summary.unclaimed); + } + let mut report = serde_json::json!({ "project": result.project, "scan_id": result.scan_id, "strict": result.strict, - "summary": { - "files_scanned": result.files_scanned, - "claims_extracted": result.claims_extracted, - "conflicts": result.conflicts.len(), - "blocks": result.count_by_verdict(Verdict::Block), - "flags": result.count_by_verdict(Verdict::Flag), - "acks": result.count_by_verdict(Verdict::Ack), - "passes": result.count_by_verdict(Verdict::Pass), - "drifts": result.drift_count(), - "deprecated_usages": result.deprecated_usage_count(), - "observations_recorded": result.observations_recorded, - }, + "summary": summary, "conflicts": conflicts_json, "drifts": drifts_json, "deprecated_usages": deprecated_json, }); + // Add claim verification results if present + if let Some(ref verify) = result.verify { + let verify_json: Vec = verify + .results + .iter() + .filter_map(|r| { + let claim = r.claim.as_ref()?; + Some(serde_json::json!({ + "claim_id": claim.id, + "concept_path": claim.concept_path, + "invariant": claim.invariant, + "verdict": format!("{}", r.verdict), + "explanation": r.explanation, + })) + }) + .collect(); + report["claim_verification"] = serde_json::json!(verify_json); + } + // Add claims if present if let Some(claims) = &result.claims { let claims_json: Vec = claims @@ -255,13 +285,14 @@ mod tests { timing: None, claims: None, deprecated_usages: vec![], + verify: None, }; let output = formatter.format(&result); let parsed: serde_json::Value = serde_json::from_str(&output).expect("valid json"); assert_eq!(parsed["project"], "testproject"); - assert_eq!(parsed["summary"]["conflicts"], 1); + assert_eq!(parsed["summary"]["authority_conflicts"], 1); assert_eq!(parsed["summary"]["deprecated_usages"], 0); assert_eq!(parsed["summary"]["blocks"], 1); assert_eq!(parsed["conflicts"][0]["verdict"], "BLOCK"); diff --git a/applications/aphoria/src/report/markdown.rs b/applications/aphoria/src/report/markdown.rs index 9743be0..e982fe7 100644 --- a/applications/aphoria/src/report/markdown.rs +++ b/applications/aphoria/src/report/markdown.rs @@ -5,6 +5,7 @@ use super::{extract_leaf_concept, object_value_display, verdict_label, ReportFormatter}; use crate::types::{ScanResult, Verdict}; +use crate::verify::AuditVerdict; /// Markdown report formatter. pub struct MarkdownReport; @@ -17,32 +18,79 @@ impl ReportFormatter for MarkdownReport { out.push_str(&format!("# Aphoria Scan: {}\n\n", result.project)); // Summary - let drift_info = if result.has_drifts() { - format!(" | **{}** drifts", result.drift_count()) - } else { - String::new() - }; out.push_str(&format!( - "**{}** files scanned | **{}** claims extracted | **{}** conflicts{}\n", - result.files_scanned, - result.claims_extracted, - result.conflicts.len(), - drift_info + "**{}** files scanned | **{}** observations", + result.files_scanned, result.claims_extracted, )); + + if let Some(ref verify) = result.verify { + out.push_str(&format!( + " | **{}** claims ({} pass, {} conflict, {} missing)", + verify.summary.total_claims, + verify.summary.pass, + verify.summary.conflict, + verify.summary.missing, + )); + } + + if !result.conflicts.is_empty() { + out.push_str(&format!( + " | **{}** authority conflicts", + result.conflicts.len(), + )); + } + if result.has_drifts() { + out.push_str(&format!(" | **{}** drifts", result.drift_count())); + } + out.push('\n'); + if result.strict { out.push_str("\n**Mode:** strict (BLOCK >= 0.50, FLAG >= 0.30)\n"); } out.push('\n'); - if result.conflicts.is_empty() && result.drifts.is_empty() { - out.push_str("No conflicts or drifts found.\n"); + // Claim verification section + if let Some(ref verify) = result.verify { + let claim_results: Vec<_> = verify + .results + .iter() + .filter(|r| r.claim.is_some()) + .collect(); - // Still show claims if requested, even with no conflicts + if !claim_results.is_empty() { + out.push_str("## Claim Verification\n\n"); + out.push_str("| Verdict | Claim | Invariant | Explanation |\n"); + out.push_str("|---------|-------|-----------|-------------|\n"); + + for vr in &claim_results { + if let Some(ref claim) = vr.claim { + let verdict_str = match vr.verdict { + AuditVerdict::Pass => "PASS", + AuditVerdict::Conflict => "CONFLICT", + AuditVerdict::Missing => "MISSING", + AuditVerdict::Unclaimed => "UNCLAIMED", + }; + out.push_str(&format!( + "| {} | `{}` | {} | {} |\n", + verdict_str, claim.id, claim.invariant, vr.explanation, + )); + } + } + out.push('\n'); + } + } + + if result.conflicts.is_empty() && result.drifts.is_empty() { + if result.verify.is_none() { + out.push_str("No claims found. Run `aphoria claims create` to author claims.\n"); + } + + // Still show observations if requested if let Some(claims) = &result.claims { - out.push_str("\n## Extracted Claims\n\n"); + out.push_str("\n## Extracted Observations\n\n"); if claims.is_empty() { - out.push_str("No claims extracted.\n\n"); + out.push_str("No observations extracted.\n\n"); } else { out.push_str("| Concept | Value | File | Line | Confidence |\n"); out.push_str("|---------|-------|------|------|------------|\n"); @@ -271,12 +319,12 @@ impl ReportFormatter for MarkdownReport { } } - // Extracted Claims section + // Extracted Observations section if let Some(claims) = &result.claims { - out.push_str("## Extracted Claims\n\n"); + out.push_str("## Extracted Observations\n\n"); if claims.is_empty() { - out.push_str("No claims extracted.\n\n"); + out.push_str("No observations extracted.\n\n"); } else { out.push_str("| Concept | Value | File | Line | Confidence |\n"); out.push_str("|---------|-------|------|------|------------|\n"); @@ -346,6 +394,7 @@ mod tests { timing: None, claims: None, deprecated_usages: vec![], + verify: None, }; let output = formatter.format(&result); @@ -363,6 +412,6 @@ mod tests { let result = ScanResult::stub(&std::path::PathBuf::from("empty"), "markdown"); let output = formatter.format(&result); - assert!(output.contains("No conflicts or drifts found")); + assert!(output.contains("No claims found")); } } diff --git a/applications/aphoria/src/report/sarif.rs b/applications/aphoria/src/report/sarif.rs index 3213c60..c412126 100644 --- a/applications/aphoria/src/report/sarif.rs +++ b/applications/aphoria/src/report/sarif.rs @@ -474,6 +474,7 @@ mod tests { timing: None, claims: None, deprecated_usages: vec![], + verify: None, }; let output = formatter.format(&result); diff --git a/applications/aphoria/src/report/table.rs b/applications/aphoria/src/report/table.rs index 0b96fec..f5e1af0 100644 --- a/applications/aphoria/src/report/table.rs +++ b/applications/aphoria/src/report/table.rs @@ -7,6 +7,7 @@ use comfy_table::{Cell, CellAlignment, Color, ContentArrangement, Table}; use super::{object_value_display, verdict_label, ReportFormatter}; use crate::types::{extract_leaf_concept, ScanResult, Verdict}; +use crate::verify::AuditVerdict; /// Table report formatter for terminal output. pub struct TableReport; @@ -17,33 +18,101 @@ impl ReportFormatter for TableReport { // Header output.push_str(&format!("Aphoria Report: {}\n", result.project)); - let drift_info = if result.has_drifts() { - format!(" | Drifts: {}", result.drift_count()) - } else { - String::new() - }; output.push_str(&format!( - "Scanned: {} files | Claims: {} | Conflicts: {}{}\n", - result.files_scanned, - result.claims_extracted, - result.conflicts.len(), - drift_info + "Scanned: {} files | Observations: {}", + result.files_scanned, result.claims_extracted, )); + + // Show claim verification summary if claims exist + if let Some(ref verify) = result.verify { + output.push_str(&format!( + " | Claims: {} ({} pass, {} conflict, {} missing)", + verify.summary.total_claims, + verify.summary.pass, + verify.summary.conflict, + verify.summary.missing, + )); + } + + if !result.conflicts.is_empty() { + output.push_str(&format!( + " | Authority conflicts: {}", + result.conflicts.len() + )); + } + if result.has_drifts() { + output.push_str(&format!(" | Drifts: {}", result.drift_count())); + } + output.push('\n'); + if result.strict { output.push_str("Mode: strict (BLOCK >= 0.50, FLAG >= 0.30)\n"); } output.push('\n'); - if result.conflicts.is_empty() && result.drifts.is_empty() { - output.push_str("No conflicts or drifts found.\n"); + // Claim verification section (show first — this is the valuable output) + if let Some(ref verify) = result.verify { + let claim_results: Vec<_> = verify + .results + .iter() + .filter(|r| r.claim.is_some()) + .collect(); - // Still show claims if requested, even with no conflicts + if !claim_results.is_empty() { + output.push_str("Claim Verification:\n\n"); + let mut verify_table = Table::new(); + verify_table.set_content_arrangement(ContentArrangement::Dynamic); + verify_table.set_header(vec![ + Cell::new("Verdict").set_alignment(CellAlignment::Center), + Cell::new("Claim"), + Cell::new("Invariant"), + Cell::new("Explanation"), + ]); + + for vr in &claim_results { + if let Some(ref claim) = vr.claim { + let verdict_cell = match vr.verdict { + AuditVerdict::Pass => { + Cell::new("PASS").fg(Color::Green) + } + AuditVerdict::Conflict => { + Cell::new("CONFLICT").fg(Color::Red) + } + AuditVerdict::Missing => { + Cell::new("MISSING").fg(Color::Yellow) + } + AuditVerdict::Unclaimed => { + Cell::new("UNCLAIMED").fg(Color::DarkGrey) + } + }; + + verify_table.add_row(vec![ + verdict_cell, + Cell::new(&claim.id), + Cell::new(&claim.invariant), + Cell::new(&vr.explanation), + ]); + } + } + + output.push_str(&verify_table.to_string()); + output.push('\n'); + output.push('\n'); + } + } + + if result.conflicts.is_empty() && result.drifts.is_empty() { + if result.verify.is_none() { + output.push_str("No claims found. Run `aphoria claims create` to author claims.\n"); + } + + // Still show observations if requested if let Some(claims) = &result.claims { if claims.is_empty() { - output.push_str("\nExtracted Claims:\n\n"); - output.push_str(" No claims extracted.\n"); + output.push_str("\nExtracted Observations:\n\n"); + output.push_str(" No observations extracted.\n"); } else { - output.push_str("\nExtracted Claims:\n\n"); + output.push_str("\nExtracted Observations:\n\n"); let mut claims_table = Table::new(); claims_table.set_content_arrangement(ContentArrangement::Dynamic); claims_table.set_header(vec![ @@ -213,9 +282,18 @@ impl ReportFormatter for TableReport { // Show full conflict trace in debug mode if result.debug { if let Some(trace) = &conflict.trace { - output.push_str(&format!(" Trace: code = \"{}\"\n", trace.code_claim)); - output.push_str(&format!(" auth = \"{}\"\n", trace.authority_match)); - output.push_str(&format!(" score = {:.2} → {}\n", trace.conflict_score, trace.resolution)); + output.push_str(&format!( + " Trace: code = \"{}\"\n", + trace.code_claim + )); + output.push_str(&format!( + " auth = \"{}\"\n", + trace.authority_match + )); + output.push_str(&format!( + " score = {:.2} → {}\n", + trace.conflict_score, trace.resolution + )); } } @@ -278,13 +356,13 @@ impl ReportFormatter for TableReport { } } - // Extracted Claims section + // Extracted Observations section (only when --show-claims is used) if let Some(claims) = &result.claims { if claims.is_empty() { - output.push_str("\nExtracted Claims:\n\n"); - output.push_str(" No claims extracted.\n\n"); + output.push_str("\nExtracted Observations:\n\n"); + output.push_str(" No observations extracted.\n\n"); } else { - output.push_str("\nExtracted Claims:\n\n"); + output.push_str("\nExtracted Observations:\n\n"); let mut claims_table = Table::new(); claims_table.set_content_arrangement(ContentArrangement::Dynamic); claims_table.set_header(vec![ @@ -430,6 +508,7 @@ mod tests { timing: None, claims: None, deprecated_usages: vec![], + verify: None, } } @@ -450,6 +529,6 @@ mod tests { let result = ScanResult::stub(&std::path::PathBuf::from("empty"), "table"); let output = formatter.format(&result); - assert!(output.contains("No conflicts or drifts found")); + assert!(output.contains("No claims found")); } } diff --git a/applications/aphoria/src/report/verify_json.rs b/applications/aphoria/src/report/verify_json.rs index 11f079e..c12a20a 100644 --- a/applications/aphoria/src/report/verify_json.rs +++ b/applications/aphoria/src/report/verify_json.rs @@ -72,10 +72,7 @@ mod tests { #[test] fn test_empty_report_json() { - let report = VerifyReport { - results: vec![], - summary: VerifySummary::default(), - }; + let report = VerifyReport { results: vec![], summary: VerifySummary::default() }; let output = format_verify_json(&report, false); let parsed: serde_json::Value = serde_json::from_str(&output).expect("valid json"); assert_eq!(parsed["summary"]["total_claims"], 0); diff --git a/applications/aphoria/src/report/verify_table.rs b/applications/aphoria/src/report/verify_table.rs index ab72198..48b50a7 100644 --- a/applications/aphoria/src/report/verify_table.rs +++ b/applications/aphoria/src/report/verify_table.rs @@ -3,7 +3,11 @@ use crate::verify::{AuditVerdict, VerifyReport}; /// Format a verification report as a terminal table. -pub fn format_verify_table(report: &VerifyReport, project_name: &str, show_unclaimed: bool) -> String { +pub fn format_verify_table( + report: &VerifyReport, + project_name: &str, + show_unclaimed: bool, +) -> String { let mut out = String::new(); out.push_str(&format!("Aphoria Verify - {project_name}\n")); @@ -36,10 +40,7 @@ pub fn format_verify_table(report: &VerifyReport, project_name: &str, show_uncla format!("{} (present)", claim.concept_path) } _ => { - format!( - "{}/{} = {}", - claim.concept_path, claim.predicate, claim.value - ) + format!("{}/{} = {}", claim.concept_path, claim.predicate, claim.value) } }; (claim.id.as_str(), display) @@ -74,10 +75,7 @@ pub fn format_verify_table(report: &VerifyReport, project_name: &str, show_uncla } if let Some(ref claim) = result.claim { if !claim.consequence.is_empty() { - out.push_str(&format!( - " Consequence: {}\n", - claim.consequence - )); + out.push_str(&format!(" Consequence: {}\n", claim.consequence)); } } } @@ -86,10 +84,7 @@ pub fn format_verify_table(report: &VerifyReport, project_name: &str, show_uncla } AuditVerdict::Unclaimed => { for obs in &result.matching_observations { - out.push_str(&format!( - " At: {}:{}\n", - obs.file, obs.line - )); + out.push_str(&format!(" At: {}:{}\n", obs.file, obs.line)); } } } @@ -123,10 +118,7 @@ mod tests { #[test] fn test_empty_report() { - let report = VerifyReport { - results: vec![], - summary: VerifySummary::default(), - }; + let report = VerifyReport { results: vec![], summary: VerifySummary::default() }; let output = format_verify_table(&report, "test-project", false); assert!(output.contains("Aphoria Verify - test-project")); assert!(output.contains("0 total")); diff --git a/applications/aphoria/src/scan/filter.rs b/applications/aphoria/src/scan/filter.rs index 87a1bde..4b9652f 100644 --- a/applications/aphoria/src/scan/filter.rs +++ b/applications/aphoria/src/scan/filter.rs @@ -9,7 +9,7 @@ use crate::config::AphoriaConfig; use crate::learning::{ normalize_pattern, ClaimTemplate, LearnedPattern, LocalPatternStore, PatternStore, ValueType, }; -use crate::types::{Observation, Language, ScanMode}; +use crate::types::{Language, Observation, ScanMode}; /// Process extracted claims with optional pattern learning. /// diff --git a/applications/aphoria/src/scan/scanner.rs b/applications/aphoria/src/scan/scanner.rs index 54a7b04..269ef8e 100644 --- a/applications/aphoria/src/scan/scanner.rs +++ b/applications/aphoria/src/scan/scanner.rs @@ -7,6 +7,7 @@ use std::time::Instant; use tracing::{info, instrument}; use crate::bridge::{self, observation_to_assertion}; +use crate::claims_file::ClaimsFile; use crate::config::{AphoriaConfig, SyncMode}; use crate::episteme::{ create_authoritative_corpus, current_timestamp_millis, ConceptIndex, EphemeralDetector, @@ -16,9 +17,10 @@ use crate::error::AphoriaError; use crate::hosted::HostedClient; use crate::policy::PolicyManager; use crate::types::{ - ConflictResult, DriftResult, Observation, FileSource, ScanArgs, ScanMode, ScanResult, + ConflictResult, DriftResult, FileSource, Observation, ScanArgs, ScanMode, ScanResult, ScanTiming, }; +use crate::verify; use crate::walker::{walk_project, walk_staged_files}; use super::walker::extract_claims_from_files; @@ -77,17 +79,29 @@ pub async fn run_scan(args: ScanArgs, config: &AphoriaConfig) -> Result Result Result, + + /// Claim verification results from authored claims in `.aphoria/claims.toml`. + /// + /// When present, contains per-claim PASS/CONFLICT/MISSING verdicts from + /// comparing observations against human-authored claims. + pub verify: Option, } /// Timing breakdown for benchmark mode. @@ -99,6 +106,7 @@ impl ScanResult { timing: None, claims: None, deprecated_usages: vec![], + verify: None, } } @@ -470,6 +478,7 @@ mod tests { timing: None, claims: None, deprecated_usages: vec![], + verify: None, }; assert!(!result.has_blocks()); diff --git a/applications/aphoria/src/verify.rs b/applications/aphoria/src/verify.rs index 61e7f33..4537c1b 100644 --- a/applications/aphoria/src/verify.rs +++ b/applications/aphoria/src/verify.rs @@ -83,10 +83,7 @@ pub struct VerifyReport { /// - `"single"` → `None` (fewer than 2 segments) pub fn tail_path(concept_path: &str) -> Option { // Strip scheme if present - let path = concept_path - .find("://") - .map(|i| &concept_path[i + 3..]) - .unwrap_or(concept_path); + let path = concept_path.find("://").map(|i| &concept_path[i + 3..]).unwrap_or(concept_path); let mut segments = path.rsplit('/').filter(|s| !s.is_empty()); let tail2 = segments.next()?; @@ -142,63 +139,39 @@ pub fn verify_claims(claims: &[AuthoredClaim], observations: &[Observation]) -> claimed_tails.insert(tp.clone(), true); - let matching: Vec<&Observation> = obs_by_tail - .get(&tp) - .map(|v| v.as_slice()) - .unwrap_or(&[]) - .to_vec(); + let matching: Vec<&Observation> = + obs_by_tail.get(&tp).map(|v| v.as_slice()).unwrap_or(&[]).to_vec(); let claim_obj_value = claim.value.to_object_value(); let (verdict, explanation) = match claim.comparison { ComparisonMode::Equals => { if matching.is_empty() { - ( - AuditVerdict::Missing, - "No matching observation found".to_string(), - ) + (AuditVerdict::Missing, "No matching observation found".to_string()) } else if matching.iter().any(|o| o.value == claim_obj_value) { ( AuditVerdict::Pass, - format!( - "Observation matches claim value: {}", - claim.value - ), + format!("Observation matches claim value: {}", claim.value), ) } else { - let found_values: Vec = matching - .iter() - .map(|o| format!("{:?}", o.value)) - .collect(); + let found_values: Vec = + matching.iter().map(|o| format!("{:?}", o.value)).collect(); ( AuditVerdict::Conflict, - format!( - "Expected {}, found: {}", - claim.value, - found_values.join(", ") - ), + format!("Expected {}, found: {}", claim.value, found_values.join(", ")), ) } } ComparisonMode::NotEquals => { if matching.is_empty() { // No observations means no contradiction — pass - ( - AuditVerdict::Pass, - "No observations found (no contradiction)".to_string(), - ) + (AuditVerdict::Pass, "No observations found (no contradiction)".to_string()) } else if matching.iter().any(|o| o.value == claim_obj_value) { ( AuditVerdict::Conflict, - format!( - "Found observation with forbidden value: {}", - claim.value - ), + format!("Found observation with forbidden value: {}", claim.value), ) } else { - ( - AuditVerdict::Pass, - "All observations differ from forbidden value".to_string(), - ) + (AuditVerdict::Pass, "All observations differ from forbidden value".to_string()) } } ComparisonMode::Present => { @@ -216,21 +189,13 @@ pub fn verify_claims(claims: &[AuthoredClaim], observations: &[Observation]) -> } ComparisonMode::Absent => { if matching.is_empty() { - ( - AuditVerdict::Pass, - "No observations found (as expected)".to_string(), - ) + (AuditVerdict::Pass, "No observations found (as expected)".to_string()) } else { - let locations: Vec = matching - .iter() - .map(|o| format!("{}:{}", o.file, o.line)) - .collect(); + let locations: Vec = + matching.iter().map(|o| format!("{}:{}", o.file, o.line)).collect(); ( AuditVerdict::Conflict, - format!( - "Expected absent, but found at: {}", - locations.join(", ") - ), + format!("Expected absent, but found at: {}", locations.join(", ")), ) } } @@ -379,10 +344,7 @@ pub fn compute_extractor_claim_map( }) .collect(); - ExtractorClaimMap { - claim_mappings, - unmatched_extractors, - } + ExtractorClaimMap { claim_mappings, unmatched_extractors } } #[cfg(test)] @@ -680,8 +642,8 @@ created_at = "2026-02-08T12:00:00Z" #[test] fn test_compute_extractor_claim_map() { - use crate::extractors::ExtractorRegistry; use crate::config::AphoriaConfig; + use crate::extractors::ExtractorRegistry; let config = AphoriaConfig::default(); let registry = ExtractorRegistry::new(&config); @@ -708,19 +670,15 @@ created_at = "2026-02-08T12:00:00Z" // tls_verify should cover tls-001 let tls_mapping = map.claim_mappings.iter().find(|m| m.claim_id == "tls-001"); assert!(tls_mapping.is_some()); - assert!( - tls_mapping - .map(|m| m.covering_extractors.contains(&"tls_verify".to_string())) - .unwrap_or(false) - ); + assert!(tls_mapping + .map(|m| m.covering_extractors.contains(&"tls_verify".to_string())) + .unwrap_or(false)); // import_graph should cover import-001 via wildcard let import_mapping = map.claim_mappings.iter().find(|m| m.claim_id == "import-001"); assert!(import_mapping.is_some()); - assert!( - import_mapping - .map(|m| m.covering_extractors.contains(&"import_graph".to_string())) - .unwrap_or(false) - ); + assert!(import_mapping + .map(|m| m.covering_extractors.contains(&"import_graph".to_string())) + .unwrap_or(false)); } } diff --git a/crates/stemedb-api/src/bootstrap.rs b/crates/stemedb-api/src/bootstrap.rs index bd1e95d..630a814 100644 --- a/crates/stemedb-api/src/bootstrap.rs +++ b/crates/stemedb-api/src/bootstrap.rs @@ -29,7 +29,7 @@ pub fn hash_api_key(raw_key: &str) -> [u8; 32] { /// /// ```ignore /// // Set in environment before starting: -/// // STEMEDB_ROOT_API_KEY=steme_live_your_secure_key_here +/// // export STEMEDB_ROOT_API_KEY=steme_live_$(openssl rand -hex 24) /// /// // In startup code: /// bootstrap_root_api_key(&api_key_store).await?; diff --git a/crates/stemedb-api/src/handlers/aphoria/scan.rs b/crates/stemedb-api/src/handlers/aphoria/scan.rs index 0ad5a4f..f7fedb8 100644 --- a/crates/stemedb-api/src/handlers/aphoria/scan.rs +++ b/crates/stemedb-api/src/handlers/aphoria/scan.rs @@ -62,6 +62,7 @@ pub async fn scan( file_source: aphoria::FileSource::All, benchmark: false, show_claims: false, + strict: false, }; // Execute scan diff --git a/crates/stemedb-api/src/handlers/health.rs b/crates/stemedb-api/src/handlers/health.rs index e1d080e..96a218f 100644 --- a/crates/stemedb-api/src/handlers/health.rs +++ b/crates/stemedb-api/src/handlers/health.rs @@ -29,12 +29,8 @@ pub async fn health_check(State(state): State) -> Result) -> Self { + self.rate_limit = limit; + self + } + /// Check if this key has expired. pub fn is_expired(&self, now: u64) -> bool { self.expires_at.is_some_and(|exp| now > exp)