Skip to content

Pipeline

Pipeline is the v0.2 framework primitive. A single Pipeline.process(text) call composes detection + jurisdiction-aware policy + audit and returns a typed Result.

from arche import Pipeline

pipeline = Pipeline(jurisdiction="NG")
result = pipeline.process(
    "Customer Adesola Okonkwo, NIN 12345678901, phone 0803 555 7890."
)

print(result.redacted_text)
# Customer NAME_..., NIN [NIN], phone PHONE_...

Pipeline

class Pipeline:
    def __init__(
        self,
        jurisdiction: str | None = None,
        statute: str | None = None,
        detectors: list[str] | None = None,
        address_parsing: bool = False,
        audit: bool = True,
        tokenize_salt: str = "",
    ) -> None: ...

    def process(self, text: str) -> Result: ...
    def process_file(self, source: str | Path) -> Result: ...
    def describe(self) -> dict[str, Any]: ...

Source: packages/arche-core/src/arche/workflow/_primitive.py.

Constructor parameters

Parameter Type Default Description
jurisdiction str \| None None ISO-3166-1 alpha-2 country code ("NG", "KE", "ZA", "GH"). Auto-loads the matching statute via _STATUTE_FOR_JURISDICTION. Uppercased internally.
statute str \| None None Explicit statute YAML name ("NDPA-2023", "POPIA", "KENYA-DPA", "GHANA-DPA"). Overrides the jurisdiction-implied statute. If neither jurisdiction nor statute is given, no policy is applied and raw detections are returned.
detectors list[str] \| None None Which detector packages to run. When omitted and jurisdiction is set, defaults to ["<cc>", "names", "locations", "ip", "digital_id", "addr", "core"]. When jurisdiction is missing, defaults to ["africa", "names", "locations", "ip", "digital_id", "addr", "core"].
address_parsing bool False Forward-compatibility hook for arche.addr.parse_address. The "addr" detector package already runs by default; this flag is reserved for future opt-in semantics.
audit bool True When True, emit per-detection and per-policy-decision entries into Result.audit_log. PII values are never recorded — only category labels, span offsets, document hashes, statute references, and timestamps.
tokenize_salt str "" Per-deployment salt for the tokenize policy action. Different salts across organisations prevent token re-identification when redacted documents cross trust boundaries. Pass a stable per-org secret from your environment, not a literal.

Methods

process(text: str) -> Result

Run the configured detector chain, apply the loaded statute's policy mappings, and (when audit=True) record per-event audit rows. Returns a Result dataclass.

The internal pipeline runs in five phases (per the source): detect → statute-aware enrichment (populates sensitivity_tier + regulatory_citation on each Detection) → address parsing hook → policy enforcement → in-memory audit recording.

process_file(source: str | Path) -> Result

Convenience: parse a file via arche.doc.parse (PDF / DOCX / PPTX / XLSX / HTML via docling — requires arche-core[doc]) then run process() on the extracted text. The returned Result carries metadata["source_file"] and metadata["num_pages"] for provenance.

Raises DoclingNotInstalledError (from arche.doc) if the optional extra is not installed.

describe() -> dict[str, Any]

Return a structured description of what this pipeline will do. Useful for logging / introspection — the dict carries jurisdiction, statute, detectors, address_parsing, audit.

pipeline = Pipeline(jurisdiction="NG", tokenize_salt="bank_2026")
pipeline.describe()
# {
#   "jurisdiction": "NG",
#   "statute": "NDPA-2023",
#   "detectors": ["ng", "names", "locations", "ip", "digital_id", "addr", "core"],
#   "address_parsing": False,
#   "audit": True,
# }

Result

@dataclass
class Result:
    document_hash: str
    detections: list[Detection]
    addresses: list[Any]                   # arche.addr.Address (forward-compat)
    policy_outcomes: list[PolicyOutcome]   # arche.policy.PolicyOutcome
    redacted_text: str
    audit_log: list[dict]                  # PII-free per-event rows
    metadata: dict[str, Any]

Result is a plain @dataclass with no instance methods. Read fields directly, or serialize the whole thing with dataclasses.asdict.

Field reference

Field Type What's in it
document_hash str SHA-256 hex of the input text (UTF-8). Used as the join key in the audit log so PII values never have to be stored.
detections list[Detection] Every detection the configured detector chain emitted, enriched with sensitivity_tier + regulatory_citation from the loaded statute.
addresses list[Address] Reserved for forward-compatibility. Address detections currently surface as Detection rows with category="PII-4-ADDRESS"; this list is empty in v0.2.0a3.
policy_outcomes list[PolicyOutcome] One row per detection that the statute mapped to a closed action (mask / tokenize / drop / generalize / audit / retain). Carries category, action, statute_id, statute_reference, span, detection_id.
redacted_text str The input text after policy actions have been applied. Safe to log, share, or persist.
audit_log list[dict] When audit=True, one dict per detection event (event_type="detection") and one per policy decision (event_type="policy"). PII-free by construction — category labels, span offsets, document hash, statute reference. Each dict also has timestamp (ISO 8601 UTC).
metadata dict[str, Any] Pipeline configuration snapshot: jurisdiction, statute_id, statute_version, detectors, address_parsing, audit, pipeline_version. When process_file was used, also source_file and num_pages.

Persisting the audit log to SQLite

Pipeline builds Result.audit_log as an in-memory list of plain dicts. Persisting to SQLite is the caller's job — wire it via the arche.graph.audit substrate:

from arche.graph.audit import AuditEvent, AuditLog

audit = AuditLog("./compliance.sqlite")

def record_result(audit: AuditLog, result, document_id: str) -> None:
    for d in result.detections:
        audit.emit(AuditEvent.detection(
            document_hash=document_id, category=d.category,
            span=(d.start, d.end), confidence=d.confidence, detector=d.detector,
        ))
    for o in result.policy_outcomes:
        audit.emit(AuditEvent.policy(
            document_hash=document_id, category=o.category, action=o.action,
            statute_id=o.statute_id, statute_reference=o.statute_reference,
            detection_id=o.detection_id, span=o.span,
        ))

for text in batch_of_documents:
    result = pipeline.process(text)
    record_result(audit, result, document_id=result.document_hash)

This is the same pattern the cookbooks use — see Nigerian fintech KYC and SQLite audit log example.


Detection

@dataclass
class Detection:
    id: str
    category: str
    text: str
    start: int
    end: int
    confidence: float
    detector: str
    identity_class: str = "inferred"
    sensitivity_tier: SensitivityTier = SensitivityTier.MODERATE
    regulatory_citation: str | None = None
    metadata: dict[str, Any] = field(default_factory=dict)

Field reference

Field Type What's in it
id str Stable identifier for cross-referencing in policy_outcomes and audit_log. Shape: det:<detector-slug>:<start>:<end> — e.g. det:nin:4:15, det:phone:23:36.
category str Pan-African PII Taxonomy v0.1 label — e.g. "PII-2-NIN", "PII-1-NAME", "PII-3-PHONE", "PII-4-ADDRESS", "PII-5-CRYPTO_WALLET".
text str The actual matched substring from the input. The original PII value — keep this off your wire and out of your logs.
start int Inclusive character offset into the input text.
end int Exclusive character offset.
confidence float 1.0 for structurally validated detections (Luhn-checked SA ID, NIN pattern + length + prefix). Lower for shape-only matches.
detector str Producer string — e.g. "rule:ng_nin", "phonenumbers:NG", "rule:addr_parser", "gliner" when the optional [detect] extra is active.
identity_class str One of foundational / functional / federated / inferred — the four-class identity-class distinction from the Pan-African PII Taxonomy.
sensitivity_tier SensitivityTier HIGH / MODERATE / LOW, populated from the loaded statute at detection time. Defaults to MODERATE for standalone detector calls that bypass Pipeline (no statute loaded means no tier mapping).
regulatory_citation str \| None The specific statute section ("NDPA-2023 s.29", "NDPA-2023 s.30, NIMC Act s.27") the loaded statute cites for this category. None for standalone detector calls.
metadata dict[str, Any] Detector-specific extras. For addr detections: street, city, region, country. For per-country IDs: whatever the country-specific detector chose to expose (varies — see the relevant arche.detect.<cc> module).

Reading the tier + citation that v0.2 ships:

for d in result.detections:
    print(f"{d.category:18} {d.sensitivity_tier.value:8} {d.regulatory_citation}")
# PII-2-NIN          high     NDPA-2023 s.30, NIMC Act s.27
# PII-2-BVN          high     NDPA-2023 s.30, CBN BVN policy 2014
# PII-1-NAME         moderate NDPA-2023 s.30

Examples

Basic NDPA-2023 pipeline

from arche import Pipeline

pipeline = Pipeline(jurisdiction="NG")
result = pipeline.process("NIN 12345678901, BVN 22156789012.")

print([d.category for d in result.detections])
# ['PII-2-NIN', 'PII-2-BVN']

print(result.redacted_text)
# NIN [NIN], BVN [BVN].

Inspect what the pipeline will do

pipeline = Pipeline(jurisdiction="ZA", tokenize_salt="hospital_2026")
print(pipeline.describe())
# {
#   "jurisdiction": "ZA",
#   "statute": "POPIA",
#   "detectors": ["za", "names", "locations", "ip", "digital_id", "addr", "core"],
#   "address_parsing": False,
#   "audit": True,
# }

Persisted audit log + signed regulator export

from datetime import datetime
from arche import Pipeline
from arche.graph.audit import AuditEvent, AuditLog
from arche.sign import generate_keypair

audit = AuditLog("./compliance.sqlite")
pipeline = Pipeline(jurisdiction="NG", tokenize_salt="bank_2026")

def record_result(audit, result, document_id):
    for d in result.detections:
        audit.emit(AuditEvent.detection(
            document_hash=document_id, category=d.category,
            span=(d.start, d.end), confidence=d.confidence, detector=d.detector,
        ))
    for o in result.policy_outcomes:
        audit.emit(AuditEvent.policy(
            document_hash=document_id, category=o.category, action=o.action,
            statute_id=o.statute_id, statute_reference=o.statute_reference,
            detection_id=o.detection_id, span=o.span,
        ))

for text in batch_of_documents:
    result = pipeline.process(text)
    record_result(audit, result, document_id=result.document_hash)

officer_key = generate_keypair()
report = audit.export_signed(
    key=officer_key,
    purpose="ndpc_quarterly_audit",
    since=datetime(2026, 4, 1),
    until=datetime(2026, 6, 30),
)
# `report` is a JWS-signed bundle the regulator can verify offline.

Pipeline + docling file ingest

# requires: pip install arche-core[doc]
from arche import Pipeline

pipeline = Pipeline(jurisdiction="ZA")
result = pipeline.process_file("dsar_response.pdf")
print(f"{len(result.detections)} detections")
print(result.metadata["source_file"], result.metadata["num_pages"])

Result is a plain dataclass — for JSON / CSV / Pydantic serialization patterns.


Verified against arche-core v0.2.0a3 on 2026-05-29. Constructor signature, Result dataclass fields, Detection dataclass fields, and the absence of Result methods all confirmed against packages/arche-core/src/arche/workflow/_primitive.py.