Metadata-Version: 2.3
Name: anchor-guard
Version: 0.3.0
Summary: Generation trust layer for the Anchor assurance suite — span-level claim attribution, abstention correctness, and citation generation.
Author: Prasant Mishra
Author-email: Prasant Mishra <mishraprasant73@gmail.com>
License: BSL-1.1
Requires-Dist: pydantic>=2.7
Requires-Dist: typer>=0.12
Requires-Dist: rich>=13.7
Requires-Dist: pynacl>=1.5
Requires-Python: >=3.12
Description-Content-Type: text/markdown

# anchor-guard

**Claim-level groundedness scoring for RAG-generated answers.**

anchor-guard closes the blind spot that anchor-eval leaves open: a retrieval pipeline
can score 95% on span recall and still produce hallucinated answers. anchor-guard
decomposes each generated answer into atomic claims, attributes every claim to the
exact character range in the source document that supports it, and blocks CI deploys
when groundedness regresses — entirely on-prem, with no data egress.

---

## The problem anchor-guard solves

anchor-eval proves that your retriever found the right spans. It does not prove that
the LLM built a grounded answer from them.

Consider this scenario:

1. The retriever correctly surfaces clause §4.2(b) from `contract.pdf` — anchor-eval
   scores a hit.
2. The LLM ignores that clause and asserts the indemnification cap is $500K when the
   clause says $1M.
3. anchor-eval scores: **pass**. The answer is wrong.

This failure is not hypothetical. It is the default mode of every RAG pipeline that
evaluates retrieval but not generation. For a legal, healthcare, or financial services
deployment, a confident wrong answer is worse than no answer.

anchor-guard catches it by asking a different question: *is every claim in this answer
actually entailed by the source span it was supposedly drawn from?*

---

## How it works

```
  Your RAG pipeline
  ─────────────────────────────────────────────────────────
  User query ──► Retriever ──► Context spans ──► LLM ──► Answer
                    │                                       │
                    │ (spans + answer go to anchor-guard)   │
                    └───────────────────────────────────────┘
                                      │
                              anchor guard check
                                      │
                    ┌─────────────────▼──────────────────────┐
                    │  1. Decompose answer into atomic claims  │
                    │  2. Attribute each claim to a source span│
                    │  3. Score NLI entailment per claim       │
                    │  4. Check abstention correctness         │
                    │  5. Detect PII / restricted content      │
                    │  6. Generate claim-level citations       │
                    │  7. Gate CI on regression vs. baseline   │
                    └────────────────────────────────────────-┘
                                      │
                              GuardReport (JSON)
                                      │
                    ┌─────────────────┼─────────────────┐
                    │                 │                  │
             anchor-review      anchor-ledger      CI exit code
           (SME HTML bundle)  (audit evidence)    (0 / 1 / 2)
```

anchor-guard runs entirely **out of the production request path**. It consumes answers
after the fact — from files, OTel traces, or test fixtures. It cannot cause an outage
and never adds to production latency.

---

## Quickstart

```bash
pip install anchor-guard
# or: uv add anchor-guard

# Run a groundedness check on a set of answers
anchor guard check answers.json --corpus ./docs

# Save a groundedness baseline for CI regression gating
anchor guard baseline answers.json --output guard_baseline.json

# Regression gate: exits 1 if groundedness drops > 2%
anchor guard check answers.json --corpus ./docs \
  --baseline guard_baseline.json

# Generate an HTML review bundle for an SME
anchor guard review guard_report.json --output review.html
```

### answers.json format

anchor-guard expects a JSON array of answer objects. The minimal shape:

```json
[
  {
    "answer_id": "ans_001",
    "question_id": "q_001",
    "raw_answer": "The indemnification cap is $1M per the contract.",
    "retrieved_spans": [
      {
        "doc_id": "contract.pdf",
        "char_start": 4210,
        "char_end": 4580,
        "text": "The aggregate liability of either party shall not exceed one million dollars ($1,000,000)..."
      }
    ]
  }
]
```

When anchor-eval's `QuestionSet` is provided via `--question-set`, each answer is
additionally checked against its `expected_span` — the character range the correct
answer should be drawn from. This is the stronger, two-layer check described in
[Dependency on anchor-eval](#dependency-on-anchor-eval).

---

## Commands

| Command | Description |
|---|---|
| `anchor guard check` | Run the full pipeline: decompose → attribute → disclose → cite → gate. |
| `anchor guard baseline` | Save current metrics as the regression baseline for a `(prompt_version, model_version)` pair. |
| `anchor guard review` | Generate a self-contained HTML bundle for SME claim attribution review. |

### `anchor guard check`

```bash
anchor guard check <answers.json> [options]

Options:
  --question-set, -q  PATH   anchor-eval QuestionSet JSON (enables IOU gating against expected_span)
  --corpus,       -C  PATH   Source document directory (required for span text lookup)
  --config,       -c  PATH   GuardConfig JSON (thresholds, NLI model, features)
  --output,       -o  PATH   Write GuardReport JSON to this path
  --review-bundle,-r  PATH   Also write an anchor-review HTML bundle
  --baseline,     -b  PATH   Compare against this baseline; exits 1 on regression
```

Exit codes follow the same convention as `anchor ci`:

| Exit | Meaning |
|---|---|
| `0` | All checks passed, no regression |
| `1` | Groundedness or abstention regressed beyond tolerance |
| `2` | Corpus drift or configuration mismatch |

### `anchor guard baseline`

```bash
anchor guard baseline <answers.json> [options]

Options:
  --output, -o  PATH   Baseline file path (default: guard_baseline.json)
  --config, -c  PATH   GuardConfig JSON
```

Saves `groundedness_rate`, `abstention_pass_rate`, and `ungrounded_claim_rate` keyed to the
`(prompt_version, model_version)` pair in the config. All three metrics participate in
regression gating — a rise in `ungrounded_claim_rate` beyond tolerance triggers exit 1 the
same as a drop in groundedness. When the version pair changes —
which happens often in on-prem deployments where teams swap local models for
GPU cost reasons — and no baseline exists for the new pair, a warning is emitted
rather than an error.

### `anchor guard review`

```bash
anchor guard review <guard_report.json> [options]

Options:
  --output, -o  PATH   HTML bundle path (default: guard_review.html)
```

Generates the anchor-review bundle. See [SME calibration](#sme-calibration-anchor-review)
for the full workflow.

---

## The five checks

### 1. Claim decomposition

The answer is split into atomic, independently verifiable claims. Each claim must be
a complete declarative sentence that can be checked against a single source span
without requiring context from other claims.

**Primary:** LLM-backed decomposition via any OpenAI-compatible `/v1/chat/completions`
endpoint — local or external, whichever is configured for the environment.  
**Fallback:** Sentence-boundary splitting when no LLM endpoint is configured (lower
confidence, flagged in the report).

```
Answer: "The termination notice period is 30 days. Either party may terminate for 
         convenience. Liquidated damages apply only to Section 12 breaches."

Claims:
  [0] "The termination notice period is 30 days."
  [1] "Either party may terminate for convenience."
  [2] "Liquidated damages apply only to Section 12 breaches."
```

### 2. Span-level attribution

Each claim is attributed to the retrieved span that best supports it. Attribution
uses a two-stage pipeline:

**Stage 1 — NLI entailment:** a small local model scores whether the span text
entails the claim. This is a narrow task (span-level entailment, not open-domain NLI)
that small models handle well — no frontier judge required.

**Stage 2 — IOU gate (when anchor-eval QuestionSet is provided):** the winning span
is compared against the question's `expected_span` using intersection-over-union. A
claim is only classified as `GROUNDED` when both NLI confidence and IOU meet their
thresholds.

```
Claim:  "The termination notice period is 30 days."
Span:   contract.pdf §4210–4580  (NLI confidence: 0.92, IOU vs expected_span: 0.71)
Status: GROUNDED ✓

Claim:  "Liquidated damages apply only to Section 12 breaches."
Span:   contract.pdf §7100–7350  (NLI confidence: 0.41)
Status: UNGROUNDED ✗  ← LLM hallucinated; span does not entail this claim
```

| Status | Meaning |
|---|---|
| `GROUNDED` | NLI confidence ≥ threshold and (IOU ≥ threshold or no expected_span) |
| `UNGROUNDED` | No candidate span achieves NLI confidence ≥ threshold |
| `ABSTAINED` | System correctly declined to answer a question with no corpus answer |
| `PENDING_SME` | Attribution awaits SME review via anchor-review |

Default thresholds (configurable in GuardConfig):

| Parameter | Default |
|---|---|
| `iou_threshold` | 0.5 |
| `nli_confidence_threshold` | 0.7 |

### 3. Abstention correctness

For questions where no corpus answer exists (`expected_span: null` in the QuestionSet,
corresponding to anchor-eval's `negative_existence` and `implicit_negative` archetypes),
anchor-guard verifies the system responds with a recognizable abstention rather than
fabricating an answer.

Two-sided check:
- **Should abstain, didn't:** system hallucinated where it should have said "not in corpus." Flagged as high-severity.
- **Should answer, abstained:** system incorrectly declined a question it should have answered.

`abstention_pass_rate` is a top-level metric in GuardReport and participates in regression gating.
When `enable_abstention_check` is `false`, it is omitted (`null`) from the report and skipped in the regression gate — a disabled check never triggers a spurious regression.

### 4. Disclosure detection

Scans generated answer text for PII and restricted content. Runs entirely locally —
no external API calls, no data egress.

| Finding type | Severity | Pattern |
|---|---|---|
| `pii_email` | High | Email addresses |
| `pii_phone` | High | US phone numbers |
| `pii_ssn` | Critical | Social Security Numbers |
| `pii_credit_card` | Critical | Credit card numbers |
| `restricted_term` | Medium | Terms from the active guard pack |

Disclosure findings are reported independently from groundedness — a clean answer can
still contain PII, and a hallucinated answer may not. They are separate quality dimensions.

### 5. Citation generation

For every `GROUNDED` claim, anchor-guard emits a claim-level citation:

```json
{
  "claim_id": "c_001",
  "claim_text": "The termination notice period is 30 days.",
  "doc_id": "contract.pdf",
  "char_start": 4210,
  "char_end": 4580,
  "nli_confidence": 0.92
}
```

Inline format: `[contract.pdf §4210–4580]`

Document-level citations ("Source: contract.pdf") are insufficient for legal and
regulated use — the precise clause is required. Claim-level citations enable auditors
to trace each assertion to the exact character range.

---

## LLM backend configuration

> **Recommendation:** use a model running inside your private network.
> NLI scoring and claim decomposition send retrieved span text and generated answer
> text to the configured endpoint.  For legal, healthcare, and financial deployments
> that data is almost certainly regulated.  A local model keeps it inside your
> infrastructure with no additional controls required.  External providers are
> supported but require an explicit opt-in — see [Data residency and the allow_external_calls flag](#data-residency-and-the-allow_external_calls-flag) below.

anchor-guard sends NLI scoring and claim decomposition prompts to a
`/v1/chat/completions` endpoint. Which endpoint that is — and whether it sits inside
your private network or reaches an external service — is entirely your decision.

```bash
# Any OpenAI-compatible server on the private network
# (vLLM, llama.cpp server, LM Studio, LocalAI, Ollama, HF TGI, …)
anchor guard check answers.json --corpus ./docs

# Custom endpoint and model
anchor guard check answers.json --corpus ./docs \
  --config guard_config.json
```

`guard_config.json`:

```json
{
  "nli_provider": {
    "model": "llama3.2:3b",
    "base_url": "http://gpu-host:8000",
    "timeout_seconds": 60,
    "api_key": null
  },
  "iou_threshold": 0.5,
  "nli_confidence_threshold": 0.7,
  "enable_disclosure_check": true,
  "enable_abstention_check": true,
  "enable_citation_generation": true
}
```

`base_url` is the scheme + host + port of the server. anchor-guard always POSTs to
`{base_url}/v1/chat/completions`. Any server that exposes this path works without
additional configuration.

### Credential resolution

`api_key` in the config file is optional. anchor-guard reads credentials from environment
variables at runtime — no library files to edit, no secrets in version control:

| Env var | Used for |
|---|---|
| `ANCHOR_NLI_API_KEY` | NLI scoring endpoint (`nli_provider`) |
| `ANCHOR_DECOMPOSE_API_KEY` | Claim decomposition endpoint (`decompose_provider`); falls back to `ANCHOR_NLI_API_KEY` if unset |

```bash
export ANCHOR_NLI_API_KEY=sk-...
anchor guard check answers.json --config guard_config.json
```

### Separate decomposition and scoring models

By default, both claim decomposition and NLI scoring use `nli_provider`. To use a
larger model for decomposition (higher fidelity) and a faster model for NLI (lower
latency), add a separate `decompose_provider`:

```json
{
  "decompose_provider": {
    "model": "llama3.1:70b",
    "base_url": "http://big-gpu:8000",
    "timeout_seconds": 120
  },
  "nli_provider": {
    "model": "llama3.2:3b",
    "base_url": "http://fast-gpu:8000",
    "timeout_seconds": 30
  }
}
```

When `decompose_provider` is absent, `nli_provider` is used for both stages.

### Built-in provider wrappers

For external services, anchor-guard ships named wrappers that pre-configure the right
endpoint and read API keys from standard environment variables:

| Provider | NLI scorer | Claim splitter | Env var |
|---|---|---|---|
| OpenAI | `OpenAINLIScorer` | `OpenAIClaimSplitter` | `OPENAI_API_KEY` |
| Anthropic | `AnthropicNLIScorer` | `AnthropicClaimSplitter` | `ANTHROPIC_API_KEY` |
| Azure OpenAI | `AzureOpenAINLIScorer` | `AzureOpenAIClaimSplitter` | `AZURE_OPENAI_API_KEY` |
| Mistral AI | `MistralNLIScorer` | `MistralClaimSplitter` | `MISTRAL_API_KEY` |
| Groq | `GroqNLIScorer` | `GroqClaimSplitter` | `GROQ_API_KEY` |
| Together AI | `TogetherNLIScorer` | `TogetherClaimSplitter` | `TOGETHER_API_KEY` |

Import from `anchor_guard.attribute.providers` (scorers) and
`anchor_guard.decompose.providers` (splitters).

**Azure OpenAI via config file:** set `base_url` to the resource base URL and `model` to the deployment name:

```json
{
  "nli_provider": {
    "model": "my-gpt4o-deployment",
    "base_url": "https://mycompany-oai.openai.azure.com",
    "api_key": null
  },
  "allow_external_calls": true
}
```

The factory extracts the resource name from the hostname and routes to `AzureOpenAINLIScorer` / `AzureOpenAIClaimSplitter` automatically. `AZURE_OPENAI_API_KEY` is read from the environment when `api_key` is null.

### Custom backends

For any other service — gRPC, a proprietary REST API, an in-process Python model —
subclass the abstract base and implement one method:

```python
from anchor_guard.attribute.nli_scorer import NLIScorer

class MyNLIScorer(NLIScorer):
    def score(self, premise: str, hypothesis: str) -> float:
        return my_internal_service.entailment_confidence(premise, hypothesis)
```

The same pattern applies to `ClaimSplitter` in `anchor_guard.decompose.claim_splitter`.

### Data residency and the `allow_external_calls` flag

External provider wrappers are **blocked by default**. Calling one without opting in
raises `ExternalCallBlockedError` immediately, before any network request is made.
The flag is checked on **every call**, not once at construction time.

There are two usage paths, each with its own opt-in point:

**Via the pipeline (CLI / `GuardConfig`)**

Set `allow_external_calls=True` in `GuardConfig`. The pipeline factory
(`build_nli_scorer` / `build_claim_splitter` in `anchor_guard.pipeline`) inspects
`base_url`, detects known external provider hosts, routes to the appropriate named
provider class, and passes `config.allow_external_calls` through. Every subsequent
`score()` and `split()` call re-checks the flag — there is no way to call an external
provider through the pipeline without it being `True`.

```json
{
  "nli_provider": {
    "model": "gpt-4o-mini",
    "base_url": "https://api.openai.com",
    "api_key": null
  },
  "allow_external_calls": true
}
```

**Via the Python API (direct construction)**

When constructing a provider class directly — outside the pipeline — pass
`allow_external_calls=True` to the constructor:

```python
from anchor_guard.attribute.providers import OpenAINLIScorer
from anchor_guard.decompose.providers import OpenAIClaimSplitter

scorer = OpenAINLIScorer(allow_external_calls=True)   # reads OPENAI_API_KEY from env
splitter = OpenAIClaimSplitter(allow_external_calls=True)
```

For OpenAI-compatible local/private endpoints (the default path), use `ANCHOR_NLI_API_KEY`
instead of the provider-specific env vars above.

### CI / zero-dependency fallback

For CI environments without a running LLM endpoint, set
`"model": "sentence-boundary-fallback"` to use the deterministic sentence-splitting
fallback. Attribution quality is lower but the pipeline runs with zero dependencies.

In this mode the NLI scorer is replaced with a fixture that returns full confidence
(1.0) for every claim, so the pipeline completes without a false regression exit.
**This mode is for smoke-testing pipeline wiring only** — groundedness scores produced
in fallback mode are not meaningful and must not be committed as a baseline.

---

## Dependency on anchor-eval

anchor-guard has two operating modes depending on whether anchor-eval's `QuestionSet`
is provided.

### Standalone mode

anchor-guard runs independently. You provide answers and the retrieved spans from
your own pipeline. No anchor-eval installation required.

```bash
anchor guard check answers.json --corpus ./docs
```

What you get:
- NLI-based claim attribution against your provided retrieved spans
- Abstention correctness (if your answer objects include `retrieval_required`)
- Disclosure detection
- Claim-level citations for grounded claims
- Regression gating against a saved baseline

What you don't get:
- IOU-gated attribution against a ground-truth expected span
- Per-archetype groundedness breakdown
- Correlation between retrieval quality and generation quality

### Paired mode (recommended)

When anchor-eval's `QuestionSet` is available, each answer is additionally checked
against its `expected_span`. This is the two-layer assurance claim:

> "The retriever found the right span (anchor-eval), AND the LLM built its answer
>  from that span (anchor-guard), AND the answer is grounded in the retrieved
>  text (NLI) AND attributable to the ground-truth location (IOU)."

```bash
# Step 1: Generate and commit a QuestionSet with anchor-eval
anchor generate --corpus ./contracts --domain legal --output question_set.json

# Step 2: Run your RAG pipeline, collect answers (your code)
python run_rag.py --questions question_set.json --output answers.json

# Step 3: Score retrieval with anchor-eval
anchor ci --question-set question_set.json --corpus ./contracts \
  --baseline anchor_baseline.json

# Step 4: Score generation with anchor-guard
anchor guard check answers.json --corpus ./contracts \
  --question-set question_set.json \
  --baseline guard_baseline.json
```

The two baselines are independent. A retrieval regression exits from `anchor ci`; a
groundedness regression exits from `anchor guard check`. Both gates run in the same
CI pipeline.

### Shared identity key

Both products use `(doc_id, char_start, char_end)` in Unicode codepoints as the
durable span identity. This key survives re-chunking, re-embedding, and model swaps —
the same property that makes anchor-eval benchmarks stable across pipeline changes
applies equally to anchor-guard's attribution records.

---

## SME calibration — anchor-review

The local NLI model is a judge, and judges make mistakes. anchor-guard's calibration
track lets a domain expert (lawyer, clinician, compliance officer) review and correct
individual claim attributions without touching the command line.

### Workflow

```
anchor guard check answers.json \
  --question-set question_set.json \
  --review-bundle guard_review.html
      │
      ▼
  guard_review.html  ◄── open in any browser, no server required
      │
  SME clicks through each claim attribution:
    [accept]  — span correctly entails this claim
    [reject]  — span does not support this claim
    [abstain] — SME cannot determine
    + free-text comment
      │
  Export  calibration.json  (ed25519-signed)
      │
anchor guard check answers.json \
  --calibration calibration.json \
  --baseline guard_baseline.json
      │
  PENDING_SME claims resolved → final groundedness_rate updated
```

### Design properties

- **Zero dependencies.** The HTML bundle has all CSS and JS inlined. Opens in Chrome,
  Firefox, Safari, or Edge. Works in fully air-gapped environments.
- **Signed export.** The `calibration.json` is signed with the reviewer's ed25519 key.
  anchor-guard verifies the signature before ingesting it.
- **Tamper-evident.** The signature covers the canonical JSON of all calibration records.
  Any post-export modification fails verification.
- **Auditable.** Every calibration ingestion is logged to anchor-ledger (when installed)
  with reviewer identity, timestamp, and verdict.

---

## CI/CD integration

anchor-guard fits into the same CI pipeline as anchor-eval without additional
infrastructure.

### GitHub Actions example

```yaml
- name: Score retrieval (anchor-eval)
  run: |
    anchor ci \
      --question-set question_set.json \
      --corpus ./contracts \
      --baseline anchor_baseline.json
  # exits 1 on retrieval regression, blocks deploy

- name: Score generation (anchor-guard)
  run: |
    anchor guard check answers.json \
      --corpus ./contracts \
      --question-set question_set.json \
      --baseline guard_baseline.json
  # exits 1 on groundedness regression, blocks deploy
```

### Updating baselines

When you intentionally improve the pipeline (new model, better chunking, tuned prompt),
update both baselines:

```bash
anchor ci --question-set question_set.json --corpus . --save-baseline anchor_baseline.json
anchor guard baseline answers.json --output guard_baseline.json
```

Commit both files. The baselines are version-controlled alongside the corpus and the
question set — they define what "acceptable" means for this version of the pipeline.

---

## The Anchor suite

anchor-guard is the second module in the six-module Anchor assurance suite. All modules
are designed for fully on-prem, air-gapped deployment — no customer data transits a
vendor API.

| Module | Purpose | Phase |
|---|---|---|
| **anchor-eval** | Span-anchored retrieval evaluation, archetype diagnosis, CI gating | Shipped (v0.3.1) |
| **anchor-guard** | Claim-level groundedness, abstention, citation, disclosure | Phase 1 (this repo) |
| **anchor-ledger** | OTel ingestion, append-only evidence store, compliance export | Phase 1.5 |
| **anchor-mirror** | Production query drift, PII-redacted intent distillation | Phase 2 |
| **anchor-shield** | Corpus security: injection detection, poisoning, ACL integrity | Phase 2 |
| **anchor-review / anchor-studio** | SME calibration UI (static bundle → local web workspace) | Phase 1 → 3 |

### How they connect

```
  anchor-eval           anchor-guard          anchor-ledger
  ───────────           ────────────          ─────────────
  QuestionSet ────────► expected_span         OTel traces ──► local OTLP
  (expected_span)       IOU gate                               endpoint
                             │
                        GuardReport ─────────────────────────► evidence store
                             │
                        guard_review.html ──► SME ──► calibration.json
                                                           │
                                                      anchor-guard
                                                      (ingested back)
```

Every artefact exchange is local — files, a localhost OTLP endpoint, or a local
web server. No external service dependencies at any integration point.

### Governing rule

> **No component of the Anchor suite sits in the live production request path.**

anchor-guard runs out-of-band against answer files or sampled OTel traces. The one
exception — a local proxy sidecar for dev/staging/CI — is explicitly scoped to
non-production environments. It is never permitted in production.

---

## Key concepts

| Term | Definition |
|---|---|
| **AtomicClaim** | A single, independently verifiable assertion extracted from an answer. The unit of attribution. |
| **ClaimSet** | All atomic claims extracted from one answer. |
| **SpanRef** | `(doc_id, char_start, char_end)` in Unicode codepoints. The shared identity key across anchor-eval and anchor-guard. |
| **NLI confidence** | Entailment probability that a source span text logically supports a claim. Scored by a local model. |
| **IOU threshold** | Minimum intersection-over-union between the winning span and the ground-truth `expected_span` for a claim to be classified as GROUNDED. Only active when a QuestionSet is provided. |
| **groundedness_rate** | Fraction of claims classified as GROUNDED in a run. Top-level GuardReport metric. |
| **abstention_pass_rate** | Fraction of abstention-required questions where the system correctly declined to answer. |
| **GuardReport** | The complete output of an anchor-guard run: attributions, disclosure findings, regression findings, citations. |
| **CalibrationExport** | Signed JSON produced by an SME in anchor-review containing accept/reject verdicts per claim. |
| **guard pack** | JSON bundle of abstention signal phrases and restricted terms. `guard-base-v0` ships with the package. |

---

## Packs

Guard packs configure abstention detection and restricted-term scanning. The active
pack is resolved from the `--corpus` domain or set explicitly.

| Pack | Domain | Ships with |
|---|---|---|
| `guard-base-v0` | General | anchor-guard (default) |

Custom packs:

```json
{
  "pack_id": "guard-legal-v0",
  "version": "0.1.0",
  "abstention_signals": [
    "not addressed in the agreement",
    "the contract is silent on",
    "outside the scope of this document"
  ],
  "restricted_terms": ["PRIVILEGED", "CONFIDENTIAL", "ATTORNEY-CLIENT"]
}
```

---

## Python API

anchor-guard can be used as a library inside evaluation scripts and notebooks.

```python
from anchor_guard.decompose.claim_splitter import SentenceBoundaryFallback, OpenAICompatibleClaimSplitter
from anchor_guard.attribute.nli_scorer import OpenAICompatibleNLIScorer
from anchor_guard.attribute.span_matcher import SpanMatcher
from anchor_guard.citation.generator import CitationGenerator
from anchor_guard.disclosure.detector import DisclosureDetector
from anchor_guard.models.span import SpanRef

# Decompose
splitter = SentenceBoundaryFallback()
claim_set = splitter.split(
    answer_id="ans_001",
    question_id="q_001",
    raw_answer="The indemnification cap is $1M. Notice must be given 30 days in advance.",
)

# Attribute — point base_url at any OpenAI-compatible server in the private network
nli_scorer = OpenAICompatibleNLIScorer(base_url="http://localhost:11434", model="mistral:7b")
candidate_spans = [
    (SpanRef(doc_id="contract.pdf", char_start=4210, char_end=4580), "...shall not exceed one million dollars..."),
]
matcher = SpanMatcher(nli_scorer=nli_scorer, candidate_spans=candidate_spans)
result = matcher.attribute(claim_set, expected_span=SpanRef(doc_id="contract.pdf", char_start=4210, char_end=4580))

print(f"Groundedness rate: {result.groundedness_rate:.0%}")
# Groundedness rate: 100%

# Cite
citations = CitationGenerator().generate(result)
for c in citations:
    print(c.to_inline())
# [contract.pdf §4210–4580]

# Disclose
findings = DisclosureDetector().detect("ans_001", "The cap is $1M.")
# findings == []  (no PII in this answer)
```

---

## Air-gapped deployment

anchor-guard is designed for fully disconnected environments. By default, no component
makes outbound network calls. External providers (OpenAI, Anthropic, Azure, etc.) are
available as opt-in wrappers — egress happens only when you explicitly configure one.

| Requirement | How it's met |
|---|---|
| No egress (default) | Default config points to a local `/v1/chat/completions` endpoint. No outbound calls unless an external provider is configured. |
| Offline license | ed25519-signed token at `~/.config/anchor/license.token`. Verified locally. |
| Offline packs | Guard packs are versioned signed archives distributed via sneakernet or internal artifact registry. |
| Reproducible runs | A prior `anchor guard check` run is exactly reproducible on the same pack versions months later — an audit requirement. |

### License

The `guard:claims` entitlement is required for `anchor guard check` and
`anchor guard review`. `anchor guard baseline` is always unlicensed — baselines
must be writable in CI environments before a license is provisioned.

```bash
# Install license (air-gapped: copy token file manually)
anchor license install --token <path-to-token>

# Verify guard entitlement
anchor license verify
# guard:claims ✓
```

---

## Content capture and OTel

anchor-guard can consume answers directly from OpenTelemetry GenAI traces when
anchor-ledger is installed and an OTLP endpoint is configured.

Production OTel collectors frequently omit message content (`gen_ai.prompt`,
`gen_ai.completion`) for privacy reasons. anchor-guard handles this gracefully:

| Mode | What's available | Capability |
|---|---|---|
| **Full content** (dev/staging/CI) | Spans + answer text + retrieved text | All five checks |
| **Metadata-only** (production) | Spans + latency + token counts | Trajectory and abstention checks only |
| **Sampled** (production with content) | Full fidelity on a configured sample rate | All five checks on sampled traffic |

The GuardReport records which mode was active. This constraint is a scoping item to
raise during customer onboarding — discovering it mid-POC reads as a product defect.

---

## Roadmap

### v0.1.0 (current)

- Folder structure, data models, and module stubs
- `DisclosureDetector` — PII and restricted-term scanning (fully implemented)
- `SpanRef` — shared identity model compatible with anchor-eval
- `RegressionGate` — baseline comparison logic
- `SentenceBoundaryFallback` — zero-dependency claim splitter for CI smoke tests
- anchor-review HTML bundle generator stub

### v0.2.0 (planned)

**Attribution pipeline**
- `SpanMatcher.attribute()` — two-stage attribution (NLI confidence + IOU gate)
- `AbstentionChecker.check()` — two-sided abstention correctness against guard pack signal phrases

**Calibration loop**
- `ReviewBundleGenerator.generate()` — complete anchor-review static HTML bundle
- `CalibrationIngester` — ed25519 signature verification + GROUNDED/UNGROUNDED verdict application from `CalibrationExport`

**CLI (all commands fully implemented)**
- `anchor guard check` — full pipeline: decompose → attribute → disclose → cite → gate; includes `--calibration` (ingest SME verdicts) flag
- `anchor guard baseline` — save groundedness metrics as regression baseline with version-mismatch warning
- `anchor guard review` — generate anchor-review HTML bundle from a saved GuardReport

**Infrastructure**
- `LicenseGate.check()` — offline ed25519 `guard:claims` entitlement verification
- `GuardReportRenderer.render_console()` — Rich terminal summary with severity-grouped disclosure counts and regression highlighting
- `schema_version: "1"` in `GuardReport`, `ClaimSet`, and `CalibrationExport` output files
- `content_mode: "full" | "metadata_only"` in `GuardReport`
- MAX_CLAIMS_PER_ANSWER enforcement with `ClaimSet.truncated` flag

**Already implemented in v0.1.0 (ships with v0.2.0)**
- `OpenAICompatibleClaimSplitter`, `OpenAICompatibleNLIScorer`
- Built-in provider wrappers: OpenAI, Anthropic, Azure OpenAI, Mistral, Groq, Together AI
- `CitationGenerator`

### v0.3.0 (current)

- Custom guard pack authoring — `GuardPack` model, `GuardPackLoader`, `anchor guard pack validate/new`
- `content_mode: "full" | "metadata_only"` in `GuardConfig` — skips NLI pipeline in metadata-only mode
- `--corpus` flag on `anchor guard check` and `anchor guard baseline` — reads span text directly from source document files when not embedded in `answers.json`
- `span_text` stored on `ClaimAttribution` at attribution time — `anchor guard review` now shows full span context without needing the original `answers.json`

### v0.4.0 (planned — blocked on external modules)

The following items are explicitly blocked on modules that do not exist yet in the
Anchor suite. They cannot be implemented inside anchor-guard alone.

| Item | Blocked on | Phase |
|---|---|---|
| OTel trace ingestion → GuardReport evidence records | anchor-ledger (Phase 1.5) | 1.5 |
| Sampled production monitoring with configurable sample rate | anchor-ledger (Phase 1.5) | 1.5 |
| SME review queues and judge-drift dashboards | anchor-studio (Phase 3) | 3 |

Once anchor-ledger exists, anchor-guard will add:
- `anchor guard check --from-traces <otlp_export.json>` — consume GenAI OTel spans as input instead of `answers.json`
- `GuardReportExporter` — write GuardReport events as OTLP-compatible JSON for anchor-ledger ingestion
- Sampled-mode evidence records: full fidelity on a configured sample rate, metadata-only on the remainder

Once anchor-studio exists, the static `anchor guard review` HTML bundle will be
promoted into an SME review queue with multi-reviewer support, verdict history, and
judge-drift dashboards.

---

## License

anchor-guard is licensed under the [Business Source License 1.1](LICENSE).
`anchor guard baseline` always runs without a license.
`anchor guard check` and `anchor guard review` require a `guard:claims` entitlement.
