# @pleach/compliance (/docs/compliance)



Pruning shears at the garden wall — what crosses the boundary, in
what shape, with what scrub. `@pleach/compliance` is the
regulated-environment add-on to `@pleach/core`. The first release ships
four `Scrubber` implementations that gate event log writes, plus two
CI audit gates that hosts inherit at adoption time. The package is aimed at deployments where the audit
ledger is evidence — healthcare, finance, govtech — and where every
`harness_event_log` row needs both a tenant scope and a redaction
guarantee before it lands on disk.

> **Beta.** While `@pleach/core` ships stable, `@pleach/compliance` ships
> **beta** for the first release. The scrubbers, tenant-scoping, and CI
> audit gates below are production-solid and battery-proven; the
> **attestation / audit-report** surface is the beta part — envelopes are
> unsigned by default unless a real key-store is wired, and the version
> maps are Phase-A-empty (see [Attestation](/docs/attestation#beta)). A
> stable `@pleach/compliance` attestation claim lands at the 1.0 cut.

The canonical use case is alongside `@pleach/core`'s
`EventLogWriter`: scrubbers run on the persistence boundary, the CI
gates enforce that no code path writes an untenanted row, and the
schema check guarantees `tenant_id NOT NULL` after migration.

<SourceMeta pkg="{ name: &#x22;@pleach/compliance&#x22;, href: &#x22;https://www.npmjs.com/package/@pleach/compliance&#x22; }" source="{ label: &#x22;github.com/pleachhq/compliance&#x22;, href: &#x22;https://github.com/pleachhq/compliance&#x22; }" />

## Install [#install]

<Tabs items="['npm', 'pnpm', 'yarn', 'bun']" groupId="pkg">
  <Tab value="npm">
    ```bash
    npm install @pleach/compliance
    ```
  </Tab>

  <Tab value="pnpm">
    ```bash
    pnpm add @pleach/compliance
    ```
  </Tab>

  <Tab value="yarn">
    ```bash
    yarn add @pleach/compliance
    ```
  </Tab>

  <Tab value="bun">
    ```bash
    bun add @pleach/compliance
    ```
  </Tab>
</Tabs>

```typescript
import {
  SsnUsScrubber,
  CreditCardScrubber,
  UsDriverLicenseScrubber,
  KeyedRegexScrubber,
} from "@pleach/compliance";
```

The package exports the four scrubber classes (instantiate with `new`),
the `complianceProfilePlugin(profile)` factory that bundles them by
named profile (`"hipaa"` / `"gdpr"` / `"pci-dss"` / `"soc2"`), the
`createComplianceRuntime` wrapper for transparent interception, and
the lower-level `scrubbersForProfile(profile)` helper for hand-rolled
plugins. The published README is the source of truth for export names
and constructor options — check it before pinning a version.

## The four bundled scrubbers [#the-four-bundled-scrubbers]

Each implements the `Scrubber` contract from `@pleach/core/scrubbers`. The
contract — `scrub(input, ctx) => ScrubResult` returning `redacted` and
matched spans for audit reporting — is documented in [Scrubbers](/docs/scrubbers);
this section covers what ships in the bundle.

| Scrubber                  | Pattern                                                                                                  | Redaction key     |
| ------------------------- | -------------------------------------------------------------------------------------------------------- | ----------------- |
| `SsnUsScrubber`           | US Social Security numbers (`NNN-NN-NNNN`; opt-in unseparated `NNNNNNNNN`) with SSA invalid-range guards | `[REDACTED:SSN]`  |
| `CreditCardScrubber`      | Credit-card-shaped digit runs validated by Luhn check digit AND IIN range gate                           | `[REDACTED:CARD]` |
| `UsDriverLicenseScrubber` | US driver's license numbers — per-state format table, anchored or per-state mode                         | `[REDACTED:DL]`   |
| `KeyedRegexScrubber`      | Generic pattern + key adapter — the building block hosts extend                                          | caller-supplied   |

`SsnUsScrubber` matches the canonical hyphenated form and (when opted
in) the unhyphenated nine-digit form, with SSA invalid-range guards
(area ≠ 000/666/900–999, group ≠ 00, serial ≠ 0000). The redacted
form replaces the matched span with `[REDACTED:SSN]`; the original
digits never reach storage.

`CreditCardScrubber` validates against the Luhn check digit AND the
IIN range before redacting. The double gate cuts the false-positive
rate that a naive 16-digit regex would produce against arbitrary
numeric content — order numbers, session ids, and similar digit runs
pass through untouched. Opt out of the IIN gate via
`new CreditCardScrubber({ validateIin: false })` if you process exotic
card-network ranges.

`UsDriverLicenseScrubber` composes a state-keyed pattern table because
the driver's license format varies per state. The bundle ships the
table; consumers that need a state the upstream table doesn't cover
yet should open an issue on the package repo rather than fork the
scrubber.

`KeyedRegexScrubber` is the parameterized scrubber — the other three
are specializations that bind a fixed pattern and redaction key. The
next section covers it as the extension hook.

## `KeyedRegexScrubber` — the extension hook [#keyedregexscrubber--the-extension-hook]

`KeyedRegexScrubber` is the one scrubber consumers commonly instantiate
directly. It's not a custom scrubber — it's a parameterized one — so
it stays inside the contract and inherits the matched-spans audit
reporting that ships with the bundle.

```typescript
// lib/compliance/tenantMrnPlugin.ts
import { KeyedRegexScrubber } from "@pleach/compliance";
import type { HarnessPlugin } from "@pleach/core";

const mrnScrubber = new KeyedRegexScrubber({
  id: "tenant-mrn",
  entries: [
    {
      name:        "mrn",
      pattern:     /\b\d{7}\b/g,
      replacement: "[REDACTED:MRN]",
    },
  ],
});

export const tenantCompliancePlugin: HarnessPlugin = {
  name: "tenant-mrn-compliance",
  contributeScrubbers: () => [mrnScrubber],
};
```

The `name` (here `"mrn"`) is what shows up in the matched-spans report
and in any downstream audit consumer — pick a name that reads in a
compliance review, not the regex itself. The `replacement` may be a
string or a function `(match, name) => string` if the redacted form
needs to be derived from match groups (e.g. preserving the last 4
chars).

For a host with multiple regulated identifier families — MRN,
employee ID, internal account number — register them as separate
`entries` on a single `KeyedRegexScrubber`, or use one scrubber per
family if you want the audit report grouped per-scrubber-`id`. The
per-entry `name` field is the attribution unit either way.

## Inherited audit gates [#inherited-audit-gates]

Adopting `@pleach/compliance` adds two CI audit gates to the consumer
build. The gates are the load-bearing piece for regulated deployments:
they fail the build before a code path that violates tenant scoping
can reach production.

### `audit:tenant-scoping` [#audittenant-scoping]

Enforces that every event log write threads `tenant_id`. The gate
walks the consumer's source for call sites against `EventLogWriter`
(and its registered wrappers) and fails if any call site can reach
`append` without a `tenant_id` resolved from the runtime.

The check is structural — it flags the absence of the threading, not
the value. A code path that resolves `tenantId` from a request and
then drops it before calling the writer fails the gate at the drop
site, with the drop site named in the failure output.

### `audit:harness-event-log-tenant-id-required` [#auditharness-event-log-tenant-id-required]

Schema-level check that `harness_event_log.tenant_id` is `NOT NULL`
after migration. The gate reads the consumer's applied schema (via
the standard migration introspection helper) and fails if the column
is nullable.

Run it as part of the post-migration smoke step. A nullable
`tenant_id` column is the silent-isolation failure mode: writes
succeed, rollups miss rows, and the regulator's "show me tenant X's
events" query under-counts.

## Deployment pattern [#deployment-pattern]

Adopt the package once at the top of your `SessionRuntime`
construction via the standard plugin registration. The scrubbers wire
into `EventLogWriter` automatically; consumer code doesn't call them
directly.

```typescript
import { SessionRuntime } from "@pleach/core";
import { complianceProfilePlugin } from "@pleach/compliance";

const runtime = new SessionRuntime({
  storage,
  checkpointer,
  userId:   req.user.id,
  tenantId: req.user.orgId,
  plugins:  [complianceProfilePlugin("hipaa") /* , gatewayPlugin, ... */],
});
```

`complianceProfilePlugin(profile)` accepts `"hipaa" | "gdpr" | "pci-dss" | "soc2"`
and returns a `HarnessPlugin` whose `contributeScrubbers` resolves to
the scrubber bundle for that profile. Use `scrubbersForProfile(profile)`
directly if you need to hand-roll the plugin shape (e.g. composing
your own `name` + `id` fields). Custom scrubbers (additional
`KeyedRegexScrubber` instances for tenant-specific identifiers)
register through their own plugin alongside this one — composition is
additive.

The CI gates are picked up by the consumer's existing audit harness;
no per-build wiring beyond installing the package and listing it in
the audit config.

## What's NOT in this package today [#whats-not-in-this-package-today]

Honest about the scope of the 1.0 cut:

* **No gateway-level auth or rate limiting.** That lives in
  [`@pleach/gateway@0.1.0`](/docs/gateway). Today's compliance
  package does not see the outbound provider call.
* **No SBOM or attestation bundle.** `@pleach/trust-pack` remains
  reserved at `0.0.1 · UNLICENSED`. Supply-chain attestation is
  out of scope for this cut. (Cryptographic attestation of
  individual audit rows ships in `@pleach/core/attestation` — see
  [Attestation](/docs/attestation).)
* **Deterministic replay over the event log** ships in
  [`@pleach/replay@0.1.0`](/docs/replay) (`replayTurn`,
  `fromSnapshot`, `fork`, and `aggregateMultiTenant` bodies all
  land at the `0.1.0` cut) and
  [`@pleach/eval@0.1.0`](/docs/eval) (scoring + diff). The
  compliance package writes the rows; the replay/eval packages
  read them.
* **Tamper-evidence hash chain** stamping ships in
  `@pleach/core/eventLog` (`chainStep`, `computeRowHash`) behind
  the `c9PhaseBEnabled` flag; verification ships in the same
  `@pleach/core/eventLog` barrel (`verifyChainForChat`,
  `generateProof`). The `TamperEvidence` plug-point in
  `@pleach/core/audit` keeps its no-op default for hosts that route
  attestation elsewhere.
* **No broader scrubber set.** Four scrubbers ship in 1.0. Additional
  patterns (international tax IDs, EU formats, sector-specific
  identifiers) are planned but not present. Use `KeyedRegexScrubber`
  for anything outside the bundled four.

Check the [package README](https://github.com/pleachhq/compliance) for
the current roadmap before assuming a feature.

## Adopting in stages [#adopting-in-stages]

A host migrating an existing deployment toward mandatory tenant
scoping may want the scrubbers immediately but defer the CI gates
until the codebase is ready. The gates fail fast — a multi-week
migration with the gates on means a broken build for the duration.

Register only the scrubbers via `contributeScrubbers` from a custom
plugin, and skip the package's full plugin export:

```typescript
// lib/compliance/scrubbersOnlyPlugin.ts
import {
  SsnUsScrubber,
  CreditCardScrubber,
  UsDriverLicenseScrubber,
} from "@pleach/compliance";
import type { HarnessPlugin } from "@pleach/core";

export const scrubbersOnlyPlugin: HarnessPlugin = {
  name: "compliance-scrubbers-only",
  contributeScrubbers: () => [
    new SsnUsScrubber(),
    new CreditCardScrubber(),
    new UsDriverLicenseScrubber(),
  ],
};
```

The scrubbers run; the CI gates stay off. Track the migration as a
real ticket — running the scrubbers without the tenant-scoping
guarantee means redacted rows can still land untenanted, which is a
partial-isolation posture, not a finished one. Flip to the full
`complianceProfilePlugin("hipaa")` (or the profile matching your
posture) once the codebase passes the gates locally.

## Related SKUs [#related-skus]

* [`@pleach/core`](/docs/core) — the substrate this package wraps.
  Scrubbers run at `EventLogWriter`'s persistence boundary; the CI
  gates check call sites against `@pleach/core/eventLog`.
* [`@pleach/gateway`](/docs/gateway) — emits `domain.gateway.cost.recorded`
  events with the full `RoutingDecision` shape. Compliance's
  attestation runtime reads those events without round-tripping the
  audit ledger.
* [`@pleach/replay`](/docs/replay) — `verifyChainIntegrity` walks the
  hash-chained event log. Compliance writes the rows; replay attests
  them.
* [`@pleach/eval`](/docs/eval) — scoring + diff over replayed,
  scrubbed event logs.

For the full SKU map see [Which SKU do I need?](/docs/which-sku).

## Where to go next [#where-to-go-next]

<Cards>
  <Card title="Scrubbers" href="/docs/scrubbers" description="The `Scrubber` contract — event-type allowlists, matched-spans, and the `contributeScrubbers` hook." />

  <Card title="Multi-tenant" href="/docs/multi-tenant" description="Where `tenantId` lives in the substrate and the production tenant-isolation checklist." />

  <Card title="Tenant facet" href="/docs/multi-tenant#the-runtimetenant-facet" description="`runtime.tenant.id` — reading the substrate's tenant identity inside plugins and tool handlers." />

  <Card title="Audit ledger" href="/docs/audit-ledger" description="The `ProviderDecisionLedger` and the three compliance plug-points the package will extend." />
</Cards>
