Metadata-Version: 2.4
Name: tacit-citadel
Version: 0.3.0
Summary: Policy-driven JSON de-identification CLI
Requires-Python: <3.14,>=3.11
Requires-Dist: click>=8.4.1
Requires-Dist: logfire>=4.15.1
Requires-Dist: pydantic>=2.13.4
Requires-Dist: pyyaml>=6.0.3
Requires-Dist: rich>=14.2.0
Description-Content-Type: text/markdown

# Citadel

Citadel applies a YAML de-identification policy to JSON or JSONL records and
writes the transformed data to an explicit output path.

```bash
uvx tacit-citadel policy.yaml input.json output.json
```

For local development from this checkout:

```bash
uv run tacit-citadel policy.yaml sample.jsonl sample.citadel.jsonl
```

The PyPI package and installed console command are both `tacit-citadel`.

Project-specific policies can live with the dataset they are used for. The
checked-in `policy.yaml` and `sample.jsonl` are generic support-ticket examples
for local smoke tests.

## Policy Shape

Policies have deterministic path-based rules and an optional whole-record
rewrite step.

```yaml
version: 1
name: support-ticket-sanitizer
description: De-identification policy for customer support tickets.

validators:
  - path: .customer.contract_value_usd
    action: number_range
    params:
      min: 1
      max: 100000000

rewrite:
  backend: codex exec
  sandbox: read-only
  system_prompt: You are a conservative de-identification rewriter.
  user_prompt: |
    Rewrite the INPUT JSON object.

    INPUT JSON
    {{content}}
  preserve:
    - path: .customer.contract_value_usd
      required: false

rules:
  - path: .account.id
    action: drop
```

`rewrite.backend` must be `codex exec` or `claude -p`. Rewrites always receive
the whole record after deterministic rules have run and must return a complete
JSON object. `rewrite.preserve` selectors snapshot values after deterministic
rules and require the same values after the rewrite; `required` defaults to
`true`. If a rewrite fails, returns invalid JSON, or changes a preserved value
after three attempts, Citadel logs an error, aborts the run, and does not write
the output file.

## Validators

Validators run before deterministic rules and rewrite. If a record fails any
validator, Citadel omits that record from the output.

`number_range` requires every matched value to be numeric and within inclusive
`min` / `max` bounds. At least one bound is required. `required` defaults to
`true`; a missing required validator path skips the record.

```yaml
validators:
  - path: .intake_details.weight
    action: number_range
    params:
      min: 30
      max: 300
```

## Rules

`path` is a small jq-like selector. It supports dotted object fields, list
wildcards, numeric list indexes, quoted bracket fields, and simple
`| select(.field == "value")` / `| select(.field != "value")` filters.

`required` defaults to `true`. Use `required: false` for sparse paths.

### `drop`

Deletes matched object fields.

```yaml
- path: .account.id
  action: drop
```

### `fuzz_number`

Perturbs numeric values with either percentage or range mode.

```yaml
- path: .customer.contract_value_usd
  action: fuzz_number
  params:
    mode: percent
    max_percent: 3
    precision: 0
```

```yaml
- path: .risk_score
  action: fuzz_number
  params:
    mode: range
    min_delta: -1
    max_delta: 1
    step: 1
```

### `date_offset`

Replaces date or datetime strings with a day offset from an anchor date.

```yaml
- path: .events[].timestamp
  action: date_offset
  required: false
  params:
    anchor_path: .reported_at
    output: human_relative
```

Outputs are `same day`, `N day later`, `N days later`, `N day ago`, or
`N days ago`.

## Inputs

Citadel accepts:

- a JSON object
- a JSON array of objects
- JSONL with one object per non-empty line

The output format matches the input shape. Skipped JSONL records are omitted,
skipped JSON array items are removed, and a skipped single JSON object writes
`null`.

## Development

The implementation is intentionally contained in [run.py](run.py).

```bash
uv run pytest
uv run ruff check .
uv run ty check
```
