---
title: "Scan For Phd"
method: POST
path: "/felix/data-editing/scan-phd"
tags: ["felix"]
---

# Scan For Phd

`POST /felix/data-editing/scan-phd`

Scan dataset columns for Prompt Hack/Injection attempts (PHD).

Uses specialized model to detect prompt injection attempts.

Args:
    request.dataset_name: Dataset name.
    request.version: Optional version number. Returns latest if omitted.

## Request body

- DataEditingScanRequest — Request to scan dataset for PII or PHD
  - `dataset_name` string, required — Dataset name
  - `version` string, nullable — Version number (latest if omitted)
  - `columns` string[], nullable — Columns to scan. If not specified, scans all string columns.
  - `threshold` number — Detection threshold

## Response `200`

Successful Response

- DataEditingScanResponse — Response from PII/PHD scanning. A scan has two detection mechanisms of very different quality -- the GLiNER model, and a three-pattern regex (or seven-keyword) sweep for cells the model could not read -- and until ENG-6775 the response was identical either way. `findings_count: 0` therefore could not distinguish "the model read every cell and this dataset is clean" from "the model was down, and three regexes found nothing", which is the ENG-6537 false clean one layer up: the sweep stops an *unread* dataset reading as clean, but a sweep that also finds nothing returns to a bare zero. Attributes: success: Always true; a scan that cannot run reports `scan_status` rather than a non-200, so the heuristic findings are not discarded. dataset_name: The scanned dataset. dataset_version: The scanned version. scan_type: Which scan ran. scan_status: How much of the dataset the model actually read. See `ScanStatus`. unscanned_cells: How many cells the model could not read. Text cells among them were covered by the heuristic sweep instead; non-text cells were covered by nothing, since the sweep skips them while the model would have read them as strings. total_cells: How many cells were in scope, so `unscanned_cells` has a denominator. Without it the magnitude is unrecoverable: 3 of 4 is a failed scan and 3 of 10,000 is a footnote, and both report `unscanned_cells: 3`. findings_count: Total findings, model-derived and heuristic combined. affected_rows: Distinct rows carrying at least one finding. findings: The findings themselves. These do not individually say which mechanism produced them -- that is ENG-6878. phases: Per-phase wall-clock cost, when measured.
  - `success` boolean, required
  - `dataset_name` string, required
  - `dataset_version` string, required
  - `scan_type` 'pii' | 'phd', required
  - `scan_status` 'scanned' | 'degraded' | 'unavailable', required
  - `unscanned_cells` integer, required
  - `total_cells` integer, required
  - `findings_count` integer, required
  - `affected_rows` integer, required
  - `findings` PIIFinding[], required
    - `row_index` integer, required
    - `column` string, required
    - `entity_type` string, required
    - `text` string, required
    - `start` integer, required
    - `end` integer, required
    - `score` number, required
  - `phases` ScanPhaseTimings — Wall-clock cost of each phase of a scan, in milliseconds. A scan reports one latency, and that number cannot answer the question operators actually ask of it: was the time spent reaching the model, or in the model. The two have opposite remedies -- a slow resolve/load is storage or query work, a slow scan is inference concurrency -- so a single figure sends every investigation back to guessing. ENG-6537 records a 3m25s scan with no attribution at all, and the at-scale stress budget (90s) is calibrated against a total that mixes both. The split also has a second consumer: the client-abort stress probe has to abandon a scan *after* the process-wide admission gate is engaged, which is exactly ``pre_scan_ms`` later. Before this it used a static 5s guess that nothing verified (ENG-6723). Attributes: resolve_ms: Resolving the dataset name to a row. load_ms: Reading the dataset's records out of storage. prepare_ms: Column selection/validation and materialising the frame. scan_ms: The scan itself, including any regex fallback sweep.
    - `resolve_ms` number, required
    - `load_ms` number, required
    - `prepare_ms` number, required
    - `scan_ms` number, required
    - `pre_scan_ms` number, required — Return everything that happens before the scan can start. Serialised rather than left to the caller: it is the figure both consumers want, and summing three fields by hand is how a consumer silently omits one when a phase is added. Returns: Milliseconds spent resolving, loading and preparing.

## Other responses

- `422` — Validation Error

## Changes

- **2026-09-24** `1cffaad2a921` — 1 warning, 5 info
  - removed the optional property `detail` from the response with the `422` status
  - added the optional property `phases` to the response with the `200` status
  - added the required property `error` to the response with the `422` status
  - added the required property `scan_status` to the response with the `200` status
  - …2 more

[Change history](https://skmtc.dev/pioneer/apis/brain-api/changes/felix/data-editing/scan-phd/post.md)

---

[API](https://skmtc.dev/pioneer/apis/brain-api.md) · [All operations](https://skmtc.dev/pioneer/apis/brain-api/llms.txt) · [OpenAPI document](https://skmtc.dev/pioneer/apis/brain-api/revisions/1cffaad2a921?raw)
