# Policy Alias Implementation Guide

**Related:** [Concept Matching Analysis](./concept-matching-analysis.md)
**Status:** Implementation Ready
**Estimated Effort:** 2-3 days

---

## Implementation Phases

### Phase 1: Schema Extension (Day 1, Morning)

**Goal:** Add `PolicyAlias` type and extend `TrustPack`.

#### 1.1 Define PolicyAlias Type

**File:** `applications/aphoria/src/policy.rs`

```rust
/// Maps policy assertion paths to extractor output patterns.
///
/// Enables enterprise security teams to define standards using logical hierarchies
/// (e.g., "code://standards/tls/*") that match extractor output
/// (e.g., "code://rust/myapp/tls/*").
#[derive(Archive, Deserialize, Serialize, Debug, Clone)]
#[archive(check_bytes)]
pub struct PolicyAlias {
    /// The policy path used in assertions (e.g., "code://standards/tls/cert_verification").
    pub policy_path: String,

    /// Glob patterns that should resolve to this policy path.
    /// Supports '*' wildcard for single-segment match.
    ///
    /// Examples:
    /// - "code://rust/*/tls/cert_verification"  (matches any project)
    /// - "code://*/myapp/tls/cert_verification" (matches any language)
    /// - "code://rust/myapp/*/cert_verification" (matches any module)
    pub target_patterns: Vec<String>,
}
```

#### 1.2 Extend TrustPack

**File:** `applications/aphoria/src/policy.rs`

```rust
#[derive(Archive, Deserialize, Serialize, Debug, Clone)]
#[archive(check_bytes)]
pub struct TrustPack {
    pub header: PackHeader,
    pub assertions: Vec<Assertion>,
    pub aliases: Vec<ConceptAlias>,

    /// Policy-level aliases for matching extractor output to policy paths.
    /// Optional: Empty vec = no policy aliases (backward compatible).
    pub policy_aliases: Vec<PolicyAlias>,

    pub signature: [u8; 64],
}
```

#### 1.3 Update TrustPack Constructor

```rust
impl TrustPack {
    pub fn new(
        name: String,
        version: String,
        assertions: Vec<Assertion>,
        aliases: Vec<ConceptAlias>,
        policy_aliases: Vec<PolicyAlias>,  // NEW
        signing_key: &SigningKey,
    ) -> Result<Self, AphoriaError> {
        // ... existing timestamp/issuer logic

        let temp_pack = TrustPack {
            header: header.clone(),
            assertions: assertions.clone(),
            aliases: aliases.clone(),
            policy_aliases: policy_aliases.clone(),  // NEW
            signature: [0u8; 64],
        };

        // ... existing signing logic

        Ok(TrustPack {
            header,
            assertions,
            aliases,
            policy_aliases,  // NEW
            signature
        })
    }
}
```

**Testing:**
```bash
cargo test -p aphoria policy::tests::trust_pack_with_policy_aliases
```

---

### Phase 2: Pattern Matching (Day 1, Afternoon)

**Goal:** Implement glob-based pattern matching for policy aliases.

#### 2.1 Add Glob Matching Function

**File:** `applications/aphoria/src/episteme/concept_index.rs`

```rust
/// Check if a subject matches a glob pattern with '*' wildcard.
///
/// Supports single-segment wildcards only (not recursive `**`).
///
/// # Examples
/// ```
/// assert!(glob_match("code://rust/*/tls/cert_verification", "code://rust/myapp/tls/cert_verification"));
/// assert!(glob_match("code://*/myapp/tls/*", "code://python/myapp/tls/min_version"));
/// assert!(!glob_match("code://rust/*/tls", "code://go/myapp/tls/cert_verification"));
/// ```
fn glob_match(pattern: &str, subject: &str) -> bool {
    let pattern_parts: Vec<&str> = pattern.split('/').collect();
    let subject_parts: Vec<&str> = subject.split('/').collect();

    if pattern_parts.len() != subject_parts.len() {
        return false;
    }

    pattern_parts.iter().zip(subject_parts.iter()).all(|(p, s)| {
        *p == "*" || *p == *s
    })
}
```

#### 2.2 Extend ConceptIndex Lookup

**File:** `applications/aphoria/src/episteme/concept_index.rs`

```rust
use crate::policy::PolicyAlias;

impl ConceptIndex {
    /// Look up assertions with policy alias fallback.
    ///
    /// Algorithm:
    /// 1. Try direct tail-path match (existing behavior)
    /// 2. If no match, try each policy alias pattern
    /// 3. If pattern matches subject, lookup using policy_path
    /// 4. Return first match (policy aliases processed in order)
    pub fn lookup_with_policy_aliases(
        &self,
        subject: &str,
        predicate: &str,
        policy_aliases: &[PolicyAlias],
    ) -> Option<&Vec<Assertion>> {
        // Try direct tail-path match first (fast path)
        if let Some(result) = self.lookup(subject, predicate) {
            return Some(result);
        }

        // Try policy alias patterns (fallback)
        for alias in policy_aliases {
            // Check if any pattern matches the subject
            let pattern_matches = alias.target_patterns.iter().any(|pattern| {
                glob_match(pattern, subject)
            });

            if pattern_matches {
                // Look up using the policy path instead
                if let Some(result) = self.lookup(&alias.policy_path, predicate) {
                    return Some(result);
                }
            }
        }

        None
    }
}
```

**Testing:**
```bash
cargo test -p aphoria episteme::concept_index::tests::policy_alias_matching
```

**Test Cases:**
```rust
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_glob_match_wildcard() {
        assert!(glob_match("code://rust/*/tls", "code://rust/myapp/tls"));
        assert!(glob_match("code://*/myapp/tls", "code://rust/myapp/tls"));
        assert!(!glob_match("code://rust/*/tls", "code://go/myapp/tls"));
    }

    #[test]
    fn test_policy_alias_lookup() {
        // Policy assertion: "code://standards/tls/cert_verification"
        let policy_assertion = Assertion {
            subject: "code://standards/tls/cert_verification".to_string(),
            predicate: "enabled".to_string(),
            object: ObjectValue::Boolean(true),
            // ... other fields
        };

        let index = ConceptIndex::build(&[policy_assertion]);

        let alias = PolicyAlias {
            policy_path: "code://standards/tls/cert_verification".to_string(),
            target_patterns: vec![
                "code://rust/*/tls/cert_verification".to_string(),
            ],
        };

        // Should match via alias
        let result = index.lookup_with_policy_aliases(
            "code://rust/myapp/tls/cert_verification",
            "enabled",
            &[alias],
        );

        assert!(result.is_some());
        assert_eq!(result.unwrap().len(), 1);
    }
}
```

---

### Phase 3: Integration (Day 2, Morning)

**Goal:** Wire policy aliases into scan flow.

#### 3.1 Pass Policy Aliases to ConceptIndex

**File:** `applications/aphoria/src/scan.rs`

```rust
async fn check_conflicts_persistent(
    all_claims: &[ExtractedClaim],
    project_root: &Path,
    config: &AphoriaConfig,
    sync: bool,
) -> Result<ConflictCheckResult, AphoriaError> {
    // ... existing setup

    // Load policies (Trust Packs)
    let policy_manager = PolicyManager::new(&config.corpus.cache_dir);
    let policies = policy_manager.load_policies(&config.policies)?;

    // Extract policy aliases from all Trust Packs
    let policy_aliases: Vec<PolicyAlias> = policies
        .iter()
        .flat_map(|pack| &pack.policy_aliases)
        .cloned()
        .collect();

    info!(
        policy_alias_count = policy_aliases.len(),
        "Loaded policy aliases from Trust Packs"
    );

    // Build corpus and index
    let mut corpus = create_authoritative_corpus(&signing_key);
    let imported_assertions = episteme.fetch_authoritative_assertions().await?;
    corpus.extend(imported_assertions);
    let index = ConceptIndex::build(&corpus);

    // Pass aliases to conflict checker
    let conflicts = episteme
        .check_conflicts_with_aliases(all_claims, config, &index, &policy_aliases)
        .await?;

    // ... rest of function
}
```

#### 3.2 Extend LocalEpisteme::check_conflicts

**File:** `applications/aphoria/src/episteme/local.rs`

```rust
use crate::policy::PolicyAlias;

impl LocalEpisteme {
    /// Check conflicts with policy alias support.
    pub async fn check_conflicts_with_aliases(
        &self,
        claims: &[ExtractedClaim],
        config: &AphoriaConfig,
        index: &ConceptIndex,
        policy_aliases: &[PolicyAlias],
    ) -> Result<Vec<ConflictResult>, AphoriaError> {
        // ... existing setup (fetch acks, etc.)

        for claim in claims {
            // Use extended lookup with policy aliases
            let auth_assertions = match index.lookup_with_policy_aliases(
                &claim.concept_path,
                &claim.predicate,
                policy_aliases,
            ) {
                Some(assertions) => assertions,
                None => continue,
            };

            // ... rest of conflict detection logic (unchanged)
        }

        // ... return results
    }
}
```

**Backward Compatibility:**
```rust
// Keep existing method for ephemeral mode
pub async fn check_conflicts(
    &self,
    claims: &[ExtractedClaim],
    config: &AphoriaConfig,
    index: &ConceptIndex,
) -> Result<Vec<ConflictResult>, AphoriaError> {
    // Delegate to new method with empty aliases
    self.check_conflicts_with_aliases(claims, config, index, &[]).await
}
```

#### 3.3 Update EphemeralDetector

**File:** `applications/aphoria/src/episteme/ephemeral.rs`

```rust
pub struct EphemeralDetector {
    // ... existing fields
    policy_aliases: Vec<PolicyAlias>,  // NEW
}

impl EphemeralDetector {
    pub fn ingest_policies(&mut self, policies: &[TrustPack]) {
        for pack in policies {
            // Ingest assertions (existing)
            self.ingest_authoritative(&pack.assertions);

            // Ingest policy aliases (NEW)
            self.policy_aliases.extend(pack.policy_aliases.clone());
        }
    }

    pub fn check_conflicts(
        &self,
        claims: &[ExtractedClaim],
        config: &AphoriaConfig,
    ) -> Vec<ConflictResult> {
        let index = ConceptIndex::build(&self.corpus);

        // Use policy aliases in lookup
        for claim in claims {
            let auth_assertions = index.lookup_with_policy_aliases(
                &claim.concept_path,
                &claim.predicate,
                &self.policy_aliases,
            );
            // ... rest of conflict logic
        }
    }
}
```

---

### Phase 4: CLI Tooling (Day 2, Afternoon)

**Goal:** Enable security teams to create policy aliases easily.

#### 4.1 Add `policy add-alias` Command

**File:** `applications/aphoria/src/types/command.rs`

```rust
#[derive(Debug, Subcommand)]
pub enum PolicyCommand {
    // ... existing commands (export, import, list)

    /// Add a policy alias to a Trust Pack.
    ///
    /// Allows mapping extractor output patterns to policy assertion paths.
    #[command(name = "add-alias")]
    AddAlias {
        /// Path to the Trust Pack file.
        #[arg(short, long)]
        pack: PathBuf,

        /// Policy path (e.g., "code://standards/tls/cert_verification").
        #[arg(long)]
        policy_path: String,

        /// Target pattern (e.g., "code://rust/*/tls/cert_verification").
        /// Can be specified multiple times.
        #[arg(long = "target")]
        target_patterns: Vec<String>,

        /// Output path for updated pack (default: overwrite input).
        #[arg(short, long)]
        output: Option<PathBuf>,
    },
}
```

#### 4.2 Implement Handler

**File:** `applications/aphoria/src/policy_ops.rs`

```rust
use crate::policy::{PolicyAlias, TrustPack};

pub fn handle_policy_add_alias(
    pack_path: &Path,
    policy_path: String,
    target_patterns: Vec<String>,
    output_path: Option<&Path>,
    signing_key: &SigningKey,
) -> Result<(), AphoriaError> {
    // Load existing Trust Pack
    let mut pack = TrustPack::load(pack_path)?;

    info!(
        pack = %pack.header.name,
        version = %pack.header.version,
        "Loaded Trust Pack"
    );

    // Validate patterns
    for pattern in &target_patterns {
        if !is_valid_glob_pattern(pattern) {
            return Err(AphoriaError::Config(format!(
                "Invalid glob pattern: {}",
                pattern
            )));
        }
    }

    // Check if alias already exists (avoid duplicates)
    let exists = pack.policy_aliases.iter().any(|a| {
        a.policy_path == policy_path && a.target_patterns == target_patterns
    });

    if exists {
        info!("Policy alias already exists, skipping");
        return Ok(());
    }

    // Add new policy alias
    let alias = PolicyAlias { policy_path: policy_path.clone(), target_patterns };
    pack.policy_aliases.push(alias);

    // Re-sign the pack (required because we modified it)
    let new_pack = TrustPack::new(
        pack.header.name,
        pack.header.version,
        pack.assertions,
        pack.aliases,
        pack.policy_aliases,
        signing_key,
    )?;

    // Save to output path (or overwrite input)
    let save_path = output_path.unwrap_or(pack_path);
    new_pack.save(save_path)?;

    info!(
        policy_path,
        output = %save_path.display(),
        "Added policy alias to Trust Pack"
    );

    Ok(())
}

fn is_valid_glob_pattern(pattern: &str) -> bool {
    // Check for balanced segments (no double slashes, etc.)
    !pattern.is_empty()
        && !pattern.contains("//")
        && pattern.split('/').all(|seg| !seg.is_empty() || seg == "*")
}
```

#### 4.3 Wire into CLI

**File:** `applications/aphoria/src/handlers.rs`

```rust
PolicyCommand::AddAlias { pack, policy_path, target_patterns, output } => {
    let signing_key = load_or_generate_key(&project_root)?;
    crate::policy_ops::handle_policy_add_alias(
        &pack,
        policy_path,
        target_patterns,
        output.as_deref(),
        &signing_key,
    )?;
}
```

**Example Usage:**
```bash
# Security team workflow
aphoria policy export security-standards-v1.0.pack

aphoria policy add-alias \
  --pack security-standards-v1.0.pack \
  --policy-path "code://standards/tls/cert_verification" \
  --target "code://rust/*/tls/cert_verification" \
  --target "code://go/*/tls/cert_verification" \
  --target "code://python/*/tls/cert_verification"

# Dev team imports and scans
aphoria scan --mode persistent
```

---

### Phase 5: Documentation & Testing (Day 3)

#### 5.1 Update User Guide

**File:** `applications/aphoria/docs/guides/federating-truth.md`

Add section:

```markdown
### Policy Aliases

When security teams create standards using logical hierarchies (e.g., `code://standards/*`),
these may not match extractor output (e.g., `code://rust/myapp/*`).

Policy aliases bridge this gap:

```bash
# Add alias to Trust Pack
aphoria policy add-alias \
  --pack security.pack \
  --policy-path "code://standards/tls/cert_verification" \
  --target "code://rust/*/tls/cert_verification"
```

Now scans will match code extractors against the policy path.
```

#### 5.2 Write UAT Scenario

**File:** `applications/aphoria/uat/2026-02-05-policy-alias-uat.md`

```markdown
# UAT: Policy Alias Matching

## Scenario
Security team creates standard at `code://standards/tls/cert_verification`.
Dev team code has `code://rust/myapp/tls/cert_verification`.

## Setup
1. Create security-team project with blessed assertion
2. Export Trust Pack with policy alias
3. Create dev-team project with violating code
4. Import Trust Pack
5. Scan

## Expected Outcome
- Scan detects conflict via policy alias
- Report shows policy source
- Exit code = 1 (BLOCK)
```

#### 5.3 Integration Tests

**File:** `applications/aphoria/src/tests/policy_alias_integration.rs`

```rust
#[tokio::test]
async fn test_policy_alias_matching_integration() {
    // 1. Create policy assertion
    let policy_assertion = create_test_assertion(
        "code://standards/tls/cert_verification",
        "enabled",
        ObjectValue::Boolean(true),
        SourceClass::Expert,
    );

    // 2. Create policy alias
    let alias = PolicyAlias {
        policy_path: "code://standards/tls/cert_verification".to_string(),
        target_patterns: vec![
            "code://rust/*/tls/cert_verification".to_string(),
        ],
    };

    // 3. Build Trust Pack
    let key = SigningKey::generate(&mut rand::thread_rng());
    let pack = TrustPack::new(
        "Test Policy".to_string(),
        "1.0.0".to_string(),
        vec![policy_assertion],
        vec![],
        vec![alias],
        &key,
    ).unwrap();

    // 4. Simulate scan with code claim
    let code_claim = ExtractedClaim {
        concept_path: "code://rust/myapp/tls/cert_verification".to_string(),
        predicate: "enabled".to_string(),
        value: ObjectValue::Boolean(false),  // CONFLICT
        // ... other fields
    };

    // 5. Check conflicts
    let corpus = vec![pack.assertions[0].clone()];
    let index = ConceptIndex::build(&corpus);
    let result = index.lookup_with_policy_aliases(
        &code_claim.concept_path,
        &code_claim.predicate,
        &pack.policy_aliases,
    );

    // 6. Assert match
    assert!(result.is_some(), "Policy alias should match");
    assert_eq!(result.unwrap()[0].object, ObjectValue::Boolean(true));
}
```

---

## Migration & Rollout

### Backward Compatibility

✅ **Existing Trust Packs:**
- `policy_aliases` field is optional (deserializes as empty vec)
- No re-signing required unless adding aliases

✅ **Existing Scans:**
- Empty aliases vec = current behavior
- No performance impact (skips alias loop)

✅ **Existing CLI:**
- All existing commands work unchanged
- `policy add-alias` is additive

### Rollout Plan

**Week 1 (Dev):**
- [ ] Implement Phases 1-3
- [ ] Write unit tests
- [ ] Manual testing with UAT scenario

**Week 2 (Validation):**
- [ ] Implement Phase 4 (CLI)
- [ ] Write integration tests
- [ ] Performance benchmarks

**Week 3 (Docs & Release):**
- [ ] Update user documentation
- [ ] Write migration guide
- [ ] Release 0.2.0 with feature flag

**Week 4 (Enterprise Pilot):**
- [ ] Deploy to 2-3 enterprise teams
- [ ] Collect feedback
- [ ] Iterate on pattern syntax if needed

---

## Performance Considerations

### Lookup Complexity

**Direct tail-path:** O(1) hash lookup
**Policy alias:** O(P * A) where:
- P = patterns per alias
- A = number of aliases

**Mitigation:**
- Try direct lookup first (fast path)
- Only iterate aliases on miss
- Most scans will have < 10 aliases
- Pattern matching is simple string comparison

**Benchmark Target:** < 5% scan time increase

### Memory Overhead

**Per Trust Pack:**
- PolicyAlias: ~100 bytes
- 10 aliases: ~1 KB
- Negligible compared to corpus (MBs)

---

## Future Enhancements

### 1. Recursive Wildcards

**Current:** `code://rust/*/tls` (single segment)
**Future:** `code://rust/**/tls` (any depth)

**Implementation:** Use `globset` crate for full glob support.

### 2. Regex Patterns

**Current:** Glob wildcards
**Future:** Full regex support

```rust
pub enum PatternSyntax {
    Glob(String),
    Regex(String),
}
```

### 3. Alias Auto-Discovery

**During Scan:** Suggest aliases when tail-path matches but full path differs.

```rust
// In conflict detection
if tail_match && !full_match {
    warn!(
        "Potential alias needed: {} -> {}",
        claim.concept_path,
        assertion.subject
    );
}
```

### 4. Trust Pack Composition

**Idea:** Allow Trust Packs to "extend" other packs.

```rust
pub struct TrustPack {
    pub header: PackHeader,
    pub extends: Vec<String>,  // URLs of parent packs
    // ...
}
```

---

## Success Criteria

### Functional
- [ ] Security team can create policy at `code://standards/*`
- [ ] Dev team code at `code://rust/myapp/*` matches
- [ ] Conflicts detected and reported correctly
- [ ] Trust Pack signature verifies with aliases

### Performance
- [ ] Scan time increase < 5%
- [ ] Memory overhead < 10 KB per pack

### Usability
- [ ] `policy add-alias` command works intuitively
- [ ] Trust Pack import is automatic (no manual config)
- [ ] Error messages are clear (invalid patterns, etc.)

### Quality
- [ ] 100% test coverage on pattern matching
- [ ] Integration test covers full workflow
- [ ] UAT scenario passes

---

## Questions for Review

1. **Glob Syntax:** Single wildcard (`*`) sufficient, or support recursive (`**`)?
2. **Alias Priority:** First match wins, or most specific match?
3. **Validation:** Fail Trust Pack creation if pattern is invalid?
4. **Caching:** Cache pattern match results, or recompute each time?

---

**Ready to implement.** Feedback welcome before starting Phase 1.
