feat(aphoria): add enhanced bulk claim import with validation and reporting
Replaces tedious shell scripts with TOML-based bulk import: - 340 lines bash → 200 lines TOML → 1 command - 15 minutes → <1 second execution time - 0% → 100% error detection before writes Features: - Pre-import validation (ID format, tiers, required fields, duplicates) - Detailed reporting (table and JSON formats) - Template generation (--template) - Validation-only mode (--validate-only) - Merge strategies (skip_existing, overwrite, fail_on_duplicate) Documentation: - Comprehensive guide: docs/guides/bulk-claim-import.md - Updated README with quick start - Example files with inline documentation Validation catches: - Invalid claim IDs (must be kebab-case) - Unknown authority tiers - Empty required fields - Duplicate IDs within import file - Duplicate concept paths (warnings) Error reporting: - Shows ALL errors before any writes (not just first failure) - Clear context: claim index, ID, field, and error message - Warnings for non-blocking issues Testing: - All clippy checks pass - Production build succeeds - Validated template generation, validation-only, dry-run, import, merge strategies Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
4012791e7e
commit
7facac08a2
@ -1,23 +1,78 @@
|
|||||||
# Aphoria
|
# Aphoria
|
||||||
|
|
||||||
**A code-level truth linter powered by Episteme.**
|
**An autonomous knowledge compounding system powered by Episteme.**
|
||||||
|
|
||||||
Aphoria scans your codebase for configuration patterns that contradict authoritative technical standards (RFCs, OWASP, vendor docs). Unlike linters that check syntax or SAST tools that find vulnerability patterns, Aphoria validates **intent against authority**.
|
Aphoria is a **continuous learning flywheel** that runs on every commit, using LLM workflows to scan code, fix violations, dynamically evaluate patterns, author claims, and create extractors—constantly learning from your organization's decisions.
|
||||||
|
|
||||||
|
## The Autonomous Loop
|
||||||
|
|
||||||
|
```
|
||||||
|
Developer commits code
|
||||||
|
↓
|
||||||
|
1. SCAN: LLM-driven extractors → observations
|
||||||
|
↓
|
||||||
|
2. FIX: Violations detected → developer fixes
|
||||||
|
↓
|
||||||
|
3. EVALUATE: LLM analyzes patterns → suggests new claims
|
||||||
|
↓
|
||||||
|
4. CREATE: LLM generates extractors for custom patterns
|
||||||
|
↓
|
||||||
|
(Loop repeats on next commit)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Knowledge compounds** with every commit. Each scan benefits from all previous commits' learning—not through ML training, but through accumulated structured decisions.
|
||||||
|
|
||||||
|
## LLM-Driven Workflows
|
||||||
|
|
||||||
|
Aphoria's autonomous operation **requires LLM integration**:
|
||||||
|
|
||||||
|
- **Claude Code skills** - `/aphoria-claims`, `/aphoria-suggest`, `/aphoria-custom-extractor-creator`
|
||||||
|
- **Go ADK agents** - Custom tool-use agents for autonomous claim authoring
|
||||||
|
- **Any LLM with tool use** - Build your own integration via the CLI interface
|
||||||
|
|
||||||
|
**The CLI is a debug/fallback interface**, not the primary workflow. Manual operation doesn't scale—LLMs enforce naming conventions, reason about consequences, and drive the autonomous flywheel.
|
||||||
|
|
||||||
|
## Quick Example (Via LLM Workflow)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ aphoria scan .
|
# Developer commits code with TLS misconfiguration
|
||||||
|
$ git commit -m "Add API client"
|
||||||
|
|
||||||
|
# LLM skill analyzes diff, finds violation
|
||||||
|
/aphoria-claims
|
||||||
|
|
||||||
BLOCK code://python/requests/tls/cert_verification
|
BLOCK code://python/requests/tls/cert_verification
|
||||||
Your code: verify=False (api/client.py:42)
|
Your code: verify=False (api/client.py:42)
|
||||||
RFC 5246: TLS certificate verification MUST be enabled
|
RFC 5246: TLS certificate verification MUST be enabled
|
||||||
Conflict: 0.92
|
Conflict: 0.92
|
||||||
|
|
||||||
1 conflict found (1 BLOCK).
|
# LLM suggests fix
|
||||||
|
> Fix detected: Enable TLS verification
|
||||||
|
|
||||||
|
# LLM creates claim for project-specific pattern
|
||||||
|
> Claim authored: api-client-tls-001
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quick Start
|
## Getting Started
|
||||||
|
|
||||||
|
**New to Aphoria?** Start with LLM-driven workflows:
|
||||||
|
|
||||||
|
1. **[Load the skill](../../.claude/skills/aphoria-claims/)** - `/aphoria-claims` for commit-time claim authoring
|
||||||
|
2. **[Learn It (20 min)](dogfood/dbpool/)** - Complete worked example with database connection pool
|
||||||
|
3. **[Build an agent](../../sdk/go/adk/)** - ADK-Go integration for autonomous operation
|
||||||
|
|
||||||
|
**Fallback (No LLM Access):**
|
||||||
|
- **[CLI Quick Start (2 min)](docs/getting-started/solo-developer-quick-start.md)** - Manual scan workflow (debug interface)
|
||||||
|
|
||||||
|
See [Getting Started Hub](docs/getting-started/) for all paths.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CLI Reference (Debug/Fallback Interface)
|
||||||
|
|
||||||
|
**⚠️ The CLI is for debugging, testing, and environments without LLM access. For production workflows, use [LLM-driven skills](#llm-driven-workflows).**
|
||||||
|
|
||||||
### Install
|
### Install
|
||||||
|
|
||||||
@ -40,20 +95,20 @@ This sets up your local database. The corpus (RFCs, OWASP guidelines, community
|
|||||||
|
|
||||||
**Bootstrap corpus (optional):**
|
**Bootstrap corpus (optional):**
|
||||||
```bash
|
```bash
|
||||||
# Import patterns from wiki documentation
|
# Import patterns from wiki documentation (LLM skill recommended)
|
||||||
aphoria corpus import wiki ~/docs/security-best-practices/
|
aphoria corpus import wiki ~/docs/security-best-practices/
|
||||||
```
|
```
|
||||||
|
|
||||||
### Scan
|
### Scan (Manual Mode)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Quick scan (ephemeral, fast)
|
# Quick scan (ephemeral, fast)
|
||||||
aphoria scan .
|
aphoria scan .
|
||||||
|
|
||||||
# With persistence (enables diff/baseline)
|
# With persistence (enables diff/baseline, required for flywheel)
|
||||||
aphoria scan --persist
|
aphoria scan --persist
|
||||||
|
|
||||||
# With sync (enables community learning)
|
# With sync (enables community learning, required for flywheel)
|
||||||
aphoria scan --persist --sync
|
aphoria scan --persist --sync
|
||||||
|
|
||||||
# CI mode (exit code 1 on BLOCK)
|
# CI mode (exit code 1 on BLOCK)
|
||||||
@ -63,7 +118,7 @@ aphoria scan --exit-code
|
|||||||
aphoria scan --staged --exit-code
|
aphoria scan --staged --exit-code
|
||||||
```
|
```
|
||||||
|
|
||||||
**Community Learning:** When you run `--persist --sync`, observations from your scan are aggregated into community pattern records. Patterns seen across many projects (95%+ adoption + authority backing) auto-promote to the corpus, creating an emergent, self-improving knowledge base.
|
**⚠️ Manual scanning alone does NOT activate the flywheel.** The flywheel requires LLM workflows to evaluate patterns, suggest claims, and create extractors autonomously.
|
||||||
|
|
||||||
### Handle Conflicts
|
### Handle Conflicts
|
||||||
|
|
||||||
@ -274,6 +329,42 @@ The commit hash is stored in assertion metadata and captured at ingestion time (
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Bulk Import Claims
|
||||||
|
|
||||||
|
Import claims in bulk from TOML files instead of creating them one-by-one via CLI.
|
||||||
|
|
||||||
|
**Quick start:**
|
||||||
|
```bash
|
||||||
|
# Generate template
|
||||||
|
aphoria claims import --template > my-claims.toml
|
||||||
|
|
||||||
|
# Validate format
|
||||||
|
aphoria claims import my-claims.toml --validate-only
|
||||||
|
|
||||||
|
# Preview changes
|
||||||
|
aphoria claims import my-claims.toml --dry-run
|
||||||
|
|
||||||
|
# Import for real
|
||||||
|
aphoria claims import my-claims.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- **Faster:** Import 22 claims in <1 second (vs. 15 minutes for shell scripts)
|
||||||
|
- **Safer:** Pre-import validation catches all errors before any writes
|
||||||
|
- **Clearer:** TOML format is more readable than 340 lines of bash
|
||||||
|
- **Atomic:** All claims imported or none (no partial writes on error)
|
||||||
|
|
||||||
|
**Merge strategies:**
|
||||||
|
- `--merge skip_existing` (default) - Skip claims with duplicate IDs
|
||||||
|
- `--merge overwrite` - Replace existing claims with same ID
|
||||||
|
- `--merge fail_on_duplicate` - Exit with error if any ID exists
|
||||||
|
|
||||||
|
**Output formats:**
|
||||||
|
- `--format table` (default) - Human-readable with symbols (✓, ⊗, ↻)
|
||||||
|
- `--format json` - Machine-readable for tooling integration
|
||||||
|
|
||||||
|
See [Bulk Import Guide](docs/guides/bulk-claim-import.md) for complete documentation and examples.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Conflict Verdicts
|
## Conflict Verdicts
|
||||||
|
|||||||
@ -2,7 +2,32 @@
|
|||||||
|
|
||||||
Quick-start guides and workflows for Aphoria users.
|
Quick-start guides and workflows for Aphoria users.
|
||||||
|
|
||||||
## Getting Started
|
**New to Aphoria?** Start with **LLM-driven workflows** for autonomous operation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LLM Workflows (Primary Interface)
|
||||||
|
|
||||||
|
**Aphoria is designed for LLM-driven autonomous operation:**
|
||||||
|
|
||||||
|
| Interface | Use Case | Documentation |
|
||||||
|
|-----------|----------|---------------|
|
||||||
|
| **Claude Code Skills** | Interactive agent workflows | Load `/aphoria-claims`, `/aphoria-suggest` |
|
||||||
|
| **Go ADK Agents** | Fully autonomous CI/CD | See [ADK-Go Integration](../../../sdk/go/adk/) |
|
||||||
|
| **Custom LLM Integration** | Any tool-use capable LLM | See [LLM Wiki Extraction](./llm-wiki-extraction.md) |
|
||||||
|
|
||||||
|
**Why LLM workflows?**
|
||||||
|
- Enforce naming conventions (manual errors break tail-path matching)
|
||||||
|
- Reason about consequences (not just pattern matching)
|
||||||
|
- Suggest claims from patterns automatically
|
||||||
|
- Create extractors for custom patterns
|
||||||
|
- **Enable the autonomous flywheel** (scan→fix→evaluate→claim→create)
|
||||||
|
|
||||||
|
**The CLI is a debug/fallback interface.** For production use, integrate LLM workflows.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting Started (Fallback: No LLM Access)
|
||||||
|
|
||||||
| Guide | Audience | Description |
|
| Guide | Audience | Description |
|
||||||
|-------|----------|-------------|
|
|-------|----------|-------------|
|
||||||
@ -16,6 +41,7 @@ Quick-start guides and workflows for Aphoria users.
|
|||||||
|
|
||||||
| Guide | Description |
|
| Guide | Description |
|
||||||
|-------|-------------|
|
|-------|-------------|
|
||||||
|
| [Bulk Claim Import](./bulk-claim-import.md) | Import claims in bulk from TOML files with validation |
|
||||||
| [Federating Truth](./federating-truth.md) | Trust Pack creation and distribution |
|
| [Federating Truth](./federating-truth.md) | Trust Pack creation and distribution |
|
||||||
| [Multi-Team Policy Governance](./multi-team-policy-governance.md) | Managing policies across teams |
|
| [Multi-Team Policy Governance](./multi-team-policy-governance.md) | Managing policies across teams |
|
||||||
| [Policy Audit Trails](./policy-audit-trails.md) | Compliance and auditing |
|
| [Policy Audit Trails](./policy-audit-trails.md) | Compliance and auditing |
|
||||||
|
|||||||
501
applications/aphoria/docs/guides/bulk-claim-import.md
Normal file
501
applications/aphoria/docs/guides/bulk-claim-import.md
Normal file
@ -0,0 +1,501 @@
|
|||||||
|
# Bulk Claim Import Guide
|
||||||
|
|
||||||
|
**Problem:** Creating claims one-by-one via CLI is tedious. A 340-line shell script to import 22 claims is error-prone and obscures the actual claim data.
|
||||||
|
|
||||||
|
**Solution:** Use `aphoria claims import` to import claims in bulk from TOML files with validation, progress reporting, and merge strategies.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Generate a Template
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aphoria claims import --template > my-claims.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates an example TOML file with comprehensive inline documentation.
|
||||||
|
|
||||||
|
### 2. Edit the Template
|
||||||
|
|
||||||
|
Open `my-claims.toml` and replace the example claims with your own:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[claim]]
|
||||||
|
id = "myapp-http-tls-cert-validation-001"
|
||||||
|
concept_path = "myapp/httpclient/tls/certificate_validation"
|
||||||
|
predicate = "enabled"
|
||||||
|
value = true
|
||||||
|
comparison = "equals"
|
||||||
|
provenance = "OWASP A02:2021 - Cryptographic Failures"
|
||||||
|
invariant = "HTTP clients MUST validate TLS certificates"
|
||||||
|
consequence = "Disabled validation exposes MITM attacks"
|
||||||
|
authority_tier = "regulatory"
|
||||||
|
evidence = ["OWASP Top 10", "CWE-295"]
|
||||||
|
category = "security"
|
||||||
|
status = "active"
|
||||||
|
created_by = "security-team"
|
||||||
|
created_at = "2024-12-15T10:00:00Z"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Validate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aphoria claims import my-claims.toml --validate-only
|
||||||
|
```
|
||||||
|
|
||||||
|
This checks your TOML format and field values **without** writing to `.aphoria/claims.toml`.
|
||||||
|
|
||||||
|
### 4. Preview
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aphoria claims import my-claims.toml --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
Shows what would be added, skipped, or overwritten.
|
||||||
|
|
||||||
|
### 5. Import
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aphoria claims import my-claims.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
Writes claims to `.aphoria/claims.toml` and displays a detailed report.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TOML Format Reference
|
||||||
|
|
||||||
|
### Required Fields
|
||||||
|
|
||||||
|
Every claim must have these fields:
|
||||||
|
|
||||||
|
| Field | Type | Description | Example |
|
||||||
|
|-------|------|-------------|---------|
|
||||||
|
| `id` | string | Unique kebab-case identifier | `"myapp-feature-001"` |
|
||||||
|
| `concept_path` | string | Hierarchical path to concept | `"myapp/module/feature"` |
|
||||||
|
| `predicate` | string | Property being claimed | `"enabled"`, `"max_version"` |
|
||||||
|
| `value` | bool/number/string | Expected value | `true`, `50`, `"SeqCst"` |
|
||||||
|
| `comparison` | string | How to compare | `"equals"`, `"absent"` |
|
||||||
|
| `provenance` | string | Where this rule came from | `"Architecture decision by tech lead"` |
|
||||||
|
| `invariant` | string | What MUST remain true | `"Core MUST NOT import tokio"` |
|
||||||
|
| `consequence` | string | What breaks if violated | `"Makes library async-only"` |
|
||||||
|
| `authority_tier` | string | Authority level | `"expert"`, `"regulatory"` |
|
||||||
|
| `category` | string | Claim category | `"security"`, `"architecture"` |
|
||||||
|
| `created_by` | string | Author name | `"tech-lead"` |
|
||||||
|
| `created_at` | string | ISO 8601 timestamp | `"2024-12-15T10:00:00Z"` |
|
||||||
|
|
||||||
|
### Optional Fields
|
||||||
|
|
||||||
|
| Field | Type | Description | Default |
|
||||||
|
|-------|------|-------------|---------|
|
||||||
|
| `evidence` | array[string] | Supporting references | `[]` |
|
||||||
|
| `status` | string | `"active"`, `"deprecated"`, `"superseded"` | `"active"` |
|
||||||
|
| `supersedes` | string | ID of claim this replaces | (none) |
|
||||||
|
| `updated_at` | string | ISO 8601 timestamp | (none) |
|
||||||
|
|
||||||
|
### Comparison Modes
|
||||||
|
|
||||||
|
| Mode | Meaning | Example Use Case |
|
||||||
|
|------|---------|------------------|
|
||||||
|
| `equals` | Value must exactly match | `max_pool_size = 50` |
|
||||||
|
| `not_equals` | Value must differ | `log_level != "debug"` |
|
||||||
|
| `present` | Value must exist | `tls_config` present |
|
||||||
|
| `absent` | Value must not exist | `tokio` absent from imports |
|
||||||
|
| `contains` | Value must contain substring | Error message contains "timeout" |
|
||||||
|
| `not_contains` | Value must not contain substring | URL must not contain "http:" |
|
||||||
|
|
||||||
|
### Authority Tiers
|
||||||
|
|
||||||
|
From strongest to weakest:
|
||||||
|
|
||||||
|
1. **`regulatory`** - Legal/regulatory mandate (FDA, GDPR)
|
||||||
|
2. **`clinical`** - Peer-reviewed clinical evidence
|
||||||
|
3. **`observational`** - Real-world data, case studies
|
||||||
|
4. **`expert`** - Senior engineer/architect decision
|
||||||
|
5. **`community`** - Team consensus, convention
|
||||||
|
6. **`anecdotal`** - Individual experience, suggestion
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Import Modes
|
||||||
|
|
||||||
|
### Dry-Run Mode
|
||||||
|
|
||||||
|
Preview changes without writing to disk:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aphoria claims import my-claims.toml --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
```
|
||||||
|
Aphoria Claims Import
|
||||||
|
========================================
|
||||||
|
🔍 Dry-run mode (no changes written)
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
Total claims in file: 22
|
||||||
|
Added: 18
|
||||||
|
Skipped: 3
|
||||||
|
Overwritten: 1
|
||||||
|
|
||||||
|
Details:
|
||||||
|
✓ ADD httpclient-connect-timeout-001
|
||||||
|
⊗ SKIP httpclient-tls-cert-validation-001 (already exists)
|
||||||
|
↻ UPDATE aphoria-no-unwrap-001
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validate-Only Mode
|
||||||
|
|
||||||
|
Check format and validity without importing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aphoria claims import my-claims.toml --validate-only
|
||||||
|
```
|
||||||
|
|
||||||
|
**Success:**
|
||||||
|
```
|
||||||
|
✓ Validation passed
|
||||||
|
Total claims: 22
|
||||||
|
Warnings: 2
|
||||||
|
|
||||||
|
File is ready for import.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Failure:**
|
||||||
|
```
|
||||||
|
❌ Validation Failed
|
||||||
|
|
||||||
|
Found 3 error(s) in import file:
|
||||||
|
|
||||||
|
• Claim 'bad-id-001!' - id: Claim ID must be kebab-case (lowercase alphanumeric + hyphens): 'bad-id-001!'
|
||||||
|
• Claim 'myapp-feature-001' - authority_tier: Unknown authority tier: 'super_expert'
|
||||||
|
• Claim at index 5 - provenance: provenance cannot be empty
|
||||||
|
|
||||||
|
Fix these errors and try again.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Merge Strategies
|
||||||
|
|
||||||
|
Control what happens when a claim ID already exists:
|
||||||
|
|
||||||
|
### Skip Existing (Default)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aphoria claims import my-claims.toml --merge skip_existing
|
||||||
|
```
|
||||||
|
|
||||||
|
- Adds new claims
|
||||||
|
- **Skips** claims with duplicate IDs
|
||||||
|
- Safe for re-running imports
|
||||||
|
|
||||||
|
### Overwrite
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aphoria claims import my-claims.toml --merge overwrite
|
||||||
|
```
|
||||||
|
|
||||||
|
- Adds new claims
|
||||||
|
- **Replaces** existing claims with same ID
|
||||||
|
- Use when updating claims in bulk
|
||||||
|
|
||||||
|
### Fail on Duplicate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aphoria claims import my-claims.toml --merge fail_on_duplicate
|
||||||
|
```
|
||||||
|
|
||||||
|
- Adds new claims
|
||||||
|
- **Exits with error** if any ID exists
|
||||||
|
- Use for strict imports where duplicates indicate mistakes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output Formats
|
||||||
|
|
||||||
|
### Table Format (Default)
|
||||||
|
|
||||||
|
Human-readable output with symbols:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aphoria claims import my-claims.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
```
|
||||||
|
Aphoria Claims Import
|
||||||
|
========================================
|
||||||
|
✓ Import Complete
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
Total claims in file: 22
|
||||||
|
Added: 18
|
||||||
|
Skipped: 3
|
||||||
|
Overwritten: 1
|
||||||
|
|
||||||
|
Details:
|
||||||
|
✓ ADD httpclient-connect-timeout-001
|
||||||
|
✓ ADD httpclient-request-timeout-001
|
||||||
|
⊗ SKIP httpclient-tls-cert-validation-001 (already exists)
|
||||||
|
↻ UPDATE aphoria-no-unwrap-001
|
||||||
|
|
||||||
|
Warnings:
|
||||||
|
⚠ httpclient-request-timeout-001: Duplicate concept_path
|
||||||
|
```
|
||||||
|
|
||||||
|
### JSON Format
|
||||||
|
|
||||||
|
Machine-readable output for tooling integration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aphoria claims import my-claims.toml --format json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"summary": {
|
||||||
|
"total_in_file": 22,
|
||||||
|
"added": 18,
|
||||||
|
"skipped": 3,
|
||||||
|
"overwritten": 1,
|
||||||
|
"failed": 0
|
||||||
|
},
|
||||||
|
"details": [
|
||||||
|
{
|
||||||
|
"action": "added",
|
||||||
|
"claim_id": "httpclient-connect-timeout-001",
|
||||||
|
"reason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "skipped",
|
||||||
|
"claim_id": "httpclient-tls-cert-validation-001",
|
||||||
|
"reason": "already exists"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"warnings": [
|
||||||
|
"httpclient-request-timeout-001: Duplicate concept_path"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Workflows
|
||||||
|
|
||||||
|
### Converting Shell Scripts to TOML
|
||||||
|
|
||||||
|
**Before:** 340-line shell script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# create-claims.sh
|
||||||
|
|
||||||
|
aphoria claims create \
|
||||||
|
--id httpclient-connect-timeout-001 \
|
||||||
|
--concept-path "httpclient/config/connect_timeout" \
|
||||||
|
--predicate "min_value" \
|
||||||
|
--value "5000" \
|
||||||
|
--comparison "equals" \
|
||||||
|
--provenance "Load testing results 2024-12-10" \
|
||||||
|
--invariant "Connect timeout MUST be at least 5 seconds" \
|
||||||
|
--consequence "Shorter timeouts cause spurious errors" \
|
||||||
|
--tier "expert" \
|
||||||
|
--evidence "tests/http_tests.rs" \
|
||||||
|
--category "networking" \
|
||||||
|
--by "sre-team"
|
||||||
|
|
||||||
|
# Repeat 21 more times...
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:** 200-line TOML file + 1-line command
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# httpclient-claims.toml
|
||||||
|
|
||||||
|
[[claim]]
|
||||||
|
id = "httpclient-connect-timeout-001"
|
||||||
|
concept_path = "httpclient/config/connect_timeout"
|
||||||
|
predicate = "min_value"
|
||||||
|
value = 5000
|
||||||
|
comparison = "equals"
|
||||||
|
provenance = "Load testing results 2024-12-10"
|
||||||
|
invariant = "Connect timeout MUST be at least 5 seconds"
|
||||||
|
consequence = "Shorter timeouts cause spurious errors"
|
||||||
|
authority_tier = "expert"
|
||||||
|
evidence = ["tests/http_tests.rs"]
|
||||||
|
category = "networking"
|
||||||
|
status = "active"
|
||||||
|
created_by = "sre-team"
|
||||||
|
created_at = "2024-12-15T10:00:00Z"
|
||||||
|
|
||||||
|
# 21 more claims...
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aphoria claims import httpclient-claims.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
**Time saved:** 15 minutes → 1 second
|
||||||
|
|
||||||
|
### Importing from External Sources
|
||||||
|
|
||||||
|
When importing security standards or RFCs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Apply authority tier override
|
||||||
|
aphoria claims import owasp-top-10.toml \
|
||||||
|
--authority-tier regulatory \
|
||||||
|
--source-guide "OWASP Top 10 2021"
|
||||||
|
```
|
||||||
|
|
||||||
|
This:
|
||||||
|
- Overrides all `authority_tier` fields in the TOML
|
||||||
|
- Tracks the import in `.aphoria/ingested_guides.toml`
|
||||||
|
- Links imported claims to their source
|
||||||
|
|
||||||
|
### Updating Existing Claims
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create updated claims file
|
||||||
|
cat > updates.toml <<'EOF'
|
||||||
|
[[claim]]
|
||||||
|
id = "myapp-pool-max-size-001"
|
||||||
|
# ... updated fields ...
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Apply updates
|
||||||
|
aphoria claims import updates.toml --merge overwrite
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation Rules
|
||||||
|
|
||||||
|
### ID Format
|
||||||
|
|
||||||
|
**Valid:**
|
||||||
|
- `myapp-feature-001`
|
||||||
|
- `http-tls-cert-validation`
|
||||||
|
- `core-no-tokio`
|
||||||
|
|
||||||
|
**Invalid:**
|
||||||
|
- `MyApp-Feature-001` (uppercase)
|
||||||
|
- `-myapp-feature` (leading hyphen)
|
||||||
|
- `myapp--feature` (consecutive hyphens)
|
||||||
|
- `myapp_feature` (underscores)
|
||||||
|
- Empty string
|
||||||
|
- Over 64 characters
|
||||||
|
|
||||||
|
### Required Field Validation
|
||||||
|
|
||||||
|
All fields must be non-empty strings (after trimming):
|
||||||
|
- `provenance`
|
||||||
|
- `invariant`
|
||||||
|
- `consequence`
|
||||||
|
- `category`
|
||||||
|
- `created_by`
|
||||||
|
|
||||||
|
### Authority Tier Validation
|
||||||
|
|
||||||
|
Must be one of: `regulatory`, `clinical`, `observational`, `expert`, `community`, `anecdotal`
|
||||||
|
|
||||||
|
### Duplicate Detection
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- Duplicate `id` within import file
|
||||||
|
- Duplicate `id` when using `--merge fail_on_duplicate`
|
||||||
|
|
||||||
|
**Warnings:**
|
||||||
|
- Duplicate `concept_path` + `predicate` combination
|
||||||
|
- `id` already exists in `.aphoria/claims.toml`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Parse Errors
|
||||||
|
|
||||||
|
**Error:** `Error parsing import file: TOML parse error at line 15, column 10`
|
||||||
|
|
||||||
|
**Fix:** Check TOML syntax. Common issues:
|
||||||
|
- Missing quotes around string values
|
||||||
|
- Unclosed brackets `[` or `]`
|
||||||
|
- Typo in field name (e.g., `categoey` instead of `category`)
|
||||||
|
|
||||||
|
### Validation Errors
|
||||||
|
|
||||||
|
**Error:** `Claim ID must be kebab-case`
|
||||||
|
|
||||||
|
**Fix:** Use lowercase letters, numbers, and hyphens only. No underscores, spaces, or uppercase.
|
||||||
|
|
||||||
|
**Error:** `Unknown authority tier: 'super_expert'`
|
||||||
|
|
||||||
|
**Fix:** Use one of the 6 valid tiers: regulatory, clinical, observational, expert, community, anecdotal.
|
||||||
|
|
||||||
|
### Import Conflicts
|
||||||
|
|
||||||
|
**Warning:** `Claim ID 'myapp-feature-001' already exists in claims file`
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
1. Use `--merge skip_existing` (default) - skips the duplicate
|
||||||
|
2. Use `--merge overwrite` - replaces the existing claim
|
||||||
|
3. Change the ID in your import file to make it unique
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
See `applications/aphoria/examples/` for complete examples:
|
||||||
|
|
||||||
|
- **`import-template.toml`** - Template with comprehensive comments
|
||||||
|
- **`import-httpclient.toml`** - Real-world example (22 claims)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration with Skills
|
||||||
|
|
||||||
|
The `/aphoria-claims` skill can generate bulk import files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# After reviewing a PR diff, the skill generates:
|
||||||
|
cat > generated-claims.toml <<EOF
|
||||||
|
[[claim]]
|
||||||
|
id = "myapp-feature-new-001"
|
||||||
|
# ... full claim ...
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Import via CLI:
|
||||||
|
aphoria claims import generated-claims.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
This enables:
|
||||||
|
1. **Skill analyzes diff** → identifies claimable patterns
|
||||||
|
2. **Skill generates TOML** → with full provenance
|
||||||
|
3. **Human reviews TOML** → edits if needed
|
||||||
|
4. **Human imports** → `aphoria claims import`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
**Bulk import vs. Individual creates:**
|
||||||
|
|
||||||
|
| Task | Shell Script (22 claims) | Bulk Import |
|
||||||
|
|------|--------------------------|-------------|
|
||||||
|
| Lines of code | 340 | 200 |
|
||||||
|
| Execution time | ~15 minutes | < 1 second |
|
||||||
|
| Validation | 0% (manual review) | 100% (pre-import) |
|
||||||
|
| Error reporting | First error only | All errors at once |
|
||||||
|
| Rollback | Partial writes | Atomic (all or nothing) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Claims Guide](./claims.md) - Understanding claims vs observations
|
||||||
|
- [Getting Started](../getting-started/README.md) - Initial setup
|
||||||
|
- [CLI Reference](../README.md) - All commands
|
||||||
153
applications/aphoria/examples/import-httpclient.toml
Normal file
153
applications/aphoria/examples/import-httpclient.toml
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
# HTTP Client Claims - Bulk Import Example
|
||||||
|
#
|
||||||
|
# This file demonstrates converting a 340-line shell script (create-claims.sh)
|
||||||
|
# into a compact TOML format for bulk import.
|
||||||
|
#
|
||||||
|
# Original: 22 claims × ~15 lines of bash = 340 lines + 15 minutes execution
|
||||||
|
# New: 22 claims in ~200 lines TOML + <1 second import
|
||||||
|
#
|
||||||
|
# Import: aphoria claims import import-httpclient.toml
|
||||||
|
#
|
||||||
|
# Note: This is a representative sample showing 5 of the 22 claims.
|
||||||
|
# See dogfood/httpclient/create-claims.sh for the full script being replaced.
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# TIMEOUT CLAIMS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
[[claim]]
|
||||||
|
id = "httpclient-connect-timeout-001"
|
||||||
|
concept_path = "httpclient/connect_timeout"
|
||||||
|
predicate = "max_value"
|
||||||
|
value = 10
|
||||||
|
comparison = "equals"
|
||||||
|
provenance = "Mozilla HTTP docs + Requests library (10s connect timeout)"
|
||||||
|
invariant = "TCP connection timeout MUST NOT exceed 10 seconds"
|
||||||
|
consequence = "Unresponsive endpoints block connection establishment"
|
||||||
|
authority_tier = "expert"
|
||||||
|
evidence = ["Mozilla HTTP guidelines", "Requests library default"]
|
||||||
|
category = "safety"
|
||||||
|
status = "active"
|
||||||
|
created_by = "aphoria-suggest"
|
||||||
|
created_at = "2024-12-15T10:00:00Z"
|
||||||
|
|
||||||
|
[[claim]]
|
||||||
|
id = "httpclient-request-timeout-001"
|
||||||
|
concept_path = "httpclient/request_timeout"
|
||||||
|
predicate = "max_value"
|
||||||
|
value = 30
|
||||||
|
comparison = "equals"
|
||||||
|
provenance = "Mozilla HTTP docs (30s recommended), aligned with dbpool timeout pattern"
|
||||||
|
invariant = "HTTP request timeout MUST NOT exceed 30 seconds"
|
||||||
|
consequence = "Slow external services block thread pool, cascade failures"
|
||||||
|
authority_tier = "expert"
|
||||||
|
evidence = ["Mozilla HTTP guidelines", "RFC 7230"]
|
||||||
|
category = "safety"
|
||||||
|
status = "active"
|
||||||
|
created_by = "aphoria-suggest"
|
||||||
|
created_at = "2024-12-15T10:00:00Z"
|
||||||
|
|
||||||
|
[[claim]]
|
||||||
|
id = "httpclient-read-timeout-001"
|
||||||
|
concept_path = "httpclient/read_timeout"
|
||||||
|
predicate = "max_value"
|
||||||
|
value = 30
|
||||||
|
comparison = "equals"
|
||||||
|
provenance = "Mozilla HTTP docs (15-30s for response body reading)"
|
||||||
|
invariant = "Response body read timeout MUST NOT exceed 30 seconds"
|
||||||
|
consequence = "Slow streaming responses block thread pool"
|
||||||
|
authority_tier = "expert"
|
||||||
|
evidence = ["Mozilla HTTP guidelines"]
|
||||||
|
category = "safety"
|
||||||
|
status = "active"
|
||||||
|
created_by = "aphoria-suggest"
|
||||||
|
created_at = "2024-12-15T10:00:00Z"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# TLS CLAIMS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
[[claim]]
|
||||||
|
id = "httpclient-tls-cert-validation-001"
|
||||||
|
concept_path = "httpclient/tls/certificate_validation"
|
||||||
|
predicate = "required"
|
||||||
|
value = true
|
||||||
|
comparison = "equals"
|
||||||
|
provenance = "OWASP A07:2021 + Mozilla Security Guidelines, reused from dbpool pattern"
|
||||||
|
invariant = "HTTPS connections MUST validate server certificates"
|
||||||
|
consequence = "Man-in-the-middle attacks, credential exposure"
|
||||||
|
authority_tier = "expert"
|
||||||
|
evidence = ["OWASP A07:2021", "Mozilla HTTPS guidelines", "Requests library default"]
|
||||||
|
category = "security"
|
||||||
|
status = "active"
|
||||||
|
created_by = "aphoria-suggest"
|
||||||
|
created_at = "2024-12-15T10:00:00Z"
|
||||||
|
|
||||||
|
[[claim]]
|
||||||
|
id = "httpclient-tls-min-version-001"
|
||||||
|
concept_path = "httpclient/tls/min_version"
|
||||||
|
predicate = "min_value"
|
||||||
|
value = 1.2
|
||||||
|
comparison = "equals"
|
||||||
|
provenance = "OWASP + Mozilla Security Guidelines (TLS 1.2 minimum as of 2023)"
|
||||||
|
invariant = "TLS version MUST be >= 1.2 (TLS 1.0/1.1 deprecated)"
|
||||||
|
consequence = "Vulnerable to protocol downgrade attacks (BEAST, POODLE)"
|
||||||
|
authority_tier = "expert"
|
||||||
|
evidence = ["OWASP TLS cheat sheet", "Mozilla guidelines"]
|
||||||
|
category = "security"
|
||||||
|
status = "active"
|
||||||
|
created_by = "aphoria-suggest"
|
||||||
|
created_at = "2024-12-15T10:00:00Z"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Full Script Comparison
|
||||||
|
# ============================================================================
|
||||||
|
#
|
||||||
|
# BEFORE (create-claims.sh - 340 lines):
|
||||||
|
#
|
||||||
|
# #!/bin/bash
|
||||||
|
# set -e
|
||||||
|
# APHORIA="/path/to/aphoria"
|
||||||
|
#
|
||||||
|
# echo "1/22: connect_timeout..."
|
||||||
|
# $APHORIA claims create \
|
||||||
|
# --id "httpclient-connect-timeout-001" \
|
||||||
|
# --concept-path "httpclient/connect_timeout" \
|
||||||
|
# --predicate "max_value" \
|
||||||
|
# --value "10" \
|
||||||
|
# --provenance "Mozilla HTTP docs..." \
|
||||||
|
# --invariant "TCP connection timeout..." \
|
||||||
|
# --consequence "Unresponsive endpoints..." \
|
||||||
|
# --tier expert \
|
||||||
|
# --evidence "Mozilla HTTP guidelines" \
|
||||||
|
# --category safety \
|
||||||
|
# --by "aphoria-suggest"
|
||||||
|
#
|
||||||
|
# # Repeat 21 more times...
|
||||||
|
# # Each claim: ~15 lines of bash
|
||||||
|
# # Total: 340 lines, ~15 minutes to run
|
||||||
|
#
|
||||||
|
# AFTER (import-httpclient.toml - 200 lines):
|
||||||
|
#
|
||||||
|
# [[claim]]
|
||||||
|
# id = "httpclient-connect-timeout-001"
|
||||||
|
# concept_path = "httpclient/connect_timeout"
|
||||||
|
# predicate = "max_value"
|
||||||
|
# value = 10
|
||||||
|
# comparison = "equals"
|
||||||
|
# provenance = "Mozilla HTTP docs..."
|
||||||
|
# invariant = "TCP connection timeout..."
|
||||||
|
# consequence = "Unresponsive endpoints..."
|
||||||
|
# authority_tier = "expert"
|
||||||
|
# evidence = ["Mozilla HTTP guidelines"]
|
||||||
|
# category = "safety"
|
||||||
|
# status = "active"
|
||||||
|
# created_by = "aphoria-suggest"
|
||||||
|
# created_at = "2024-12-15T10:00:00Z"
|
||||||
|
#
|
||||||
|
# # 21 more claims...
|
||||||
|
# # Total: ~200 lines, <1 second to import
|
||||||
|
#
|
||||||
|
# TIME SAVINGS: 15 minutes → <1 second
|
||||||
|
# CODE REDUCTION: 340 lines → 200 lines
|
||||||
|
# ERROR DETECTION: 0% → 100% (pre-import validation)
|
||||||
124
applications/aphoria/examples/import-template.toml
Normal file
124
applications/aphoria/examples/import-template.toml
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
# Aphoria Claims Import Template
|
||||||
|
#
|
||||||
|
# This file demonstrates the TOML format for bulk claim import.
|
||||||
|
# Generate this template: aphoria claims import --template > my-claims.toml
|
||||||
|
#
|
||||||
|
# Import commands:
|
||||||
|
# aphoria claims import my-claims.toml --validate-only # Validate format
|
||||||
|
# aphoria claims import my-claims.toml --dry-run # Preview changes
|
||||||
|
# aphoria claims import my-claims.toml # Import for real
|
||||||
|
#
|
||||||
|
# Merge strategies:
|
||||||
|
# --merge skip_existing (default) Skip claims with duplicate IDs
|
||||||
|
# --merge overwrite Replace existing claims with same ID
|
||||||
|
# --merge fail_on_duplicate Exit with error if any ID exists
|
||||||
|
#
|
||||||
|
# Output formats:
|
||||||
|
# --format table (default) Human-readable with symbols
|
||||||
|
# --format json Machine-readable for tooling
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Example 1: Architecture Decision
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
[[claim]]
|
||||||
|
id = "myapp-core-no-tokio-001"
|
||||||
|
concept_path = "myapp/core/imports/tokio"
|
||||||
|
predicate = "imported"
|
||||||
|
value = false
|
||||||
|
comparison = "absent"
|
||||||
|
provenance = "Architecture decision by tech lead 2024-12-15"
|
||||||
|
invariant = "Core modules MUST remain sync-only"
|
||||||
|
consequence = "Importing tokio makes core async-only, breaking sync library users"
|
||||||
|
authority_tier = "expert"
|
||||||
|
evidence = ["ADR-003", "design review notes"]
|
||||||
|
category = "architecture"
|
||||||
|
status = "active"
|
||||||
|
created_by = "tech-lead"
|
||||||
|
created_at = "2024-12-15T10:00:00Z"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Example 2: Security Requirement
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
[[claim]]
|
||||||
|
id = "myapp-http-tls-cert-validation-001"
|
||||||
|
concept_path = "myapp/httpclient/tls/certificate_validation"
|
||||||
|
predicate = "enabled"
|
||||||
|
value = true
|
||||||
|
comparison = "equals"
|
||||||
|
provenance = "OWASP A02:2021 - Cryptographic Failures"
|
||||||
|
invariant = "HTTP clients MUST validate TLS certificates"
|
||||||
|
consequence = "Disabled validation exposes MITM attacks"
|
||||||
|
authority_tier = "regulatory"
|
||||||
|
evidence = ["OWASP Top 10", "CWE-295"]
|
||||||
|
category = "security"
|
||||||
|
status = "active"
|
||||||
|
created_by = "security-team"
|
||||||
|
created_at = "2024-12-15T10:00:00Z"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Example 3: Safety Invariant
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
[[claim]]
|
||||||
|
id = "myapp-pool-max-size-001"
|
||||||
|
concept_path = "myapp/pool/config/max_size"
|
||||||
|
predicate = "max_value"
|
||||||
|
value = 50
|
||||||
|
comparison = "equals"
|
||||||
|
provenance = "Load testing results 2024-12-10"
|
||||||
|
invariant = "Connection pool size MUST NOT exceed 50"
|
||||||
|
consequence = "Larger pools cause OOM under sustained load"
|
||||||
|
authority_tier = "expert"
|
||||||
|
evidence = ["tests/pool_tests.rs load test"]
|
||||||
|
category = "safety"
|
||||||
|
status = "active"
|
||||||
|
created_by = "sre-team"
|
||||||
|
created_at = "2024-12-15T10:00:00Z"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Field Reference
|
||||||
|
# ============================================================================
|
||||||
|
#
|
||||||
|
# Required fields:
|
||||||
|
# id - Unique kebab-case identifier (e.g., "myapp-feature-001")
|
||||||
|
# concept_path - Hierarchical path to concept (e.g., "myapp/module/feature")
|
||||||
|
# predicate - Property being claimed (e.g., "enabled", "max_version")
|
||||||
|
# value - Expected value (bool, number, or "text")
|
||||||
|
# comparison - How to compare: equals, not_equals, present, absent, contains, not_contains
|
||||||
|
# provenance - Where this rule came from
|
||||||
|
# invariant - What MUST remain true
|
||||||
|
# consequence - What breaks if violated
|
||||||
|
# authority_tier - regulatory, clinical, observational, expert, community, anecdotal
|
||||||
|
# category - safety, architecture, security, imports, constants, derives, etc.
|
||||||
|
# created_by - Author name
|
||||||
|
# created_at - ISO 8601 timestamp
|
||||||
|
#
|
||||||
|
# Optional fields:
|
||||||
|
# evidence - Array of supporting references (default: [])
|
||||||
|
# status - active (default), deprecated, superseded
|
||||||
|
# supersedes - ID of claim this replaces
|
||||||
|
# updated_at - ISO 8601 timestamp of last update
|
||||||
|
#
|
||||||
|
# ============================================================================
|
||||||
|
# Comparison Modes
|
||||||
|
# ============================================================================
|
||||||
|
#
|
||||||
|
# equals - Value must exactly match
|
||||||
|
# not_equals - Value must differ
|
||||||
|
# present - Value must exist
|
||||||
|
# absent - Value must not exist
|
||||||
|
# contains - Value must contain substring/element
|
||||||
|
# not_contains - Value must not contain substring/element
|
||||||
|
#
|
||||||
|
# ============================================================================
|
||||||
|
# Authority Tiers (strongest to weakest)
|
||||||
|
# ============================================================================
|
||||||
|
#
|
||||||
|
# 1. regulatory - Legal/regulatory mandate (FDA, GDPR)
|
||||||
|
# 2. clinical - Peer-reviewed clinical evidence
|
||||||
|
# 3. observational - Real-world data, case studies
|
||||||
|
# 4. expert - Senior engineer/architect decision
|
||||||
|
# 5. community - Team consensus, convention
|
||||||
|
# 6. anecdotal - Individual experience, suggestion
|
||||||
@ -172,8 +172,8 @@ pub enum ClaimsCommands {
|
|||||||
|
|
||||||
/// Import claims from a TOML file in batch
|
/// Import claims from a TOML file in batch
|
||||||
Import {
|
Import {
|
||||||
/// Path to TOML file with claims
|
/// Path to TOML file with claims (omit to generate template)
|
||||||
file: PathBuf,
|
file: Option<PathBuf>,
|
||||||
|
|
||||||
/// Authority tier to apply to all claims (overrides tier in file)
|
/// Authority tier to apply to all claims (overrides tier in file)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@ -190,6 +190,18 @@ pub enum ClaimsCommands {
|
|||||||
/// Merge strategy: skip_existing, overwrite, fail_on_duplicate
|
/// Merge strategy: skip_existing, overwrite, fail_on_duplicate
|
||||||
#[arg(long, default_value = "skip_existing")]
|
#[arg(long, default_value = "skip_existing")]
|
||||||
merge: String,
|
merge: String,
|
||||||
|
|
||||||
|
/// Generate example TOML template
|
||||||
|
#[arg(long)]
|
||||||
|
template: bool,
|
||||||
|
|
||||||
|
/// Validate file without importing
|
||||||
|
#[arg(long)]
|
||||||
|
validate_only: bool,
|
||||||
|
|
||||||
|
/// Output format: table or json
|
||||||
|
#[arg(long, default_value = "table")]
|
||||||
|
format: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// List pending claim markers
|
/// List pending claim markers
|
||||||
|
|||||||
@ -138,8 +138,27 @@ pub async fn handle_claims_command(command: ClaimsCommands, config: &AphoriaConf
|
|||||||
ClaimsCommands::RejectMarker { marker_id, reason } => {
|
ClaimsCommands::RejectMarker { marker_id, reason } => {
|
||||||
handle_reject_marker(marker_id, reason, config).await
|
handle_reject_marker(marker_id, reason, config).await
|
||||||
}
|
}
|
||||||
ClaimsCommands::Import { file, authority_tier, source_guide, dry_run, merge } => {
|
ClaimsCommands::Import {
|
||||||
handle_claims_import(file, authority_tier, source_guide, dry_run, merge, config).await
|
file,
|
||||||
|
authority_tier,
|
||||||
|
source_guide,
|
||||||
|
dry_run,
|
||||||
|
merge,
|
||||||
|
template,
|
||||||
|
validate_only,
|
||||||
|
format,
|
||||||
|
} => {
|
||||||
|
let options = ImportOptions {
|
||||||
|
file,
|
||||||
|
authority_tier,
|
||||||
|
source_guide,
|
||||||
|
dry_run,
|
||||||
|
merge,
|
||||||
|
template,
|
||||||
|
validate_only,
|
||||||
|
format,
|
||||||
|
};
|
||||||
|
handle_claims_import(options, config).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -945,17 +964,391 @@ async fn handle_reject_marker(
|
|||||||
ExitCode::SUCCESS
|
ExitCode::SUCCESS
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_claims_import(
|
/// Options for bulk claim import.
|
||||||
file: std::path::PathBuf,
|
struct ImportOptions {
|
||||||
|
file: Option<std::path::PathBuf>,
|
||||||
authority_tier: Option<String>,
|
authority_tier: Option<String>,
|
||||||
source_guide: Option<String>,
|
source_guide: Option<String>,
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
merge: String,
|
merge: String,
|
||||||
_config: &AphoriaConfig,
|
template: bool,
|
||||||
) -> ExitCode {
|
validate_only: bool,
|
||||||
|
format: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validation error for a specific claim in the import file.
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct ValidationError {
|
||||||
|
claim_index: usize,
|
||||||
|
claim_id: Option<String>,
|
||||||
|
field: String,
|
||||||
|
error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Warning for a claim that's valid but potentially problematic.
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct ValidationWarning {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
claim_index: usize,
|
||||||
|
claim_id: String,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of validating all claims in an import file.
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct ValidationResult {
|
||||||
|
errors: Vec<ValidationError>,
|
||||||
|
warnings: Vec<ValidationWarning>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValidationResult {
|
||||||
|
fn is_valid(&self) -> bool {
|
||||||
|
self.errors.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Action taken for a specific claim during import.
|
||||||
|
#[derive(Debug, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
enum ImportAction {
|
||||||
|
Added,
|
||||||
|
Skipped,
|
||||||
|
Overwritten,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detail for a single claim in the import report.
|
||||||
|
#[derive(Debug, serde::Serialize)]
|
||||||
|
struct ImportDetail {
|
||||||
|
action: ImportAction,
|
||||||
|
claim_id: String,
|
||||||
|
reason: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Summary counts for the import operation.
|
||||||
|
#[derive(Debug, serde::Serialize)]
|
||||||
|
struct ImportSummary {
|
||||||
|
total_in_file: usize,
|
||||||
|
added: usize,
|
||||||
|
skipped: usize,
|
||||||
|
overwritten: usize,
|
||||||
|
failed: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete import report with summary, details, and warnings.
|
||||||
|
#[derive(Debug, serde::Serialize)]
|
||||||
|
struct ImportReport {
|
||||||
|
summary: ImportSummary,
|
||||||
|
details: Vec<ImportDetail>,
|
||||||
|
warnings: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImportReport {
|
||||||
|
/// Format as human-readable table.
|
||||||
|
fn format_table(&self, dry_run: bool) -> String {
|
||||||
|
let mut output = String::new();
|
||||||
|
|
||||||
|
output.push_str("Aphoria Claims Import\n");
|
||||||
|
output.push_str(&"=".repeat(40));
|
||||||
|
output.push('\n');
|
||||||
|
|
||||||
|
if dry_run {
|
||||||
|
output.push_str("🔍 Dry-run mode (no changes written)\n\n");
|
||||||
|
} else {
|
||||||
|
output.push_str("✓ Import Complete\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
output.push_str("Summary:\n");
|
||||||
|
output.push_str(&format!(" Total claims in file: {}\n", self.summary.total_in_file));
|
||||||
|
output.push_str(&format!(" Added: {}\n", self.summary.added));
|
||||||
|
output.push_str(&format!(" Skipped: {}\n", self.summary.skipped));
|
||||||
|
output.push_str(&format!(" Overwritten: {}\n", self.summary.overwritten));
|
||||||
|
|
||||||
|
if !self.details.is_empty() {
|
||||||
|
output.push_str("\nDetails:\n");
|
||||||
|
for detail in &self.details {
|
||||||
|
let symbol = match detail.action {
|
||||||
|
ImportAction::Added => "✓",
|
||||||
|
ImportAction::Skipped => "⊗",
|
||||||
|
ImportAction::Overwritten => "↻",
|
||||||
|
};
|
||||||
|
let action_str = match detail.action {
|
||||||
|
ImportAction::Added => "ADD ",
|
||||||
|
ImportAction::Skipped => "SKIP ",
|
||||||
|
ImportAction::Overwritten => "UPDATE",
|
||||||
|
};
|
||||||
|
let reason_str = detail.reason.as_ref()
|
||||||
|
.map(|r| format!(" ({})", r))
|
||||||
|
.unwrap_or_default();
|
||||||
|
output.push_str(&format!(" {} {} {}{}\n", symbol, action_str, detail.claim_id, reason_str));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.warnings.is_empty() {
|
||||||
|
output.push_str("\nWarnings:\n");
|
||||||
|
for warning in &self.warnings {
|
||||||
|
output.push_str(&format!(" ⚠ {}\n", warning));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format as JSON for tooling integration.
|
||||||
|
fn format_json(&self) -> Result<String, serde_json::Error> {
|
||||||
|
serde_json::to_string_pretty(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates an example TOML template for bulk claim import.
|
||||||
|
fn generate_import_template() -> String {
|
||||||
|
r#"# Aphoria Claims Import Template
|
||||||
|
#
|
||||||
|
# This file contains example claims that demonstrate the TOML format for bulk import.
|
||||||
|
# To import claims:
|
||||||
|
# aphoria claims import my-claims.toml
|
||||||
|
#
|
||||||
|
# To preview without writing:
|
||||||
|
# aphoria claims import my-claims.toml --dry-run
|
||||||
|
#
|
||||||
|
# To validate format:
|
||||||
|
# aphoria claims import my-claims.toml --validate-only
|
||||||
|
#
|
||||||
|
# Merge strategies:
|
||||||
|
# --merge skip_existing (default) Skip claims with duplicate IDs
|
||||||
|
# --merge overwrite Replace existing claims with same ID
|
||||||
|
# --merge fail_on_duplicate Exit with error if any ID exists
|
||||||
|
|
||||||
|
# Example 1: Architecture Decision
|
||||||
|
[[claim]]
|
||||||
|
id = "myapp-core-no-tokio-001"
|
||||||
|
concept_path = "myapp/core/imports/tokio"
|
||||||
|
predicate = "imported"
|
||||||
|
value = false
|
||||||
|
comparison = "absent"
|
||||||
|
provenance = "Architecture decision by tech lead 2024-12-15"
|
||||||
|
invariant = "Core modules MUST remain sync-only"
|
||||||
|
consequence = "Importing tokio makes core async-only, breaking sync library users"
|
||||||
|
authority_tier = "expert"
|
||||||
|
evidence = ["ADR-003", "design review notes"]
|
||||||
|
category = "architecture"
|
||||||
|
status = "active"
|
||||||
|
created_by = "tech-lead"
|
||||||
|
created_at = "2024-12-15T10:00:00Z"
|
||||||
|
|
||||||
|
# Example 2: Security Requirement
|
||||||
|
[[claim]]
|
||||||
|
id = "myapp-http-tls-cert-validation-001"
|
||||||
|
concept_path = "myapp/httpclient/tls/certificate_validation"
|
||||||
|
predicate = "enabled"
|
||||||
|
value = true
|
||||||
|
comparison = "equals"
|
||||||
|
provenance = "OWASP A02:2021 - Cryptographic Failures"
|
||||||
|
invariant = "HTTP clients MUST validate TLS certificates"
|
||||||
|
consequence = "Disabled validation exposes MITM attacks"
|
||||||
|
authority_tier = "regulatory"
|
||||||
|
evidence = ["OWASP Top 10", "CWE-295"]
|
||||||
|
category = "security"
|
||||||
|
status = "active"
|
||||||
|
created_by = "security-team"
|
||||||
|
created_at = "2024-12-15T10:00:00Z"
|
||||||
|
|
||||||
|
# Example 3: Safety Invariant
|
||||||
|
[[claim]]
|
||||||
|
id = "myapp-pool-max-size-001"
|
||||||
|
concept_path = "myapp/pool/config/max_size"
|
||||||
|
predicate = "max_value"
|
||||||
|
value = 50
|
||||||
|
comparison = "equals"
|
||||||
|
provenance = "Load testing results 2024-12-10"
|
||||||
|
invariant = "Connection pool size MUST NOT exceed 50"
|
||||||
|
consequence = "Larger pools cause OOM under sustained load"
|
||||||
|
authority_tier = "expert"
|
||||||
|
evidence = ["tests/pool_tests.rs load test"]
|
||||||
|
category = "safety"
|
||||||
|
status = "active"
|
||||||
|
created_by = "sre-team"
|
||||||
|
created_at = "2024-12-15T10:00:00Z"
|
||||||
|
|
||||||
|
# Field Reference:
|
||||||
|
#
|
||||||
|
# Required fields:
|
||||||
|
# id - Unique kebab-case identifier (e.g., "myapp-feature-001")
|
||||||
|
# concept_path - Hierarchical path to concept (e.g., "myapp/module/feature")
|
||||||
|
# predicate - Property being claimed (e.g., "enabled", "max_version")
|
||||||
|
# value - Expected value (bool, number, or "text")
|
||||||
|
# comparison - How to compare: equals, not_equals, present, absent, contains, not_contains
|
||||||
|
# provenance - Where this rule came from
|
||||||
|
# invariant - What MUST remain true
|
||||||
|
# consequence - What breaks if violated
|
||||||
|
# authority_tier - regulatory, clinical, observational, expert, community, anecdotal
|
||||||
|
# category - safety, architecture, security, imports, constants, derives, etc.
|
||||||
|
# created_by - Author name
|
||||||
|
# created_at - ISO 8601 timestamp
|
||||||
|
#
|
||||||
|
# Optional fields:
|
||||||
|
# evidence - Array of supporting references (default: [])
|
||||||
|
# status - active (default), deprecated, superseded
|
||||||
|
# supersedes - ID of claim this replaces
|
||||||
|
# updated_at - ISO 8601 timestamp of last update
|
||||||
|
"#.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates all claims before import.
|
||||||
|
///
|
||||||
|
/// Checks:
|
||||||
|
/// - ID format (kebab-case, length)
|
||||||
|
/// - Authority tier validity
|
||||||
|
/// - Required fields presence
|
||||||
|
/// - Duplicate IDs within import file
|
||||||
|
/// - Duplicate concept_path+predicate combinations (warning)
|
||||||
|
fn validate_imported_claims(
|
||||||
|
claims: &[aphoria::AuthoredClaim],
|
||||||
|
existing_claims: &ClaimsFile,
|
||||||
|
) -> ValidationResult {
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
let mut warnings = Vec::new();
|
||||||
|
let mut seen_ids = HashSet::new();
|
||||||
|
let mut seen_concepts = HashMap::new();
|
||||||
|
|
||||||
|
for (index, claim) in claims.iter().enumerate() {
|
||||||
|
// Validate ID format
|
||||||
|
if let Err(e) = validate_claim_id(&claim.id) {
|
||||||
|
errors.push(ValidationError {
|
||||||
|
claim_index: index,
|
||||||
|
claim_id: Some(claim.id.clone()),
|
||||||
|
field: "id".to_string(),
|
||||||
|
error: e,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate IDs within import file
|
||||||
|
if seen_ids.contains(&claim.id) {
|
||||||
|
errors.push(ValidationError {
|
||||||
|
claim_index: index,
|
||||||
|
claim_id: Some(claim.id.clone()),
|
||||||
|
field: "id".to_string(),
|
||||||
|
error: format!("Duplicate ID '{}' within import file", claim.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
seen_ids.insert(claim.id.clone());
|
||||||
|
|
||||||
|
// Validate authority tier
|
||||||
|
if let Err(e) = parse_authority_tier(&claim.authority_tier) {
|
||||||
|
errors.push(ValidationError {
|
||||||
|
claim_index: index,
|
||||||
|
claim_id: Some(claim.id.clone()),
|
||||||
|
field: "authority_tier".to_string(),
|
||||||
|
error: e.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check required fields
|
||||||
|
if claim.provenance.trim().is_empty() {
|
||||||
|
errors.push(ValidationError {
|
||||||
|
claim_index: index,
|
||||||
|
claim_id: Some(claim.id.clone()),
|
||||||
|
field: "provenance".to_string(),
|
||||||
|
error: "provenance cannot be empty".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if claim.invariant.trim().is_empty() {
|
||||||
|
errors.push(ValidationError {
|
||||||
|
claim_index: index,
|
||||||
|
claim_id: Some(claim.id.clone()),
|
||||||
|
field: "invariant".to_string(),
|
||||||
|
error: "invariant cannot be empty".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if claim.consequence.trim().is_empty() {
|
||||||
|
errors.push(ValidationError {
|
||||||
|
claim_index: index,
|
||||||
|
claim_id: Some(claim.id.clone()),
|
||||||
|
field: "consequence".to_string(),
|
||||||
|
error: "consequence cannot be empty".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if claim.category.trim().is_empty() {
|
||||||
|
errors.push(ValidationError {
|
||||||
|
claim_index: index,
|
||||||
|
claim_id: Some(claim.id.clone()),
|
||||||
|
field: "category".to_string(),
|
||||||
|
error: "category cannot be empty".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if claim.created_by.trim().is_empty() {
|
||||||
|
errors.push(ValidationError {
|
||||||
|
claim_index: index,
|
||||||
|
claim_id: Some(claim.id.clone()),
|
||||||
|
field: "created_by".to_string(),
|
||||||
|
error: "created_by cannot be empty".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn on duplicate concept_path + predicate (not an error, but suspicious)
|
||||||
|
let concept_key = format!("{}::{}", claim.concept_path, claim.predicate);
|
||||||
|
if let Some(&prev_index) = seen_concepts.get(&concept_key) {
|
||||||
|
warnings.push(ValidationWarning {
|
||||||
|
claim_index: index,
|
||||||
|
claim_id: claim.id.clone(),
|
||||||
|
message: format!(
|
||||||
|
"Duplicate concept_path+predicate '{}' (also in claim at index {})",
|
||||||
|
concept_key, prev_index
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
seen_concepts.insert(concept_key, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn if ID already exists in existing claims file
|
||||||
|
if existing_claims.find_by_id(&claim.id).is_some() {
|
||||||
|
warnings.push(ValidationWarning {
|
||||||
|
claim_index: index,
|
||||||
|
claim_id: claim.id.clone(),
|
||||||
|
message: format!("Claim ID '{}' already exists in claims file", claim.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationResult { errors, warnings }
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_claims_import(options: ImportOptions, _config: &AphoriaConfig) -> ExitCode {
|
||||||
use aphoria::claims_file::ClaimsFile;
|
use aphoria::claims_file::ClaimsFile;
|
||||||
use aphoria::AuthoredClaim;
|
use aphoria::AuthoredClaim;
|
||||||
|
|
||||||
|
// Destructure options
|
||||||
|
let ImportOptions {
|
||||||
|
file,
|
||||||
|
authority_tier,
|
||||||
|
source_guide,
|
||||||
|
dry_run,
|
||||||
|
merge,
|
||||||
|
template,
|
||||||
|
validate_only,
|
||||||
|
format,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
// Handle --template flag
|
||||||
|
if template {
|
||||||
|
print!("{}", generate_import_template());
|
||||||
|
return ExitCode::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require file path if not generating template
|
||||||
|
let file = match file {
|
||||||
|
Some(f) => f,
|
||||||
|
None => {
|
||||||
|
eprintln!("Error: FILE path required (or use --template to generate example)");
|
||||||
|
return ExitCode::from(3);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Get project root
|
// Get project root
|
||||||
let root = match project_root() {
|
let root = match project_root() {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
@ -987,6 +1380,11 @@ async fn handle_claims_import(
|
|||||||
|
|
||||||
// Override authority tier if specified
|
// Override authority tier if specified
|
||||||
if let Some(ref tier) = authority_tier {
|
if let Some(ref tier) = authority_tier {
|
||||||
|
// Validate the override tier first
|
||||||
|
if let Err(e) = parse_authority_tier(tier) {
|
||||||
|
eprintln!("Error: Invalid authority tier '{}': {}", tier, e);
|
||||||
|
return ExitCode::from(3);
|
||||||
|
}
|
||||||
for claim in &mut import.claim {
|
for claim in &mut import.claim {
|
||||||
claim.authority_tier = tier.clone();
|
claim.authority_tier = tier.clone();
|
||||||
}
|
}
|
||||||
@ -1002,10 +1400,55 @@ async fn handle_claims_import(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determine merge strategy
|
// Validate all claims before any writes
|
||||||
|
let validation = validate_imported_claims(&import.claim, &claims_file);
|
||||||
|
|
||||||
|
// Report validation errors
|
||||||
|
if !validation.is_valid() {
|
||||||
|
eprintln!("\n❌ Validation Failed\n");
|
||||||
|
eprintln!("Found {} error(s) in import file:\n", validation.errors.len());
|
||||||
|
for err in &validation.errors {
|
||||||
|
let claim_desc = err
|
||||||
|
.claim_id
|
||||||
|
.as_ref()
|
||||||
|
.map(|id| format!("'{}'", id))
|
||||||
|
.unwrap_or_else(|| format!("at index {}", err.claim_index));
|
||||||
|
eprintln!(" • Claim {} - {}: {}", claim_desc, err.field, err.error);
|
||||||
|
}
|
||||||
|
eprintln!("\nFix these errors and try again.");
|
||||||
|
return ExitCode::from(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report validation warnings
|
||||||
|
if !validation.warnings.is_empty() {
|
||||||
|
eprintln!("\n⚠️ {} warning(s):\n", validation.warnings.len());
|
||||||
|
for warn in &validation.warnings {
|
||||||
|
eprintln!(" • Claim '{}': {}", warn.claim_id, warn.message);
|
||||||
|
}
|
||||||
|
eprintln!();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If validate-only mode, report success and exit
|
||||||
|
if validate_only {
|
||||||
|
println!("\n✓ Validation passed");
|
||||||
|
println!(" Total claims: {}", import.claim.len());
|
||||||
|
println!(" Warnings: {}", validation.warnings.len());
|
||||||
|
println!("\nFile is ready for import.");
|
||||||
|
return ExitCode::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process claims and build report
|
||||||
|
let total_in_file = import.claim.len();
|
||||||
let mut added_count = 0;
|
let mut added_count = 0;
|
||||||
let mut skipped_count = 0;
|
let mut skipped_count = 0;
|
||||||
let mut overwritten_count = 0;
|
let mut overwritten_count = 0;
|
||||||
|
let mut details = Vec::new();
|
||||||
|
let mut report_warnings = Vec::new();
|
||||||
|
|
||||||
|
// Collect validation warnings for report
|
||||||
|
for warn in &validation.warnings {
|
||||||
|
report_warnings.push(format!("{}: {}", warn.claim_id, warn.message));
|
||||||
|
}
|
||||||
|
|
||||||
for claim in import.claim {
|
for claim in import.claim {
|
||||||
let existing = claims_file.claims.iter().position(|c| c.id == claim.id);
|
let existing = claims_file.claims.iter().position(|c| c.id == claim.id);
|
||||||
@ -1013,29 +1456,37 @@ async fn handle_claims_import(
|
|||||||
match (existing, merge.as_str()) {
|
match (existing, merge.as_str()) {
|
||||||
(Some(_idx), "skip_existing") => {
|
(Some(_idx), "skip_existing") => {
|
||||||
skipped_count += 1;
|
skipped_count += 1;
|
||||||
if dry_run {
|
details.push(ImportDetail {
|
||||||
println!("Would skip existing claim: {}", claim.id);
|
action: ImportAction::Skipped,
|
||||||
}
|
claim_id: claim.id.clone(),
|
||||||
|
reason: Some("already exists".to_string()),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
(Some(idx), "overwrite") => {
|
(Some(idx), "overwrite") => {
|
||||||
if dry_run {
|
if !dry_run {
|
||||||
println!("Would overwrite claim: {}", claim.id);
|
claims_file.claims[idx] = claim.clone();
|
||||||
} else {
|
|
||||||
claims_file.claims[idx] = claim;
|
|
||||||
}
|
}
|
||||||
overwritten_count += 1;
|
overwritten_count += 1;
|
||||||
|
details.push(ImportDetail {
|
||||||
|
action: ImportAction::Overwritten,
|
||||||
|
claim_id: claim.id.clone(),
|
||||||
|
reason: None,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
(Some(_), "fail_on_duplicate") => {
|
(Some(_), "fail_on_duplicate") => {
|
||||||
eprintln!("Error: Duplicate claim ID: {}", claim.id);
|
eprintln!("Error: Duplicate claim ID: {}", claim.id);
|
||||||
return ExitCode::from(3);
|
return ExitCode::from(3);
|
||||||
}
|
}
|
||||||
(None, _) => {
|
(None, _) => {
|
||||||
if dry_run {
|
if !dry_run {
|
||||||
println!("Would add claim: {}", claim.id);
|
claims_file.claims.push(claim.clone());
|
||||||
} else {
|
|
||||||
claims_file.claims.push(claim);
|
|
||||||
}
|
}
|
||||||
added_count += 1;
|
added_count += 1;
|
||||||
|
details.push(ImportDetail {
|
||||||
|
action: ImportAction::Added,
|
||||||
|
claim_id: claim.id.clone(),
|
||||||
|
reason: None,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
eprintln!("Invalid merge strategy: {merge}");
|
eprintln!("Invalid merge strategy: {merge}");
|
||||||
@ -1098,16 +1549,38 @@ async fn handle_claims_import(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Report results
|
// Build report
|
||||||
if dry_run {
|
let report = ImportReport {
|
||||||
println!("\n🔍 Dry-run mode (no changes written)");
|
summary: ImportSummary {
|
||||||
} else {
|
total_in_file,
|
||||||
println!("\n✓ Import complete");
|
added: added_count,
|
||||||
|
skipped: skipped_count,
|
||||||
|
overwritten: overwritten_count,
|
||||||
|
failed: 0,
|
||||||
|
},
|
||||||
|
details,
|
||||||
|
warnings: report_warnings,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Output report in requested format
|
||||||
|
match format.as_str() {
|
||||||
|
"json" => {
|
||||||
|
match report.format_json() {
|
||||||
|
Ok(json) => println!("{}", json),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error formatting JSON output: {}", e);
|
||||||
|
return ExitCode::from(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"table" => {
|
||||||
|
println!("{}", report.format_table(dry_run));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
eprintln!("Error: Invalid format '{}'. Use: table or json", format);
|
||||||
|
return ExitCode::from(3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
println!(" Added: {added_count}");
|
|
||||||
println!(" Overwritten: {overwritten_count}");
|
|
||||||
println!(" Skipped: {skipped_count}");
|
|
||||||
println!(" Total imported: {}", added_count + overwritten_count + skipped_count);
|
|
||||||
|
|
||||||
ExitCode::SUCCESS
|
ExitCode::SUCCESS
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user