# Response example validation report

> **Archived 2026-05.** Point-in-time snapshot from 2026-04-28. The validator that
> produced this report (`scripts/validate_response_examples.py`) is still active —
> regenerate the current state with `uv run poe validate-response-examples`. See
> [`docs/upstream-specs/README.md`](../../upstream-specs/README.md) for the full
> spec-audit workflow. The text below is preserved verbatim.

Generated by `poe validate-response-examples` (script:
`scripts/validate_response_examples.py`). Each inline response example in
`docs/upstream-specs/readme-portal.yaml` (the README.io developer portal's OpenAPI spec,
refreshed by `poe refresh-upstream-spec`) is validated against our local response schema
for the same `(path, method, status_code)`.

The live OpenAPI spec at `https://api.katanamrp.com/v1/openapi.json` defines no response
shapes, so it can't tell us whether our local response schemas (reverse-engineered from
observed behaviour) are right. The README.io developer portal's spec, on the other hand,
has inline response examples authored by Katana's docs team — this validator uses those
as ground truth and flags drift in our local schemas.

## Status (after fixes)

- 9 initial failures triaged into three buckets.
- **3 real schema drift items fixed** in this PR (regen output reflects).
- **4 documentation-artefact failures remain** (5 individual errors); these are Katana
  docs with stale/wrong example data — verified against the live API or CLAUDE.md.

## Fixes applied (real schema drift)

### `StockAdjustmentBatchTransaction.batch_id` — now nullable

Scraped examples include `"batch_id": null`; local schema previously required non-null
integer. Behaviour confirmed by the docs example for `POST /stock_adjustments` showing a
transaction with `batch_id: null` next to a real batch transaction (Katana allows mixing
batched and unbatched adjustments in the same row). Schema relaxed to `[integer, null]`
and description updated to explain when `null` appears.

### `ProductOperationRow.cost_parameter` / `.planned_time_parameter` — now numeric

Scraped examples have numeric values (`15`, `7200`); local schema typed both as
`string|null`. Verified against the live API: `GET /v1/product_operation_rows` returns
either a JSON number or `null` for both fields. Schema corrected to `number|null`.

While there, tightened `ProductOperationRow.type` from plain `string` to a `$ref` of the
existing `ManufacturingOperationType` enum (`[process, setup, perUnit, fixed]`) — the
field was already enum-constrained on the request side via
`UpdateProductOperationRowRequest`, the response side just hadn't been tightened.

### `PATCH /stock_transfers/{id}/status` — fixed by wrapper-strip

The remaining failures here were a parsing artefact, not real drift: the scraped example
was wrapped in `{"example": {...}}` (README.io's labelled-example markup). Adding the
`_unwrap_labeled_example` heuristic to the validator strips the wrapper before
validation; schema is fine.

## Closed as no-op (documentation artefacts)

These remain in the failure report because the tool is comparing examples against
schemas; the *schemas are correct* (verified) and the *docs are wrong*. We're not
chasing them in this PR.

### `GET /bin_locations` — example uses `name` instead of `bin_name`

Live API returns `bin_name` (verified by integration tests in
`tests/test_real_api.py::TestSchemaValidation`). The doc example uses an old field name
that's been deprecated. Same root cause as the `StorageBinListResponse.example` fix in
PR #420.

### `GET /operators`, `GET /serial_numbers` — example is a raw array

Both scraped examples show a raw `[...]` array; our local schema wraps in
`{"data": [...]}`. CLAUDE.md is explicit: "Katana wraps ALL list responses in
`{data: [...]}`. The only documented exception is `/user_info`." The integration test
`tests/test_real_api.py::TestSchemaValidation::test_operator_endpoint_validates`
exercises both endpoints against the live API and confirms the wrap. Doc examples are
wrong.

### `GET /purchase_order_accounting_metadata` — example uses camelCase keys

Doc example shows `createdAt`, `purchaseOrderId`, `integrationType`,
`porReceivedGroupId`, `billId`. Verified against the live API — actual response uses
snake_case (`purchase_order_id`, `integration_type`, `received_items_group_id`,
`bill_id`, `created_at`). Local schema is correct; doc is wrong.

## Raw findings (post-fix)

```
- Markdown files scanned: 249
- Response examples extracted: 685
- Examples validated: 122
- Examples skipped (no local schema): 10
- Examples skipped (non-2xx): 553
- Examples skipped (invalid JSON): 67
- Failures: 5  (all documentation-artefacts; see "Closed as no-op" above)
```

The 67 invalid-JSON skips are response code blocks where Katana's docs contain
multi-line string values (e.g. error message bodies with embedded newlines) that
`json.loads` rejects. They aren't dropped silently — the counter surfaces them so audit
reproducibility is preserved.

To reproduce: `uv run poe validate-response-examples`.
