# Catalog Registration API

## 1. Purpose

`POST /api/v1/catalog-items` registers a long-lived SQL dossier before a Finding exists. Use it when
the SQL comes from a customer ticket, is high frequency but not currently abnormal, or deserves
proactive observation for another reason.

This endpoint does not report an anomaly. It creates no Finding, Symptom, metric observation, or
synthetic evidence. External systems that have actually detected an abnormal event must use the
[Finding Intake API](finding-intake.md) instead.

## 2. Identity and Duplicate Behavior

A Catalog Item is unique by:

```text
org_id + cluster_id + sql_digest
```

If this identity already exists, the API returns `409 catalog_item_exists` and includes the existing
`catalog_item`. Clients should open that dossier instead of creating a duplicate.

## 3. Request

```http
POST /api/v1/catalog-items
Authorization: Bearer <access-token>
Content-Type: application/json
```

| Field | Required | Type | Constraints and meaning |
|---|---:|---|---|
| `org_id` | Yes | string | Organization identity, up to 128 characters. |
| `cluster_id` | Yes | string | Cluster identity, up to 128 characters. |
| `project_id` | No | string | Optional project identity, up to 128 characters. |
| `sql_digest` | Yes | string | SQL digest, up to 256 characters. |
| `normalized_sql` | No | string | Normalized SQL text, up to 200,000 characters. |
| `registration_reason` | Yes | enum | `customer_ticket`, `high_frequency`, `proactive_watch`, or `other`. |
| `initial_action` | No | enum | `register_only`, `start_review`, or `watch`; defaults to `register_only`. |
| `source_reference` | No | string | Ticket, task, or other source identifier, up to 512 characters. |
| `source_url` | No | URL | Absolute HTTP or HTTPS source link, up to 2,000 characters. |
| `note` | Conditional | string | Registration context, up to 10,000 characters. Required for `other`. |
| `owner` | No | string | Initial dossier owner, up to 128 characters. |
The registering actor is always derived from the authenticated user. Client-supplied audit identity
fields are ignored.

Unknown fields are rejected. Callers cannot set `status`, `severity`, Finding timestamps, counts, or
other system-owned facts.

## 4. Derived Initial Posture

The server derives the read-only `current_posture` from the registration Timeline events:

| Initial action | `current_posture.state` |
|---|---|
| `register_only` | `registered` |
| `start_review` | `reviewing` |
| `watch` | `watching` |

Until a Finding arrives:

- `current_posture.risk` is `null`, meaning no Finding-side abnormal risk signal is available.
- `first_seen_at`, `last_seen_at`, and `latest_finding_at` are `null`.
- Finding and Symptom counts are zero.
- The Catalog timeline contains a manual registration milestone with the reason and source context.

## 5. Customer Ticket Example

```json
{
  "org_id": "org-example-001",
  "cluster_id": "cluster-example-001",
  "sql_digest": "abc123",
  "normalized_sql": "select * from orders where customer_id = ?",
  "registration_reason": "customer_ticket",
  "initial_action": "start_review",
  "source_reference": "SUPPORT-4821",
  "source_url": "https://support.example.com/tickets/SUPPORT-4821",
  "note": "Customer asked us to review this query's resource usage."
}
```

## 6. High-frequency SQL Example

```bash
curl -sS http://127.0.0.1:8765/api/v1/catalog-items \
  -H "authorization: Bearer $SQL_CATALOG_ACCESS_TOKEN" \
  -H 'content-type: application/json' \
  -d '{
    "org_id": "org-example-001",
    "cluster_id": "cluster-example-001",
    "sql_digest": "high-frequency-digest",
    "normalized_sql": "select name from products where category_id = ?",
    "registration_reason": "high_frequency",
    "initial_action": "watch",
    "note": "No current anomaly; retain this SQL for longitudinal observation."
  }'
```

## 7. Responses

Successful creation returns `201`:

```json
{
  "created": true,
  "catalog_item": {
    "id": "cat_...",
    "lifecycle": "active",
    "current_posture": {
      "state": "watching",
      "risk": null,
      "last_known_risk": null,
      "historical_peak_risk": null,
      "attention": "normal",
      "reasons": [],
      "next_action": "monitor",
      "unreviewed_finding_count": 0,
      "active_treatment_count": 0
    },
    "first_seen_at": null,
    "latest_finding_at": null,
    "finding_count": 0,
    "registration": {
      "origin": "manual",
      "reason": "high_frequency",
      "initial_action": "watch",
      "registered_by": "usr_...",
      "registered_at": "2026-07-14T06:40:16Z"
    }
  }
}
```

Invalid input returns `400 invalid_catalog_item`. Duplicate identity returns `409`:

```json
{
  "error": {
    "code": "catalog_item_exists",
    "message": {
      "key": "api:errors.catalog_item_exists",
      "text": "This SQL digest is already registered for the selected cluster."
    },
    "details": {
      "catalog_item": { "id": "cat_existing" }
    }
  }
}
```

Send `Accept-Language: zh-CN` to receive Chinese product-owned messages. Domain codes and user data
remain unchanged; see [Internationalization](../i18n.md).

## 8. Later Finding Behavior

When a Finding later arrives with the same identity, Finding intake links it to this Catalog Item.
The server then fills the first/latest observation fields and derives current risk from the new
Finding. The Finding has `is_first_observation=true` and `is_new_digest=false`: it is the first
observed abnormal event, but the dossier was already registered.

The Finding is evaluated against the selected Cluster's review-admission policy. With the system
default, one Critical Finding enters `needs_review` immediately; a lower-severity Finding remains
`registered` until at least five Findings occur in a rolling 14-day window. A manually started review
still enters `reviewing`, and an explicit `watch` registration remains under observation until stronger
governance facts supersede it.

The first Finding is not marked `reappeared`; that signal requires an earlier Finding history. If the
Finding is later deleted as erroneous, the manually registered dossier remains visible and returns to
its original Finding-free posture and initial handling state.
