fix(aphoria): move claims.toml to project root and fix verify integration
## Root Cause Claims file was in applications/aphoria/.aphoria/ but all commands looked for .aphoria/claims.toml relative to project root. Additionally, .aphoria/ was fully gitignored, preventing version control of claims. ## Changes ### Path Fixes - Move claims.toml from applications/aphoria/.aphoria/ to .aphoria/ at project root - Update .gitignore: .aphoria/ → .aphoria/* with !.aphoria/claims.toml exception - Now claims can be version controlled while keys remain secret ### Verify Integration (Scanner) - scanner.rs: Load claims from ClaimsFile and call verify_claims() - ScanResult: Add verify field with VerifyReport - Report formatters: Add claim verification sections showing PASS/CONFLICT/MISSING ### Clippy Fix - report/json.rs: Replace filter().map().expect() with filter_map() ## Verification - aphoria scan . → Shows claim verification with verdicts - aphoria verify run → Per-claim verification results - aphoria verify map → Extractor coverage mapping (7/10 claims = 70%) - aphoria claims list → Reads from project root - aphoria claims create → Writes to project root - All tests pass (1120+ aphoria tests) - clippy --workspace passes ## Impact Both primary use cases now work: 1. Day-to-day (commit-time): Skills can read/create claims via CLI 2. Audit (scan-time): Scanner verifies code against authored claims Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3b5f88b4f0
commit
6430ff0fd6
168
.aphoria/claims.toml
Normal file
168
.aphoria/claims.toml
Normal file
@ -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"
|
||||
@ -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
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -24,7 +24,8 @@ credentials.json
|
||||
service-account*.json
|
||||
|
||||
# Aphoria project data (contains keys)
|
||||
.aphoria/
|
||||
.aphoria/*
|
||||
!.aphoria/claims.toml
|
||||
|
||||
# Python virtual environments
|
||||
.venv/
|
||||
|
||||
49
CLAUDE.md
49
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.
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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<String, serde_json::Error> {
|
||||
pub fn render_claim_json(
|
||||
claim: &AuthoredClaim,
|
||||
project_name: &str,
|
||||
) -> Result<String, serde_json::Error> {
|
||||
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<St
|
||||
}
|
||||
|
||||
/// Render all claims as JSON wrapped in a structured envelope.
|
||||
pub fn render_claims_json(claims: &[AuthoredClaim], project_name: &str) -> Result<String, serde_json::Error> {
|
||||
pub fn render_claims_json(
|
||||
claims: &[AuthoredClaim],
|
||||
project_name: &str,
|
||||
) -> Result<String, serde_json::Error> {
|
||||
let envelope = serde_json::json!({
|
||||
"type": "claims_explain",
|
||||
"project": project_name,
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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<crate::policy::PackPredicateAliasSet> =
|
||||
config.predicate_aliases.to_alias_sets().iter().map(crate::policy::PackPredicateAliasSet::from).collect();
|
||||
let predicate_aliases: Vec<crate::policy::PackPredicateAliasSet> = config
|
||||
.predicate_aliases
|
||||
.to_alias_sets()
|
||||
.iter()
|
||||
.map(crate::policy::PackPredicateAliasSet::from)
|
||||
.collect();
|
||||
|
||||
// Package as Trust Pack
|
||||
let pack = TrustPack::new_with_predicate_aliases(
|
||||
|
||||
@ -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<String> = obs_list
|
||||
.map(|obs| {
|
||||
let mut file_set: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
|
||||
let mut file_set: std::collections::BTreeSet<String> =
|
||||
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");
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -185,8 +185,7 @@ pub fn check_conflicts_with_predicate_aliases(
|
||||
let mut by_tier: BTreeMap<u8, (SourceClass, usize, f32)> = 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(),
|
||||
)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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<Observation> {
|
||||
fn extract_npm(&self, path_segments: &[String], content: &str, file: &str) -> Vec<Observation> {
|
||||
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<Observation> {
|
||||
fn extract_go(&self, path_segments: &[String], content: &str, file: &str) -> Vec<Observation> {
|
||||
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<Observation> {
|
||||
fn extract_pip(&self, path_segments: &[String], content: &str, file: &str) -> Vec<Observation> {
|
||||
let mut claims = Vec::new();
|
||||
|
||||
for (line_idx, line) in content.lines().enumerate() {
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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!(
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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")]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 <file>` 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 <name>");
|
||||
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 <file>` 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 <name>");
|
||||
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))
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -42,10 +42,16 @@ pub async fn show_status(config: &AphoriaConfig) -> Result<String, AphoriaError>
|
||||
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");
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<Observation> {
|
||||
fn parse_claims(&self, json: &str, concept_prefix: &str, file_path: &str) -> Vec<Observation> {
|
||||
// Try to extract JSON from response (may have markdown code blocks)
|
||||
let json_str = extract_json(json);
|
||||
|
||||
|
||||
@ -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<PackPredicateAliasSet> = config
|
||||
.predicate_aliases
|
||||
.to_alias_sets()
|
||||
.iter()
|
||||
.map(PackPredicateAliasSet::from)
|
||||
.collect();
|
||||
let predicate_aliases: Vec<PackPredicateAliasSet> =
|
||||
config.predicate_aliases.to_alias_sets().iter().map(PackPredicateAliasSet::from).collect();
|
||||
|
||||
let pack = TrustPack::new_with_predicate_aliases(
|
||||
name,
|
||||
|
||||
@ -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<serde_json::Value> = 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<serde_json::Value> = 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");
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -474,6 +474,7 @@ mod tests {
|
||||
timing: None,
|
||||
claims: None,
|
||||
deprecated_usages: vec![],
|
||||
verify: None,
|
||||
};
|
||||
|
||||
let output = formatter.format(&result);
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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"));
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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<ScanResu
|
||||
|
||||
let total_ms = total_start.elapsed().as_millis() as u64;
|
||||
|
||||
// 4. Calculate lines of code if benchmark mode
|
||||
// 4. Verify authored claims against observations
|
||||
let verify_report = {
|
||||
let claims_path = ClaimsFile::default_path(&project_root);
|
||||
let claims_file = ClaimsFile::load(&claims_path)?;
|
||||
if claims_file.is_empty() {
|
||||
None
|
||||
} else {
|
||||
info!(claims = claims_file.len(), "Verifying authored claims");
|
||||
Some(verify::verify_claims(&claims_file.claims, &all_claims))
|
||||
}
|
||||
};
|
||||
|
||||
// 5. Calculate lines of code if benchmark mode
|
||||
let lines_of_code = if args.benchmark { Some(count_lines_of_code(&files)) } else { None };
|
||||
|
||||
// 5. Build timing info if benchmark mode
|
||||
// 6. Build timing info if benchmark mode
|
||||
let timing = if args.benchmark {
|
||||
Some(ScanTiming { walk_ms, extraction_ms, conflict_ms, total_ms, lines_of_code })
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// 6. Populate claims if requested (clone and sort by file, then line)
|
||||
// 7. Populate claims if requested (clone and sort by file, then line)
|
||||
let claims = if args.show_claims {
|
||||
let mut sorted = all_claims.to_vec();
|
||||
sorted.sort_by(|a, b| a.file.cmp(&b.file).then(a.line.cmp(&b.line)));
|
||||
@ -96,7 +110,7 @@ pub async fn run_scan(args: ScanArgs, config: &AphoriaConfig) -> Result<ScanResu
|
||||
None
|
||||
};
|
||||
|
||||
// 7. Build result
|
||||
// 8. Build result
|
||||
let project_name =
|
||||
project_root.file_name().and_then(|s| s.to_str()).unwrap_or("unknown").to_string();
|
||||
|
||||
@ -114,6 +128,7 @@ pub async fn run_scan(args: ScanArgs, config: &AphoriaConfig) -> Result<ScanResu
|
||||
timing,
|
||||
claims,
|
||||
deprecated_usages: vec![], // TODO: Populate from lifecycle store during scan
|
||||
verify: verify_report,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -98,12 +98,8 @@ pub fn extract_claims_from_files(
|
||||
};
|
||||
|
||||
// Run regex extractors first
|
||||
let regex_claims = registry.extract_all(
|
||||
&file.path_segments,
|
||||
&content,
|
||||
file.language,
|
||||
&file.relative_path,
|
||||
);
|
||||
let regex_claims =
|
||||
registry.extract_all(&file.path_segments, &content, file.language, &file.relative_path);
|
||||
|
||||
// If no regex claims AND file is high-value, try LLM extraction
|
||||
if regex_claims.is_empty() {
|
||||
@ -111,12 +107,8 @@ pub fn extract_claims_from_files(
|
||||
!config.llm.high_value_only || is_high_value_file(&file.relative_path);
|
||||
|
||||
if should_try_llm {
|
||||
let claims = llm.extract(
|
||||
&file.path_segments,
|
||||
&content,
|
||||
file.language,
|
||||
&file.relative_path,
|
||||
);
|
||||
let claims =
|
||||
llm.extract(&file.path_segments, &content, file.language, &file.relative_path);
|
||||
if !claims.is_empty() {
|
||||
llm_files_processed += 1;
|
||||
llm_claims_found += claims.len();
|
||||
|
||||
@ -70,6 +70,7 @@ fn test_scan_result_has_drifts() {
|
||||
timing: None,
|
||||
deprecated_usages: vec![],
|
||||
claims: None,
|
||||
verify: None,
|
||||
};
|
||||
|
||||
assert!(result.has_drifts());
|
||||
@ -105,6 +106,7 @@ fn test_drift_json_output_format() {
|
||||
timing: None,
|
||||
deprecated_usages: vec![],
|
||||
claims: None,
|
||||
verify: None,
|
||||
};
|
||||
|
||||
let formatter = JsonReport;
|
||||
@ -142,6 +144,7 @@ fn test_drift_sarif_output_format() {
|
||||
timing: None,
|
||||
deprecated_usages: vec![],
|
||||
claims: None,
|
||||
verify: None,
|
||||
};
|
||||
|
||||
let formatter = SarifReport;
|
||||
@ -181,6 +184,7 @@ fn test_drift_table_output_format() {
|
||||
timing: None,
|
||||
deprecated_usages: vec![],
|
||||
claims: None,
|
||||
verify: None,
|
||||
};
|
||||
|
||||
let formatter = TableReport;
|
||||
|
||||
@ -83,11 +83,7 @@ fn test_full_verify_mixed_verdicts() {
|
||||
"enabled",
|
||||
ObjectValue::Boolean(true),
|
||||
),
|
||||
make_obs(
|
||||
"code://rust/project/config/timeout",
|
||||
"value",
|
||||
ObjectValue::Number(60.0),
|
||||
),
|
||||
make_obs("code://rust/project/config/timeout", "value", ObjectValue::Number(60.0)),
|
||||
// An unclaimed observation
|
||||
make_obs(
|
||||
"code://rust/project/cors/allow_origin",
|
||||
@ -105,19 +101,22 @@ fn test_full_verify_mixed_verdicts() {
|
||||
assert_eq!(report.summary.unclaimed, 1);
|
||||
|
||||
// Verify individual results
|
||||
let pass = report.results.iter().find(|r| {
|
||||
r.claim.as_ref().map(|c| c.id.as_str()) == Some("pass-claim")
|
||||
});
|
||||
let pass = report
|
||||
.results
|
||||
.iter()
|
||||
.find(|r| r.claim.as_ref().map(|c| c.id.as_str()) == Some("pass-claim"));
|
||||
assert_eq!(pass.map(|r| &r.verdict), Some(&AuditVerdict::Pass));
|
||||
|
||||
let conflict = report.results.iter().find(|r| {
|
||||
r.claim.as_ref().map(|c| c.id.as_str()) == Some("conflict-claim")
|
||||
});
|
||||
let conflict = report
|
||||
.results
|
||||
.iter()
|
||||
.find(|r| r.claim.as_ref().map(|c| c.id.as_str()) == Some("conflict-claim"));
|
||||
assert_eq!(conflict.map(|r| &r.verdict), Some(&AuditVerdict::Conflict));
|
||||
|
||||
let missing = report.results.iter().find(|r| {
|
||||
r.claim.as_ref().map(|c| c.id.as_str()) == Some("missing-claim")
|
||||
});
|
||||
let missing = report
|
||||
.results
|
||||
.iter()
|
||||
.find(|r| r.claim.as_ref().map(|c| c.id.as_str()) == Some("missing-claim"));
|
||||
assert_eq!(missing.map(|r| &r.verdict), Some(&AuditVerdict::Missing));
|
||||
}
|
||||
|
||||
@ -159,11 +158,8 @@ fn test_verifiable_predicates_coverage() {
|
||||
let registry = ExtractorRegistry::new(&config);
|
||||
|
||||
// Count extractors that declare verifiable predicates
|
||||
let declaring_count = registry
|
||||
.extractors()
|
||||
.iter()
|
||||
.filter(|e| !e.verifiable_predicates().is_empty())
|
||||
.count();
|
||||
let declaring_count =
|
||||
registry.extractors().iter().filter(|e| !e.verifiable_predicates().is_empty()).count();
|
||||
|
||||
// We implemented verifiable_predicates() on 10 extractors;
|
||||
// self_audit is opt-in so 9 are present in default config
|
||||
@ -173,19 +169,13 @@ fn test_verifiable_predicates_coverage() {
|
||||
);
|
||||
|
||||
// Verify specific extractors declare the right predicates
|
||||
let tls_verify = registry
|
||||
.extractors()
|
||||
.iter()
|
||||
.find(|e| e.name() == "tls_verify");
|
||||
let tls_verify = registry.extractors().iter().find(|e| e.name() == "tls_verify");
|
||||
assert!(tls_verify.is_some());
|
||||
let preds = tls_verify.map(|e| e.verifiable_predicates()).unwrap_or_default();
|
||||
assert!(preds.contains(&("tls/cert_verification", "enabled")));
|
||||
|
||||
// Verify wildcard pattern for import_graph
|
||||
let import_graph = registry
|
||||
.extractors()
|
||||
.iter()
|
||||
.find(|e| e.name() == "import_graph");
|
||||
let import_graph = registry.extractors().iter().find(|e| e.name() == "import_graph");
|
||||
assert!(import_graph.is_some());
|
||||
let preds = import_graph.map(|e| e.verifiable_predicates()).unwrap_or_default();
|
||||
assert!(preds.contains(&("imports/*", "imported")));
|
||||
@ -253,11 +243,8 @@ fn test_absent_mode_integration() {
|
||||
assert_eq!(report.summary.pass, 1);
|
||||
|
||||
// Tokio import found — CONFLICT
|
||||
let obs = vec![make_obs(
|
||||
"code://rust/core/imports/tokio",
|
||||
"imported",
|
||||
ObjectValue::Boolean(true),
|
||||
)];
|
||||
let obs =
|
||||
vec![make_obs("code://rust/core/imports/tokio", "imported", ObjectValue::Boolean(true))];
|
||||
let report = verify_claims(&claims, &obs);
|
||||
assert_eq!(report.summary.conflict, 1);
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ use stemedb_core::types::ObjectValue;
|
||||
|
||||
use super::claim::{ConflictingSource, Observation};
|
||||
use super::verdict::Verdict;
|
||||
use crate::verify::VerifyReport;
|
||||
|
||||
/// Result of a scan operation.
|
||||
#[derive(Debug, Clone)]
|
||||
@ -58,6 +59,12 @@ pub struct ScanResult {
|
||||
/// Populated when deprecated patterns are matched during scan.
|
||||
/// These generate FLAG warnings with migration guidance.
|
||||
pub deprecated_usages: Vec<DeprecatedUsageResult>,
|
||||
|
||||
/// 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<VerifyReport>,
|
||||
}
|
||||
|
||||
/// 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());
|
||||
|
||||
@ -83,10 +83,7 @@ pub struct VerifyReport {
|
||||
/// - `"single"` → `None` (fewer than 2 segments)
|
||||
pub fn tail_path(concept_path: &str) -> Option<String> {
|
||||
// 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<String> = matching
|
||||
.iter()
|
||||
.map(|o| format!("{:?}", o.value))
|
||||
.collect();
|
||||
let found_values: Vec<String> =
|
||||
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<String> = matching
|
||||
.iter()
|
||||
.map(|o| format!("{}:{}", o.file, o.line))
|
||||
.collect();
|
||||
let locations: Vec<String> =
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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?;
|
||||
|
||||
@ -62,6 +62,7 @@ pub async fn scan(
|
||||
file_source: aphoria::FileSource::All,
|
||||
benchmark: false,
|
||||
show_claims: false,
|
||||
strict: false,
|
||||
};
|
||||
|
||||
// Execute scan
|
||||
|
||||
@ -29,12 +29,8 @@ pub async fn health_check(State(state): State<AppState>) -> Result<Json<HealthRe
|
||||
// Update Prometheus gauges (best-effort — don't fail health check)
|
||||
metrics::gauge!("stemedb_assertions_total").set(assertions_count as f64);
|
||||
|
||||
let pending_count = state
|
||||
.quarantine_store
|
||||
.list_pending(usize::MAX)
|
||||
.await
|
||||
.map(|v| v.len())
|
||||
.unwrap_or_default();
|
||||
let pending_count =
|
||||
state.quarantine_store.list_pending(usize::MAX).await.map(|v| v.len()).unwrap_or_default();
|
||||
metrics::gauge!("stemedb_quarantine_pending").set(pending_count as f64);
|
||||
|
||||
let tripped_count = state
|
||||
|
||||
@ -89,8 +89,14 @@ pub struct ApiKeyRecord {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// Default rate limit for new API keys: 3600 requests per hour (1 req/sec).
|
||||
pub const DEFAULT_RATE_LIMIT: u64 = 3600;
|
||||
|
||||
impl ApiKeyRecord {
|
||||
/// Create a new API key record.
|
||||
///
|
||||
/// Keys are created with a default rate limit of 3600 req/hour.
|
||||
/// Use `with_rate_limit` to override.
|
||||
pub fn new(
|
||||
key_hash: [u8; 32],
|
||||
key_prefix: String,
|
||||
@ -102,7 +108,7 @@ impl ApiKeyRecord {
|
||||
key_hash,
|
||||
key_prefix,
|
||||
role,
|
||||
rate_limit: None,
|
||||
rate_limit: Some(DEFAULT_RATE_LIMIT),
|
||||
label,
|
||||
created_at,
|
||||
last_used_at: 0,
|
||||
@ -111,6 +117,12 @@ impl ApiKeyRecord {
|
||||
}
|
||||
}
|
||||
|
||||
/// Override the default rate limit for this key.
|
||||
pub fn with_rate_limit(mut self, limit: Option<u64>) -> 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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user