API Reference

July 11, 2026 · View on GitHub

datapitfalls ships as an npm package you can build on. This document is the supported public API: the functions, types, and constants that the package commits to keeping stable. The CLI (datapitfalls scan) and the web app are both thin layers over this same surface.

npm install datapitfalls

Requires Node.js 18 or later. The package is ESM-only ("type": "module") and ships its own TypeScript types.

import { detectPitfalls, formatReport, hasBlockingFindings } from 'datapitfalls';

API stability

The supported surface is exactly what this document describes. The package also re-exports some lower-level helpers; those are implementation details and may change without notice — if it isn't documented here, don't rely on it.

datapitfalls follows Semantic Versioning. The project is pre-1.0, so per semver a minor bump (0.x.0) may contain a breaking change to the documented API — but every such change is called out in CHANGELOG.md. Patch releases never break the documented API.

Everything in the engine requires an Anthropic API key (it calls the Claude API). Provide it via options.apiKey, a pre-built options.client, or the ANTHROPIC_API_KEY environment variable.


detectPitfalls

function detectPitfalls(input: DetectionInput, options?: DetectionOptions): Promise<PitfallReport>;

Scans one artifact — or a whole analysis chain — against the pitfall catalog and returns a structured report. It grounds Claude on the relevant taxonomy rules, collects findings via a forced tool call, and validates every rule id in the result against the catalog, so a finding can only ever reference a real rule.

import { detectPitfalls } from 'datapitfalls';

const report = await detectPitfalls(
  { kind: 'code', content: 'SELECT AVG(rate) FROM metrics;', language: 'SQL' },
  { apiKey: process.env.ANTHROPIC_API_KEY }
);

for (const f of report.findings) {
  console.log(`[${f.severity}] ${f.name} — ${f.explanation}`);
}

DetectionOptions

interface DetectionOptions {
  /** Model id. Defaults to ANTHROPIC_MODEL, then claude-sonnet-4-6. */
  model?: string;
  /** API key. Defaults to the ANTHROPIC_API_KEY environment variable. */
  apiKey?: string;
  /** Pre-constructed Anthropic client (overrides apiKey). */
  client?: Anthropic;
  /** Restrict grounding to these domains. Defaults to the whole catalog. */
  domains?: Domain[];
  /** Max output tokens. Defaults to 16000. */
  maxTokens?: number;
  /** EXPERIMENTAL — presentation variant to A/B test ('baseline' | 'summary').
   *  Defaults to 'baseline'. Not covered by the API-stability policy; may
   *  change or be removed once the experiment concludes. See evals/compare.mjs. */
  variant?: PresentationVariant;
}

Inputs

detectPitfalls accepts a DetectionInput: either a single artifact or a multi-stage chain.

type DetectionInput = SingleArtifactInput | ChainDetectionInput;

type SingleArtifactInput =
  | TextDetectionInput // code or plain-English description
  | ImageDetectionInput // one or several chart images
  | DocumentDetectionInput // a PDF, read natively
  | SlidesDetectionInput; // a slide deck, per-slide text + charts

Code or prose — TextDetectionInput

interface TextDetectionInput {
  kind: 'code' | 'text';
  content: string;
  language?: string; // e.g. "Python", "SQL" — code only
  filename?: string;
}

Charts — ImageDetectionInput

Pass several images together to catch cross-chart pitfalls (inconsistent scales, inconsistent encodings, contradictory messages).

interface ImageDetectionInput {
  kind: 'image';
  images: ImageSource[];
}

interface ImageSource {
  content: string; // base64-encoded image bytes
  mediaType: 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp';
  filename?: string;
}

A PDF report — DocumentDetectionInput

The PDF is sent to Claude as a native document, so it reads the prose and sees the charts and tables on the page.

interface DocumentDetectionInput {
  kind: 'document';
  content: string; // base64-encoded PDF bytes
  mediaType: 'application/pdf';
  filename?: string;
}

A slide deck — SlidesDetectionInput

interface SlidesDetectionInput {
  kind: 'slides';
  slides: SlideContent[]; // each slide's text + embedded chart images
  filename?: string;
}

interface SlideContent {
  text: string;
  images: ImageSource[];
}

Use extractSlides to build this from .pptx bytes.

A whole analysis — ChainDetectionInput

Scan the ordered stages of one analysis together (data prep → analysis → chart → narrative) so pitfalls that only emerge across stages surface — a transform that biases a later chart, a metric computed one way and described another, a chart the narrative over-claims.

interface ChainDetectionInput {
  kind: 'chain';
  stages: ChainStage[];
}

interface ChainStage {
  role: string; // e.g. "Data prep (Python)", "Chart", "Summary"
  artifact: SingleArtifactInput;
}

See fileToStage and textStage for building stages.


Reports

interface PitfallReport {
  findings: Finding[];
  kind: InputKind; // 'code' | 'text' | 'image' | 'document' | 'slides' | 'chain'
  model: string; // the model id that produced the report
  rulesConsidered: number; // how many catalog rules were in scope
  usage?: DetectionUsage; // token counts, when the SDK reports them
  // EXPERIMENTAL — present only with variant: 'summary' (not yet API-stable):
  summary?: string; // at most two sentences: overall state + top priority
  avoided?: AvoidedPitfall[]; // up to two pitfalls visibly avoided (may be empty)
}

interface Finding {
  ruleId: string; // always a real catalog rule id
  name: string;
  domain: Domain;
  severity: Severity; // 'info' | 'warning' | 'error'
  confidence: 'low' | 'medium' | 'high';
  /** 'active' = evident from the artifact; 'latent' = depends on unseen data. */
  nature: 'active' | 'latent';
  condition: string; // for latent findings: when the pitfall becomes a problem
  evidence: string;
  explanation: string;
  remediation: string;
  // EXPERIMENTAL — present only with variant: 'summary' (not yet API-stable):
  consequence?: 'changes-takeaway' | 'weakens-support' | 'polish';
}

// EXPERIMENTAL — a pitfall the work visibly avoided ('summary' variant only).
interface AvoidedPitfall {
  ruleId: string; // always a real catalog rule id, never one also in findings
  name: string;
  domain: Domain;
  evidence: string; // the guard, caveat, or choice that constitutes the avoidance
  explanation: string;
}

The catalog fields on a finding (name, domain, severity, remediation) are filled from the taxonomy, not from the model, so they are always authoritative.


Formatting a report

formatReport

function formatReport(
  report: PitfallReport,
  options?: { showAll?: boolean; color?: boolean }
): string;

Renders a report as plain text for the terminal, led by the report's tier (see reportTier) and the checked-against denominator:

NEEDS ATTENTION — 2 detected, 1 potential · 2 warning / 1 info
Checked against 78 rules · model claude-sonnet-4-6

By default it shows all active findings plus only high-confidence latent ones; pass { showAll: true } to include lower-confidence latent findings. Pass { color: true } to colorize the header with ANSI escapes (the tier in its semantic color, the denominator dimmed) — the caller owns TTY/NO_COLOR detection, so the default is plain text.

hasBlockingFindings

function hasBlockingFindings(report: PitfallReport): boolean;

true if the report has an active finding of severity warning or error — the condition the CLI's --ci flag uses to exit non-zero. Info-level advisories and all latent findings do not block.

const report = await detectPitfalls(input, { apiKey });
console.log(formatReport(report));
if (hasBlockingFindings(report)) process.exit(1);

reportTier

const TIERS = ['clear', 'verify', 'attention', 'serious'] as const;
type Tier = (typeof TIERS)[number];
const TIER_LABEL: Record<Tier, string>;

function reportTier(report: PitfallReport): Tier;

A coarse overall tier for a report — a deterministic rollup of the findings (never a model-supplied score), best to worst:

TierLabelWhen
clearNo pitfalls detectedNothing detected (low/medium-confidence latent findings are noise and don't count)
verifyConditions to verifyOnly info-level active findings and/or high-confidence latent ones
attentionNeeds attentionAt least one active warning
seriousSerious pitfalls foundAt least one active error, or an active warning rated changes-takeaway

Latent findings never push a report below verify, whatever their severity — they are conditions to check against the data, not verdicts. The tier agrees with hasBlockingFindings: it is attention or worse exactly when that returns true.

When displaying the clear tier, pair the label with report.rulesConsidered (e.g. "No pitfalls detected · checked against 47 rules") — a clean scan means none of the cataloged pitfalls were detected, not that the work is correct.

const tier = reportTier(report); // e.g. 'attention'
badge.textContent = TIER_LABEL[tier]; // "Needs attention"

Bridging to Semiotic

A dependency-free bridge that turns a PitfallReport into the plain objects Semiotic's annotations prop consumes (Semiotic v3's native annotation shape — flat title/label/wrap, a type from v3's taxonomy, an emphasis, and a provenance block), so a chart you audited can render its own warnings. It's the structural mirror of nteract/semiotic#1030, which bridges the other direction (Semiotic → datapitfalls): that one lives in their repo, emits our shape, and never imports us; this one lives in our repo, emits their shape, and never imports Semiotic. It adds zero runtime dependencies — it imports only local types.

function toSemioticAnnotations(
  report: PitfallReport,
  opts?: SemioticAnnotationOptions
): SemioticAnnotation[];

function buildSemioticAnnotationBridge(
  report: PitfallReport,
  opts?: SemioticAnnotationOptions
): SemioticAnnotationBridge; // { annotations, meta: { count, kind } }

toSemioticAnnotations maps each finding, in order, to one annotation. buildSemioticAnnotationBridge mirrors Semiotic's build* + to* pairing and also returns meta (see below).

import { detectPitfalls, buildSemioticAnnotationBridge } from 'datapitfalls';

const report = await detectPitfalls({ kind: 'image', images }, { apiKey });
const { annotations, meta } = buildSemioticAnnotationBridge(report);

// In your Semiotic frame:
// <XYFrame ... annotations={annotations} />
if (meta.count > annotations.length) {
  // a `max` cap dropped some findings — surface the difference
}

The honest seam — annotations are unanchored

datapitfalls sees findings, not pixel coordinates. So every annotation is emitted unanchored: no x/y or data accessor. The bridge never invents coordinates. On Semiotic v3 this is a hard requirement, not a nicety — an annotation whose coordinates can't be resolved is dropped, not floated. So positioning is the host app's job: anchor each to a mark (add your own x/y or an accessor before rendering), render them as a stacked margin/legend list (unaffected by anchoring), or let v3 re-resolve position via anchor: 'semantic' keyed on provenance.stableId (the ruleId). Each annotation also carries a dataPitfall blob (ruleId, domain, severity, evidence) so you can filter, style, or place them by rule.

Mapping

Finding fieldAnnotation field
nametitle (flat — v3 reads it directly)
remediationlabel (the actionable fix shown on the chart)
severitycolor (via palette), className = pitfall-${severity}, and emphasis ('primary' for errors, else 'secondary')
ruleIdprovenance.stableId (enables anchor: 'semantic')
ruleId, domain, severity, evidencedataPitfall blob

Types

type SemioticAnnotationType = 'label' | 'text'; // from v3's annotation taxonomy

interface SemioticAnnotation {
  type: SemioticAnnotationType; // default 'label'
  title: string; // flat (v1 nested this under `note`)
  label: string;
  wrap: number;
  color: string; // resolved from the severity palette
  className: string; // `pitfall-${severity}`
  emphasis: 'primary' | 'secondary';
  provenance: {
    author: 'datapitfalls';
    authorKind: 'watcher';
    source: 'computed';
    basis: 'llm-inference';
    stableId: string; // the ruleId
  };
  dataPitfall: { ruleId: string; domain: Domain; severity: Severity; evidence: string };
}

interface SemioticAnnotationOptions {
  /** Override the severity → color map. Merged over the defaults. */
  palette?: Partial<Record<Severity, string>>;
  /** Cap emitted annotations. Default: no cap. The full count survives in
   *  `meta.count`, so a cap is never silent. */
  max?: number;
  /** Text-wrap width (px) passed through to v3. Default: 240. */
  wrap?: number;
  /** v3 annotation type to emit. Default 'label'; use 'text' for connector-less notes. */
  type?: SemioticAnnotationType;
}

interface SemioticAnnotationBridge {
  annotations: SemioticAnnotation[];
  /** `count` is the finding total *before* any `max` cap, so
   *  `count > annotations.length` reveals a truncation. */
  meta: { count: number; kind: InputKind };
}

The default palette is an accessible blue / amber / red 3-stop, exported as DEFAULT_SEMIOTIC_PALETTE:

const DEFAULT_SEMIOTIC_PALETTE: Record<Severity, string> = {
  info: '#2563eb', // blue-600
  warning: '#d97706', // amber-600
  error: '#dc2626', // red-600
};

Routing files to inputs

These helpers turn raw files (bytes + filename + optional MIME type) into a DetectionInput, covering images, PDF, .pptx, .docx, notebooks, and code/prose — so a new input format is wired up once and every surface gets it.

interface FileInput {
  bytes: Uint8Array;
  filename: string;
  mimeType?: string;
}

type FileInputResult =
  | { input: SingleArtifactInput }
  | { error: string; reason: 'empty' | 'too_large' | 'unsupported' | 'unreadable' };

fileToInput / filesToInput

function fileToInput(file: FileInput, opts?: FileInputOptions): Promise<FileInputResult>;
function filesToInput(files: FileInput[], opts?: FileInputOptions): Promise<FileInputResult>;

fileToInput routes a single file; filesToInput accepts several (a set of chart images becomes one multi-image input). Both return either an input or a descriptive error + reason — they don't throw on bad input.

import { readFile } from 'node:fs/promises';
import { fileToInput, detectPitfalls } from 'datapitfalls';

const bytes = await readFile('./chart.png');
const routed = await fileToInput({ bytes, filename: 'chart.png' });
if ('error' in routed) throw new Error(routed.error);

const report = await detectPitfalls(routed.input, { apiKey });

FileInputOptions lets you force prose mode (forceText), set a fallback kind for unknown extensions (fallbackKind), and cap sizes (maxImageBytes, maxBinaryBytes, maxTextChars, maxImages).

fileToStage / textStage

Build chain stages for a whole-analysis scan:

function fileToStage(
  file: FileInput,
  opts?: FileInputOptions
): Promise<{ stage: ChainStage } | { error: string; reason: FileInputErrorReason }>;

function textStage(content: string, role?: string): ChainStage;

fileToStage reads a file into a stage with an auto-derived role label; textStage wraps free text (e.g. a pasted summary) as a stage.


Querying the taxonomy

The compiled pitfall catalog is queryable directly — useful for building UIs, docs, or your own grounding.

function getAllRules(): readonly PitfallRule[];
function ruleCount(): number;
function getRule(id: string): PitfallRule | undefined;
function getRulesByDomain(domain: Domain): PitfallRule[];
function getRulesBySeverity(severity: Severity): PitfallRule[];
function ruleCountsByDomain(): Record<Domain, number>;

Domain and Severity are exported types, with the runtime arrays DOMAINS and SEVERITIES. A PitfallRule carries id, name, domain, severity, description, detection_strategy, example_bad, example_good, remediation, and references (see docs/PITFALL_TAXONOMY.md).

import { ruleCountsByDomain } from 'datapitfalls';
console.log(ruleCountsByDomain()); // { 'Epistemic Errors': 6, ... }

Extracting slide decks

extractSlides

function extractSlides(data: Uint8Array): ExtractedSlides | { error: string };

interface ExtractedSlides {
  slides: SlideContent[];
}

Pulls per-slide text and embedded chart images from .pptx bytes, ready to pass as a SlidesDetectionInput. (fileToInput calls this for you when given a .pptx file.)


Constants

const VERSION: string; // the installed package version
const TAGLINE: string;
const DEFAULT_MODEL: string; // 'claude-sonnet-4-6'
const DOMAINS: readonly Domain[]; // the eight pitfall domains
const SEVERITIES: readonly Severity[]; // 'info' | 'warning' | 'error'