# ODCS — Deep dive: Anatomy of the ODCS Coder: What Maps, What Rides, What's Declared

Where does `servers:` go?

# Anatomy of the ODCS Coder: What Maps, What Rides, What's Declared

Where does `servers:` go?

It is the right first question for any tool that claims to convert data contracts. An ODCS
document is two things braided together: a schema (objects, properties, types, keys) and a
contract around it (servers, quality, SLAs, team, pricing). CoreModels converts every format
through one neutral intermediate representation (IR), and the IR models schemas - what SQL,
Avro, LinkML, and JSON Schema can agree on. So the coder splits three ways: constructs the IR
models **structurally**, facts that **ride alongside** for exact re-emit, and everything that
gets **declared** in the lossiness ledger. This is the complete anatomy, in that order.

## The structural mapping

Import is decode (ODCS YAML or JSON → IR; JSON is a YAML subset, so one parser serves both).
Export is encode (IR → ODCS YAML). The spine:

| ODCS construct | IR construct | Notes |
|---|---|---|
| the contract document | the schema node | head fields ride schema-level extras |
| `schema[]` object | Type (label = `name`) | ids sanitized to camelCase alphanumerics |
| property | Element (id = objectId + PropertyName) | label keeps the wire name |
| `logicalType: string` | String | |
| `logicalType: number` | Double | |
| `logicalType: integer` | Integer | |
| `logicalType: boolean` | Boolean | |
| `date` / `timestamp` | DateTime | |
| `time` | DateTime + declared approximation | the IR has no time-of-day primitive |
| `required: true` | Required | ODCS's own flag is authoritative |
| `logicalType: array` | collection cardinality | bounds from `logicalTypeOptions.minItems`/`maxItems` |
| object property / object array items | child Type + type reference | re-nested on encode |
| `primaryKey`, `unique`, `partitioned` | extras, not structure | see below |

Two rows deserve fine print. Names: CoreModels node ids must be alphanumeric, so `txn_ref_dt` on
object `tbl` becomes element id `tblTxnRefDt` while the label stays `txn_ref_dt` and the emitted
contract uses the original name; id collisions (two objects both named `a`) are suffixed (`a`,
`a2`) rather than merged. Collection bounds: `minItems`/`maxItems` become real IR cardinality
bounds - they travel into formats that can hold them (JSON Schema output gets `"minItems": 1,
"maxItems": 100`) - and a nonsensical pair (`minItems` above `maxItems`) is ignored rather than
propagated.

And the opening question: `servers` - with `team`, `roles`, `slaProperties`, `support`, `price`,
and any key the coder does not recognize at all - is captured verbatim as JSON under
`odcs.raw.<key>`, declared once in the ledger, and re-emitted exactly when the target is ODCS. A
decode never hard-fails on a construct it can preserve.

## The extras channel

Every IR node carries an annotation bag; the ODCS coder writes the facts the IR cannot express
into dotted `odcs.*` keys. The namespace, grouped:

| Keys | Where | What they hold |
|---|---|---|
| `odcs.apiVersion`, `odcs.id`, `odcs.name`, `odcs.version`, `odcs.status`, `odcs.domain`, `odcs.tenant`, `odcs.dataProduct` | schema | the contract head |
| `odcs.description.purpose` / `.limitations` / `.usage`, `odcs.description.raw.*` | schema | the description block, unknown description keys verbatim |
| `odcs.stableId`, `odcs.businessName`, `odcs.physicalName`, `odcs.physicalType`, `odcs.description`, `odcs.dataGranularityDescription` | type/element | identity and physical facts |
| `odcs.logicalType`, `odcs.logicalTypeOptions`, `odcs.itemsLogicalType`, `odcs.items.raw.*` | element (`odcs.logicalType` also on type) | the verbatim wire types and options |
| `odcs.primaryKey`, `odcs.primaryKeyPosition`, `odcs.unique`, `odcs.partitioned`, `odcs.partitionKeyPosition`, `odcs.classification`, `odcs.encryptedName`, `odcs.criticalDataElement` | element | key, partition, and sensitivity markers |
| `odcs.transformLogic`, `odcs.transformDescription`, `odcs.transformSourceObjects`, `odcs.examples` | element | provenance and samples |
| `odcs.tags`, `odcs.customProperties`, `odcs.authoritativeDefinitions`, `odcs.quality`, `odcs.relationships` | type/element (at the contract root these keys ride `odcs.raw.*`) | preserved verbatim as JSON |
| `odcs.parent` | type | marks a nested type's enclosing object |
| `odcs.raw.*` | any | any unmodeled wire key, verbatim JSON |

The encoder reads all of it back in a fixed order, emitting preserved JSON payloads in YAML flow
style - a round-tripped contract shows `quality: [{"metric":"rowCount",...}]` where the original
had block YAML: same data, spec-valid, deterministic. Determinism is total: fixed key order,
two-space indent, and a quoting discipline under which a string that would re-parse as a number
or boolean gets quoted while `varchar(18)` stays plain - so encode after the first round trip is
a byte-for-byte fixed point, and diffs of generated contracts are meaningful.

**`primaryKey` does not imply `required`.** ODCS carries its own `required` flag, so the coder
believes it: `primaryKey: true` with `required: false` decodes as an *optional* element,
`odcs.primaryKey` riding in extras. (Our OSI import, whose format lacks a separate flag, treats
a primary key as required; the ODCS coder deliberately does not.) Convert such a contract to SQL
and the key column comes out nullable - the contract said so.

## The head, minted or preserved

The spec requires `id`, `version`, and `status` on every contract, so the encoder resolves each
head field in order: the preserved wire value if the IR came from ODCS, else a deterministic
default - `apiVersion: v3.1.0` (the current release), `kind: DataContract` (the only valid
kind), the IR schema's id, `version: 1.0.0`, `status: active`. Output is always spec-valid, even
from an IR that never saw a contract; an empty model encodes as a head plus `schema: []`. A
contract *missing* required head fields decodes anyway, with a `SemanticNarrowing` noting that a
re-encode will mint defaults.

`apiVersion` gets a symmetric policy: a non-v3 version (say `v2.2.1`) is decoded with v3
semantics and declared; on encode it is replaced with `v3.1.0` and declared again. Both records
appear on a full trip - the modernization happened in two places, so it is visible twice.

## Verbatim types beat mapped types on the way out

For scalars, the encoder prefers the preserved wire `logicalType` over re-deriving one from the
IR primitive:

- `date` decodes as DateTime but re-emits as `date`, not `timestamp`. Likewise `time` - an
  approximation on decode, restored verbatim on encode.
- An out-of-enum type like `geometry` decodes as String with a `TypeApproximation` and re-emits
  as `geometry`: textual fidelity wins on the round trip, and the decode-side ledger entry is
  the record that the IR held it coarsely in between.
- IR that never saw ODCS gets defaults: Integer → `integer`, Double → `number`, Boolean →
  `boolean`, DateTime → `timestamp`, everything else → `string` (RichText degrades, declared).

Collections and type references override all of that: they always emit `logicalType: array` and
`logicalType: object` respectively.

## Nesting, both directions

An object-typed property, or an array of objects, becomes a child Type marked with
`odcs.parent` - the treatment we use for Protobuf and Avro nested types. Ids compose from the
element: `tbl.line_items` with object items produces child type `tblLineItemsItem`, whose
properties become `tblLineItemsItemSku` and so on. On encode, a type carrying `odcs.parent` that
some element references is re-nested inside that property rather than emitted top-level - for an
array of objects, as an `items:` block with `logicalType: object` and the child's properties; for
an object-typed property, as inline `properties` under `logicalType: object`. Two safety valves: an *orphaned*
nested type (parent marker, nothing referencing it) falls back to top-level emission so it is
never dropped; a *shared* reference (two elements pointing at one type - ODCS has no `$ref`) is
flattened to an inline object with a `SemanticNarrowing` naming the flattening, while the target
also emits as its own schema object.

One round-tripped property shows the whole story - child type re-nested, bounds restored,
preserved options in JSON flow style:

```yaml
- name: line_items
  logicalType: array
  logicalTypeOptions: {"minItems":1,"maxItems":100}
  items:
    logicalType: object
    properties:
      - name: sku
        logicalType: string
        required: true
      - name: quantity
        logicalType: integer
```

## The lossiness inventory

Decode side - everything is a warning, nothing is fatal:

| Trigger | Kind | Behavior |
|---|---|---|
| unknown key anywhere | SemanticNarrowing | preserved under `odcs.raw.*`, one summarized record per node |
| `quality` / `relationships` | SemanticNarrowing | preserved verbatim, declared per occurrence |
| `logicalType: time` | TypeApproximation | DateTime, verbatim type preserved |
| out-of-enum `logicalType` | TypeApproximation | String, verbatim type preserved |
| property with no `logicalType` | TypeApproximation | String - **one summarized record for the whole contract** |
| array with no `items` | TypeApproximation | item type approximated as String |
| array items of type `array` or out-of-enum | TypeApproximation | String items, item definition preserved verbatim |
| object property with no `properties` | TypeApproximation | String, verbatim `logicalType: object` preserved |
| `items`/`properties` on a scalar property | SemanticNarrowing | preserved under `odcs.raw.*` |
| schema object with non-`object` `logicalType` | SemanticNarrowing | preserved verbatim |
| non-v3 `apiVersion` | SemanticNarrowing | decoded with v3 semantics |
| missing required head fields | SemanticNarrowing | re-encode will mint defaults |

Encode side - IR constructs ODCS cannot express, declared rather than smuggled:

| IR construct | Kind | Behavior |
|---|---|---|
| taxonomy (controlled list) | ConstraintRelaxation | ODCS has no enum construct; not represented |
| element referencing a taxonomy | ConstraintRelaxation | approximated as `string` |
| type inheritance | SemanticNarrowing | no inheritance in ODCS |
| free-standing relations | SemanticNarrowing | only verbatim-preserved `odcs.relationships` re-emit |
| components/projections | StructuralDrop | no counterpart |
| element owned by no type | StructuralDrop | ODCS has no free-standing properties |
| RichText primitive | TypeApproximation | degrades to `string` |
| nullable flag | ConstraintRelaxation | `required` models nullability only; null-vs-absent is not representable |
| unresolvable type reference | TypeApproximation | emitted without nested properties (or `string` items) |

The enum story is ODCS's sharpest structural limit, so here it is concretely. A model imported
from this table:

```sql
CREATE TABLE Customer (
  customer_key INTEGER NOT NULL,
  full_name VARCHAR(255) NOT NULL,
  signup_date TIMESTAMP,
  status ENUM('draft','active','closed')
);
```

encodes to a contract whose schema body (head omitted) is:

```yaml
schema:
  - name: Customer
    properties:
      - name: customer_key
        logicalType: integer
        required: true
      - name: full_name
        logicalType: string
        required: true
      - name: signup_date
        logicalType: timestamp
      - name: status
        logicalType: string
```

with two `ConstraintRelaxation` entries: one for the taxonomy ("ODCS has no enum/controlled-list
construct"), one for the element referencing it. The three allowed values exist in your source,
not in the contract - and the ledger names the taxonomy that was relaxed.

## Round-trip edge cases, pinned

**An itemless array gains items.** `logicalType: array` with no `items` decodes as a String
collection (declared); the re-encode emits an explicit `items` with `logicalType: string`. The
first pass changes the text; every pass after it is a fixed point.

**An array of arrays keeps its text.** The IR cannot hold nested collections, so the items
approximate as String (declared) - but `odcs.itemsLogicalType: array` rides along and the
re-encode emits `logicalType: array` items verbatim. Textual fidelity survives a structural
approximation.

**Rejection is reserved for non-contracts.** Empty input; a scalar or sequence at the root;
unparseable YAML; a `kind` other than `DataContract`; a mapping with none of `kind`,
`apiVersion`, or `schema`. These five fail with a path and message; everything else decodes.

**The fixed-point invariant, stated precisely.** For a contract built from modeled constructs -
head, description, physical facts, key markers, quality, relationships, custom properties, tags,
nested objects, arrays with bounds, unknown sections - decode → encode reproduces an equivalent
spec-valid document, decode → encode → decode yields a structurally identical IR, and one more
encode is byte-identical. Our test suite asserts exactly that, and it is what makes the coder
safe in a pipeline: the contract is not merely parseable but *reconstructible*, with every
deviation carried as a ledger entry instead of a surprise.

The companion articles cover the HTTP surface, the MCP tool, and pipeline patterns; the
transform documentation indexes them all.
