# ODCS — API: ODCS Over HTTP: The Transform Routes for Data Contracts

Direction first, because we publish it per format and it decides what you can build. The format key `odcs` appears in both of CoreModels' transform lists - decode (`jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | odm`) and encode (`jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | synapse`). ODCS round-trips. Its neighbors do not all manage that: `odm` decodes only (ODM entities are authored documentation, and we do not generate prose), and `synapse` encodes only (its output is plain draft-07 JSON Schema - re-import it as `jsonschema`). What `odcs` means here is a Bitol Open Data Contract Standard v3 document - `apiVersion: v3.1.0`, `kind: DataContract` - as YAML or JSON.

# ODCS Over HTTP: The Transform Routes for Data Contracts

Direction first, because we publish it per format and it decides what you can build. The format
key `odcs` appears in both of CoreModels' transform lists - decode (`jsonschema | shex | avro |
jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | odm`) and encode (`jsonschema |
shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | synapse`). ODCS
round-trips. Its neighbors do not all manage that: `odm` decodes only (ODM entities are authored
documentation, and we do not generate prose), and `synapse` encodes only (its output is plain
draft-07 JSON Schema - re-import it as `jsonschema`). What `odcs` means here is a Bitol Open Data
Contract Standard v3 document - `apiVersion: v3.1.0`, `kind: DataContract` - as YAML or JSON.

Four routes matter for the format, all under
`https://coremodels.example.com/graph/transform/...` (no `api/` prefix), all taking
`Authorization: Bearer $TOKEN` and `Content-Type: application/json`:

| # | Route | Role | Writes? |
|---|---|---|---|
| 1 | `POST graph/transform/schema/import/{projectId}` | Admin | yes |
| 2 | `POST graph/transform/schema/export/{projectId}` | Viewer | no |
| 3 | `POST graph/transform/schema/map/{projectId}` | Viewer (`ai`: Editor) | no |
| 4 | `POST graph/transform/plan/execute/{projectId}` | Viewer | no |

Every response uses one envelope - `success`, `lossiness` (a list of `{kind, path, explanation}`
records), `errors`, and a payload field (`projectId`, `schema`, and on the mapping routes `plan`).
`success: true` means the call ran; the ledger is where the honesty lives. There is also
`schema/mapImport`, which maps an incoming schema onto the project's existing model with a
`dryRun` flag - run the dry-run, read the ledger, then write.

## 1. Import: a contract becomes a governed model

The body is `{ "format": "odcs", "schema": "<the contract text>" }`, plus an optional `spaces`
array. With the `orders` contract from our quickstart (one schema object, six properties):

```bash
jq -n --rawfile s orders.odcs.yaml '{ format: "odcs", schema: $s }' \
| curl -sS -X POST \
    "https://coremodels.example.com/graph/transform/schema/import/$PROJECT_ID" \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    --data-binary @-
```

The project gains a Type `orders` with six Elements. Labels keep the wire names (`order_id`,
`placed_at`); internal node ids are camelCase alphanumerics (`ordersOrderId`,
`ordersPlacedAt`), because CoreModels ids must be. Logical types land as data types - `string`,
`number` → Double, `integer`, `boolean`, `timestamp` → DateTime - and each property's
`required: true` is persisted as a fact that survives a later export.

Now the honest scope statement, and it is the one thing to internalize about this route. During a
transform, everything else in the contract - the head (`id`, `version`, `status`, `domain`), the
physical names and types, primary-key markers, quality checks, the `servers` block - rides a
format-specific preservation channel inside the transform layer, which is what makes the stateless
round trip in route 3 exact. The project graph persists the structural model, not that channel. So
importing a contract gives you its *shape* under governance; it does not turn the project into a
byte-faithful archive of the contract document. Keep the contract file as the source of record and
treat the project as the semantic model. The response ledger tells you this at import time: the
decode-side entries for `servers` and `quality` arrive with the response.

## 2. Export: a model becomes a contract

The reverse needs only the format key - there are no ODCS-specific request options (`vendor`
belongs to `sql`, the `synapse*` keys to `synapse`):

```bash
curl -sS -X POST \
  "https://coremodels.example.com/graph/transform/schema/export/$PROJECT_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "format": "odcs" }'
```

ODCS is a text format, so `schema` in the response is a YAML string. The interesting part is the
contract head. The spec requires `id`, `version`, and `status` on every contract, and a CoreModels
project that never saw ODCS has none of them - so the encoder mints a deterministic, spec-valid
head: `apiVersion: v3.1.0`, `kind: DataContract`, the model's id as `id`, `version: 1.0.0`,
`status: active`. A model whose types and elements were built by hand exports like this:

```yaml
apiVersion: v3.1.0
kind: DataContract
id: invoice
name: Invoice
version: 1.0.0
status: active
schema:
  - name: invoice
    properties:
      - name: number
        logicalType: string
        required: true
      - name: total
        logicalType: number
```

That is a valid starting draft, not a publishable contract - the minted head says so
(`version: 1.0.0`, `status: active` are defaults, not decisions). There are no request keys on
this surface to set the contract id, version, or status; edit the head in review, which is where
contract identity belongs anyway. Model constructs ODCS cannot express are declared rather than
smuggled: a taxonomy (controlled list) has no ODCS construct, so an element referencing one
exports as a plain `string` property with a `ConstraintRelaxation` in the ledger naming the
taxonomy.

## 3. Map: modernize a legacy contract, statelessly

`schema/map` is the engine surface: decode, plan against a target hint, validate through the
universal gate, execute, encode - the project is never touched. Here is a real ODCS-to-ODCS use
for it: a contract written against the v2 line, mapped onto the shape of your governed v3
contract. The legacy document, `legacy.odcs.yaml`:

```yaml
apiVersion: v2.2.1
kind: DataContract
id: legacy-orders
name: legacy_orders
version: 0.9.0
status: draft
schema:
  - name: orders
    properties:
      - name: order_id
        logicalType: string
        required: true
      - name: order_total
        logicalType: number
        required: true
      - name: placed_at
        logicalType: time
      - name: legacy_flag
        logicalType: geometry
```

With the governed `orders.odcs.yaml` from route 1 as the hint:

```bash
jq -n --rawfile s legacy.odcs.yaml --rawfile h orders.odcs.yaml '{
  sourceFormat: "odcs",
  sourceSchema: $s,
  targetFormat: "odcs",
  targetHintFormat: "odcs",
  targetHintSchema: $h,
  mapping: { kind: "inferred" }
}' \
| curl -sS -X POST \
    "https://coremodels.example.com/graph/transform/schema/map/$PROJECT_ID" \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    --data-binary @-
```

The produced contract:

```yaml
apiVersion: v3.1.0
kind: DataContract
id: legacy-orders
name: legacy_orders
version: 0.9.0
status: draft
schema:
  - name: orders
    properties:
      - name: order_id
        logicalType: string
        required: true
      - name: order_total
        logicalType: number
        required: true
      - name: placed_at
        logicalType: time
```

Three decisions are visible, and the seven-entry ledger accounts for every one (two of the seven
belong to the governed hint, whose own `servers` block and quality check travel the same decoder
as any source). The `apiVersion`
was normalized - a non-v3 version is decoded with v3 semantics and re-emitted as `v3.1.0`, and
*both* sides of that say so (`SemanticNarrowing` at `apiVersion` on decode and again on encode).
The head otherwise follows the **source** contract, not the hint: `legacy-orders`, `0.9.0`,
`draft` are this contract's identity and they stay. `placed_at` kept its verbatim `time` - the
neutral model has no time-of-day primitive, so it was approximated as DateTime internally
(`TypeApproximation`, declared), but the encoder prefers the preserved wire type on re-emit. And
`legacy_flag` is gone, visibly: its `geometry` type drew a `TypeApproximation` on decode, and
because the governed hint has no counterpart, the plan neither mapped nor dropped it - so the
engine declared `StructuralDrop` at `Type[orders].ordersLegacyFlag`: "Element is a member of the
mapped type but no operation maps or drops it." Nothing vanished without a line item.

## 4. Replay: the plan is the artifact

Every `schema/map` response carries the executed plan. Store it; `plan/execute` runs it again -
same plan, same source, same output - with the plan passed **as a string**:

```bash
jq '.plan' map-result.json > modernize.plan.json

jq -n --rawfile s legacy.odcs.yaml --rawfile p modernize.plan.json '{
  sourceFormat: "odcs",
  sourceSchema: $s,
  plan: $p,
  targetFormat: "odcs"
}' \
| curl -sS -X POST \
    "https://coremodels.example.com/graph/transform/plan/execute/$PROJECT_ID" \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    --data-binary @-
```

The stored plan passes the same validation gate as every strategy; if the source contract drifts -
a renamed property, say - the gate fails loudly with the operation index and the id that no longer
resolves, instead of quietly emitting a thinner contract.

## Errors, exactly as they arrive

Failures come inside the envelope with a path and a message. The ODCS decoder's own vocabulary:

- Empty input - `$`: `The ODCS YAML document is empty.`
- A YAML scalar or list at the top level - `$`: `The document is valid YAML but is not an ODCS
  data contract (expected a mapping at the top level).`
- Broken YAML - `$`: `The document is not valid YAML: …` with the parser's detail.
- The wrong kind - `kind`: `The document's kind is 'CustomResource', not 'DataContract'; it is
  not an ODCS data contract.`
- A mapping with none of `kind`, `apiVersion`, or `schema` - `$`: `The document is valid YAML but
  is not an ODCS data contract (none of kind, apiVersion, or schema is present).`

Note what is *not* an error: unknown keys, out-of-enum logical types, missing required head
fields, quality checks, relationships. The decoder never hard-fails on a construct it can preserve
with declared lossiness - that is a design commitment, because contracts in the wild carry
tooling-specific extensions. One last boundary: the *data* routes (`data/import`, `data/export`,
`data/map`) move records in `json | csv | jsonld | sql | avro` - a contract governs records, but
an ODCS document is never itself a record format.

The transform API documentation collects all ten endpoints, roles, and ready-to-paste bodies for
every format.
