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:
jml 2026-02-08 11:09:57 +00:00
parent 3b5f88b4f0
commit 6430ff0fd6
88 changed files with 892 additions and 542 deletions

168
.aphoria/claims.toml Normal file
View 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"

View File

@ -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
View File

@ -24,7 +24,8 @@ credentials.json
service-account*.json
# Aphoria project data (contains keys)
.aphoria/
.aphoria/*
!.aphoria/claims.toml
# Python virtual environments
.venv/

View File

@ -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.

View File

@ -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());
}

View File

@ -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,

View File

@ -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]

View File

@ -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)]

View File

@ -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 {

View File

@ -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;

View File

@ -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(

View File

@ -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");

View File

@ -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);

View File

@ -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(),
)

View File

@ -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 {

View File

@ -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;

View File

@ -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)]

View File

@ -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());

View File

@ -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.
///

View File

@ -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 {

View File

@ -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.
///

View File

@ -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.
///

View File

@ -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.
///

View File

@ -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 {

View File

@ -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.
///

View File

@ -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> {

View File

@ -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 {

View File

@ -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() {

View File

@ -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.
///

View File

@ -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 {

View File

@ -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.
///

View File

@ -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)]

View File

@ -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)]

View File

@ -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)]

View File

@ -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 {

View File

@ -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};

View File

@ -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.
///

View File

@ -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.
///

View File

@ -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.
///

View File

@ -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 {

View File

@ -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)]

View File

@ -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)]

View File

@ -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)]

View File

@ -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.
///

View File

@ -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.
///

View File

@ -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 {

View File

@ -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)]

View File

@ -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!(

View File

@ -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.
///

View File

@ -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();

View File

@ -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)]

View File

@ -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.
///

View File

@ -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.
///

View File

@ -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)]

View File

@ -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 {

View File

@ -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.
///

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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")]
}
}

View File

@ -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.
///

View File

@ -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> {

View File

@ -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.
///

View File

@ -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.
///

View File

@ -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

View File

@ -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))
}

View File

@ -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

View File

@ -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");

View File

@ -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,

View File

@ -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);

View File

@ -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,

View File

@ -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");

View File

@ -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"));
}
}

View File

@ -474,6 +474,7 @@ mod tests {
timing: None,
claims: None,
deprecated_usages: vec![],
verify: None,
};
let output = formatter.format(&result);

View File

@ -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"));
}
}

View File

@ -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);

View File

@ -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"));

View File

@ -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.
///

View File

@ -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,
})
}

View File

@ -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();

View File

@ -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;

View File

@ -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);
}

View File

@ -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());

View File

@ -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));
}
}

View File

@ -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?;

View File

@ -62,6 +62,7 @@ pub async fn scan(
file_source: aphoria::FileSource::All,
benchmark: false,
show_claims: false,
strict: false,
};
// Execute scan

View File

@ -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

View File

@ -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)