AI Confirmation
Semantic confirmation is an opt-in layer that uses AI (Jev from TypeSafe's System One) to verify detected PII candidates before redacting them. It reduces false positives by asking a semantic model "yes/no" questions about each candidate.
Overview
- Opt-in feature — no behavior change unless configured
- Only affects
redactAsync()— syncredact()andstream()are unaffected - Only detectors with a
semanticConfirm()method participate - Currently only
person_name_liteis opted in - Fails open: if the AI provider is unavailable, all detections are kept
Installation
The @typesafe-ai/sdk package is an optional peer dependency:
bun add @typesafe-ai/sdkConfiguration
Add a semantic block to your RedactorConfig:
import { createRedactor } from "sensored";
const redactor = createRedactor({
rules: { person_name_lite: { action: "redact" } },
semantic: {
provider: "jev",
apiKey: process.env.TYPESAFE_API_KEY!,
},
});
const result = await redactor.redactAsync("Contact John Smith today");
// result.text: "Contact [PERSON_NAME] today"
// result.detections[0].semanticConfirmed: true
// result.detections[0].noul: 0.95SemanticConfig
interface SemanticConfig {
readonly provider: "jev";
readonly apiKey: string;
readonly model?: string;
readonly thresholds?: Readonly<Record<string, number>>;
readonly contextWindow?: number;
}Options
- provider — Must be
"jev". Only Jev is supported in v1. - apiKey — Your TypeSafe API key.
- model — Jev model to use. Defaults to
"jev-latest". - thresholds — Per-rule noul thresholds (0–1). Detections with noul below the threshold are dropped. Defaults to
0.5. Use"default"as a catch-all key. - contextWindow — Characters of context before and after each candidate sent to Jev. Defaults to
200.
Thresholds
semantic: {
provider: "jev",
apiKey: process.env.TYPESAFE_API_KEY!,
thresholds: {
person_name_lite: 0.7, // stricter for person names
default: 0.5, // fallback for other detectors
},
}redactAsync()
redactAsync() is the async variant of redact(). It runs sync detection first, then sends opted-in candidates to Jev for confirmation, drops unconfirmed detections, and renders the final text.
const result = await redactor.redactAsync(text);Returns AsyncRedactResult:
interface AsyncRedactResult {
readonly text: string;
readonly map?: RestorationMap;
readonly detections: readonly SemanticDetection[];
readonly warnings?: readonly string[];
}- text — The redacted text (same format as
redact()). - map — Present when
restore: trueis set in config. - detections — All detections that survived semantic confirmation. Each includes
semanticConfirmed: booleanand optionalnoul: number. - warnings — Present when the AI provider fails. All detections are kept (fail open).
How it works
- Sync detection runs all active detectors (same as
redact()) - Candidates from detectors with
semanticConfirm()are collected - If no candidates, early exit with all detections confirmed
- All candidates are batched into a single Jev call
- Detections with noul below threshold are dropped
- Remaining detections are rendered (same as
redact())
Streaming
Streaming stays sync-only. If semantic config is present, stream() emits a console warning and proceeds without semantic confirmation.
Custom detectors
Custom detectors can opt in to semantic confirmation by providing a semanticConfirm function:
const customDetector: DetectorDefinition = {
id: "custom_entity",
entityType: "custom_entity",
replacement: "[CUSTOM_ENTITY]",
pattern: /\b[A-Z]{3}\d{4}\b/g,
semanticConfirm({ value, before, after }) {
return {
instructions: "Determine whether the candidate is a custom entity.",
criteria: {
true: "The candidate matches the expected format.",
false: "The candidate is not a custom entity.",
},
};
},
};The semanticConfirm function receives { value, before, after } and returns a SemanticQuestion with instructions and optional criteria (with true and false strings).
Error handling
Semantic confirmation fails open. If the Jev API call fails (network error, invalid API key, rate limit, etc.), all detections are kept and a warning string is added to warnings[]. The redacted text is still returned.
const result = await redactor.redactAsync("Contact John Smith today");
if (result.warnings?.length) {
console.warn("Semantic confirmation failed:", result.warnings);
// All detections kept — text is still redacted
}