# Retest Issue

- **Method:** `POST`
- **Path:** `/api/v1/runs/{run_id}/issues/{issue_id}/retest`
- **Operation ID:** `retest_issue`
- **Tags:** artifact

Replay one issue's recorded path again, now, and keep the answer beside the run's own.

The same deterministic replay the run's reproduction ran: no persona and no model walks the path, two
attempts, the target's partition reset before each. When the failure does happen again, the claim check
the run's reproduction asks is asked once more, on the run's own reproduction provider. The issue and the
artifact keep what the run found; this record is where the new answer goes. A replay's steps are never
billed. Poll `GET .../retests/{retest_id}` until `status` is `completed` or `failed`.

## Effective servers

- `https://api.saram.sh`

  Saram

## Authentication

- **HTTPBearer**: HTTP bearer

  a Saram API key (`sk_saram_<env>_...`) or a person's AuthKit access token, as `Authorization: Bearer <credential>`. Both are told apart by shape and produce the same principal.

## Path parameters

- **`run_id` (required)**: `string`
- **`issue_id` (required)**: `string`

## Request body

**Content type:** `application/json`

`object | null`, schema: `RetestRequest`, no additional properties

The optional body of `POST /runs/{id}/issues/{issue_id}/retest`.

- **`entry_url`**: `string | null`, maxLength: `2048`, pattern: `^https?://[^/?#\s]+`

  Where the replay opens instead of the run's own entry url - a new deployment of the same app, such as a preview. Every url the run recorded on its own origin is replayed on this url's origin, path for path, so the replay compares like with like; a path the new deployment no longer has reads as the path diverging, not as a fix. Absent replays against the run's own target.

**Example:**

```json
{
  "entry_url": ""
}
```

## Responses

### 202 Successful Response

**Content type:** `application/json`

schema: `RetestRecord`, no additional properties

One retest of one issue: a later replay a person asked for, kept beside what the run concluded.

The issue itself - `GET /runs/{id}/issues/{issue_id}` - still says what the run's own reproduction found,
and so does the artifact: a retest never rewrites the run's record. Every retest is its own record.

- **`created_at` (required)**: `string`, format: `date-time`
- **`issue_id` (required)**: `string`
- **`retest_id` (required)**: `string`
- **`run_id` (required)**: `string`
- **`status` (required)**: `string`, possible values: `"pending", "running", "completed", "failed"`

  `pending` until the replay has a pool seat (it queues for one like any session), `running` while it
  replays, then `completed` with a `result`, or `failed` with an `error` and no result.
- **`workflow_id` (required)**: `string`
- **`attempts`**: `array`

  **Items:**

  schema: `RetestAttempt`, no additional properties

  One replay attempt of a retest: the session it ran in, what it expected at the judged step, and what it
  saw.
  - **`attempt_id` (required)**: `string`
  - **`expected` (required)**: `object`

    The outcome signature the run recorded at the judged step: `url_after` and the failure `markers`.
  - **`issue_id` (required)**: `string`
  - **`outcome` (required)**: `string`

    `reproduced`, `not_reproduced`, `path_diverged`, `target_moved` or `error`.
  - **`session_id` (required)**: `string`

    The throwaway body session the attempt ran in: `<run_id>.<retest_id>_<issue_id>_a<n>`, never one the run's
    own reproduction used, so its evidence sits beside the original's rather than over it.
  - **`notes`**: `string`, default: `""`
  - **`observed`**: `object | null`
  - **`screenshot_refs`**: `array of string`
  - **`surface_trace_ref`**: `string | null`
- **`check`**: `object | null`, schema: `Check`, no additional properties

  The claim check, present only when a marker matched and it was asked.
  - **`verdict` (required)**: `string`

    Schema: `Verdict1`. Known values, open (more may be added): `supported`, `unsupported`, `ungrounded`, `skipped`.

    `supported`: the checker answered yes with a quote that gates against the ledger text of the cited steps - the only verdict that lets `status` read `verified`. `unsupported`: the checker answered no; the title claims more than the quoted line shows. `ungrounded`: the checker answered yes but its quote is not in the ledger, so the answer is discarded the way an ungrounded diary quote is and recorded in the gate ledger. `skipped`: no marker matched, or the check did not run. A classification: a later verdict is additive.
  - **`checked_by`**: `string | null`

    Schema: `CheckedBy`. Known values, open (more may be added): `judge`, `code`, `manual`.

    Which instrument answered: `judge` for the observer's checker model call under its own instructions, `code` for a mechanical rule, `manual` for a human. A classification.
  - **`quote`**: `string | null`
  - **`quote_step`**: `object | null`, schema: `QuoteStep`, no additional properties

    The step whose ledger block the quote came from.
    - **`session_id` (required)**: `string`
    - **`step_id` (required)**: `integer`
  - **`reason`**: `string | null`
- **`entry_url`**: `string | null`

  The deployment this replay opened, when it was not the run's own.
- **`error`**: `string | null`

  Why a `failed` retest has no result, in a sentence for the caller: the target's reset hook failing or
  refused before a replay, quoted with `target: ` in front; that Saram could not read the target's credential;
  or that Saram could not start or finish the retest. Never a finding about the product. Absent on every other
  status.
- **`finished_at`**: `string | null`, format: `date-time`
- **`gate_discards`**: `integer | null`

  How many claims this replay's gate threw away: claim-check quotes the cited evidence does not contain, each
  of which leaves `check` `ungrounded`, and attempts whose path diverged before the judged step.
  Zero is the expected value. Absent until `status` is `completed`, and on retests completed before this was
  recorded.
- **`gate_ref`**: `string | null`

  The records behind `gate_discards`, fetched through `GET /api/v1/blobs`: `{"run_id", "retest_id",
  "issue_id", "discards"}`, one discard per claim or attempt with its `session_id`, `kind`, `reason` and a
  `payload` naming the `issue_id` and the `quote` or the step the path diverged at. This retest's own, never the
  run's reproduction's. Written even when empty, so an absent ref is a retest that has not completed, never
  "nothing thrown away".
- **`requested_by`**: `string | null`

  The subject of the credential that asked: `key_...`, a person's user id, or `operator`.
- **`result`**: `string | null`

  Schema: `Result1`. Known values, open (more may be added): `reproduced`, `not_reproduced`, `flaky`, `skipped`.

  What this replay concluded, in `issue.reproduction.result`'s words. `not_reproduced`: the failure the run saw
  at the judged step did not happen again on either attempt - the answer to "is it fixed?" that is yes as far as
  this replay can tell, and not a proof the product works. `reproduced`: it happened again on both. `flaky`: on
  one of two. `skipped`: the replay could not rule (no browser pool, a session it could not open, a control that
  moved). Absent until `status` is `completed`.
- **`started_at`**: `string | null`, format: `date-time`
- **`verification`**: `object | null`, schema: `Verification`, no additional properties

  The `claim.verification` block this replay would have written onto the issue: `verified` only when the
  failure happened again and the claim check supported the title.
  - **`status` (required)**: `string`, possible values: `"verified", "marker_matched", "inconclusive", "unverified"`

    Schema: `Status`.

    Best-supported first. `verified`: a replay matched the failure marker at `action_step` AND the claim check answered `supported` with a verbatim quote from the cited steps (`check`) AND the failure is attributed to the product (`attribution`) - or `method` is `assertion` or `manual` and the same two conditions hold. This and only this may file a ticket alone, and `reproducible` is true exactly when this reads `verified`. `marker_matched` (0.5): a replay matched the failure marker at `action_step`; the claim as titled is unchecked, unsupported, or the check could not quote. The step failed again; whether it failed for the stated reason is not established, so it never files alone. `inconclusive`: a check ran and settled nothing (the marker did not appear again, the two attempts disagreed) - not a refutation. `unverified`: nothing checked it, because there was nothing to check, reproduction never ruled, or the finding is a tester error. A wrong total or a select that resets is `unverified` by construction, not disproven. Default and safe reading when the block is absent. This enum drives an action - it is the ticket-filing gate, like `issue.severity` - so it stays closed for readers too and adding a member is a breaking change: a gate must refuse a value it cannot interpret rather than guess at one, which is why `marker_matched` made 0.5 a new family.
  - **`markers`**: `(array of string) | null`
  - **`method`**: `string | null`

    Schema: `Method`. Known values, open (more may be added): `marker`, `assertion`, `manual`.

    Which check ran, when one did; absent when `status` is `unverified`. `marker`: a replay compared the failure marker at `action_step` - name it in `markers`. A marker matching is evidence the step failed again, not that it failed for the reason the title gives, so on its own it produces `marker_matched`, and `verified` only with `check.verdict: supported` and `attribution: product`. `assertion`: a state assertion checked the claim itself (reserved; not produced yet). `manual`: a human checked it. A classification: nothing acts on it - every surface shows `status` - so a reader on an older release buckets an instrument it does not know as `unknown`, and adding one is additive.
  - **`note`**: `string | null`

**Example:**

```json
{
  "retest_id": "",
  "run_id": "",
  "issue_id": "",
  "status": "pending",
  "entry_url": "",
  "requested_by": "",
  "workflow_id": "",
  "result": "reproduced",
  "verification": {
    "status": "verified",
    "method": "marker",
    "markers": [
      ""
    ],
    "note": ""
  },
  "check": {
    "verdict": "supported",
    "quote": "",
    "quote_step": {
      "session_id": "",
      "step_id": 1
    },
    "reason": "",
    "checked_by": "judge"
  },
  "attempts": [
    {
      "attempt_id": "",
      "issue_id": "",
      "session_id": "",
      "outcome": "",
      "expected": {
        "additionalProperty": "anything"
      },
      "observed": {
        "additionalProperty": "anything"
      },
      "notes": "",
      "surface_trace_ref": "",
      "screenshot_refs": [
        ""
      ]
    }
  ],
  "gate_discards": 1,
  "gate_ref": "",
  "error": "",
  "created_at": "",
  "started_at": "",
  "finished_at": ""
}
```

### 403 the API key lacks the required scope

### 422 Validation Error

**Content type:** `application/json`

schema: `HTTPValidationError`

- **`detail`**: `array`

  **Items:**

  schema: `ValidationError`
  - **`loc` (required)**: `array of (string | integer)`
  - **`msg` (required)**: `string`
  - **`type` (required)**: `string`
  - **`ctx`**: `object`
  - **`input`**: `any`

**Example:**

```json
{
  "detail": [
    {
      "loc": [
        ""
      ],
      "msg": "",
      "type": "",
      "input": null,
      "ctx": {}
    }
  ]
}
```

## Schemas

- `Check` — shown above.

### CheckedBy

**Type:** `string`

Known values, open (more may be added): `judge`, `code`, `manual`.

Which instrument answered: `judge` for the observer's checker model call under its own instructions, `code` for a mechanical rule, `manual` for a human. A classification.

**Example:**

```json
"judge"
```

**Example:**

```json
"code"
```

**Example:**

```json
"manual"
```

- `HTTPValidationError` — shown above.

### Method

**Type:** `string`

Known values, open (more may be added): `marker`, `assertion`, `manual`.

Which check ran, when one did; absent when `status` is `unverified`. `marker`: a replay compared the failure marker at `action_step` - name it in `markers`. A marker matching is evidence the step failed again, not that it failed for the reason the title gives, so on its own it produces `marker_matched`, and `verified` only with `check.verdict: supported` and `attribution: product`. `assertion`: a state assertion checked the claim itself (reserved; not produced yet). `manual`: a human checked it. A classification: nothing acts on it - every surface shows `status` - so a reader on an older release buckets an instrument it does not know as `unknown`, and adding one is additive.

**Example:**

```json
"marker"
```

**Example:**

```json
"assertion"
```

**Example:**

```json
"manual"
```

- `QuoteStep` — shown above.

### Result1

**Type:** `string`

Known values, open (more may be added): `reproduced`, `not_reproduced`, `flaky`, `skipped`.

What the replay showed. `reproduced`: the failure happened again. `not_reproduced`: it did not. `flaky`: the attempts disagreed. `skipped`: no replay ran. More values may be added; read one you do not know as unknown.

**Example:**

```json
"reproduced"
```

**Example:**

```json
"not_reproduced"
```

**Example:**

```json
"flaky"
```

**Example:**

```json
"skipped"
```

- `RetestAttempt` — shown above.
- `RetestRecord` — shown above.
- `RetestRequest` — shown above.

### Status

**Type:** `string`, possible values: `"verified", "marker_matched", "inconclusive", "unverified"`

Best-supported first. `verified`: a replay matched the failure marker at `action_step` AND the claim check answered `supported` with a verbatim quote from the cited steps (`check`) AND the failure is attributed to the product (`attribution`) - or `method` is `assertion` or `manual` and the same two conditions hold. This and only this may file a ticket alone, and `reproducible` is true exactly when this reads `verified`. `marker_matched` (0.5): a replay matched the failure marker at `action_step`; the claim as titled is unchecked, unsupported, or the check could not quote. The step failed again; whether it failed for the stated reason is not established, so it never files alone. `inconclusive`: a check ran and settled nothing (the marker did not appear again, the two attempts disagreed) - not a refutation. `unverified`: nothing checked it, because there was nothing to check, reproduction never ruled, or the finding is a tester error. A wrong total or a select that resets is `unverified` by construction, not disproven. Default and safe reading when the block is absent. This enum drives an action - it is the ticket-filing gate, like `issue.severity` - so it stays closed for readers too and adding a member is a breaking change: a gate must refuse a value it cannot interpret rather than guess at one, which is why `marker_matched` made 0.5 a new family.

- `ValidationError` — shown above.

### Verdict1

**Type:** `string`

Known values, open (more may be added): `supported`, `unsupported`, `ungrounded`, `skipped`.

`supported`: the checker answered yes with a quote that gates against the ledger text of the cited steps - the only verdict that lets `status` read `verified`. `unsupported`: the checker answered no; the title claims more than the quoted line shows. `ungrounded`: the checker answered yes but its quote is not in the ledger, so the answer is discarded the way an ungrounded diary quote is and recorded in the gate ledger. `skipped`: no marker matched, or the check did not run. A classification: a later verdict is additive.

**Example:**

```json
"supported"
```

**Example:**

```json
"unsupported"
```

**Example:**

```json
"ungrounded"
```

**Example:**

```json
"skipped"
```

- `Verification` — shown above.
