
`import mapping` covers the one-line list-to-list translations that turn
typed query rows into contract values or JSON arrays. The intent is to
keep the row-to-response loop out of handlers so the response shape stays
in one place: the `json ... from ...` mapping declaration.

## Mapping typed rows

```text
import mapping

let summaries = mapping.map_rows(rows, lambda row: IssueRow: issue_summary(row))
```

`map_rows[Row, Out](rows, mapper)` returns a `List[Out]` with one entry
per input row. The contract assertion is "same length"; the mapper is
expected to be total.

## Mapping to JSON arrays

```text
import mapping

let issues_json = mapping.json_array_map(rows, lambda row: IssueRow: row_to_json(row))
return api_response.ok_array("issues", issues_json)
```

| Helper | Purpose |
|---|---|
| `json_array_map(rows, mapper)` | Maps to a `Json` array of objects. |
| `json_string_array_map(rows, mapper)` | Maps to a `Json` array of strings. |
| `json_object_fields(fields, values)` | `Json` object from parallel key and value lists. |
| `json_parse_or_empty_object(text)` | Empty object on parse failure (tolerant decode). |
| `json_parse_or_empty_array(text)` | Returns an empty array on parse failure. |

## Interaction with `json ... from ...`

The DSL form is preferred when the row-to-response shape is stable:

```text
json issue_summary from IssueRow as row projected by public_field_policy {
    id: row.id
    title: row.title
    severity: row.severity
}
```

Reach for `mapping` in these situations:

| Situation | Why the declarative form does not fit |
|-----------|---------------------------------------|
| Cross-row aggregation instead of per-row projection | `json ... projected by ...` is row-local. |
| Gluing typed rows into `api_response.ok_array` | The envelope wants a `List`; `mapping` builds it. |
| Migrating from hand-rolled loops | Bridge until shapes live in a `json` declaration. |

`ManualRowMappingRule` and `ManualResponseEnvelopeRule` are the check
rules that point new code at this module.
