Write your first ODCS data contract
A data contract in Kirimana is the complete, single-document source of truth for a dataset. One contract, read once, answers every operational, governance, and AI-consumption question about its data: where it comes from, what shape it has, who owns it, how sensitive it is, how fresh it must be, and what an AI assistant is allowed to do with it.
The wire format is ODCS v3 YAML — adopted straight,
no forks, no parallel fields. Anything ODCS doesn’t cover is expressed
through customProperties under the documented kiri.* namespace.
Every kiri.* namespace is a candidate for upstreaming to the ODCS
spec; the namespace is a safety valve, not a long-term home.
Anatomy of a contract
The ODCS-native fields you’ll touch in every contract:
| Field | Purpose |
|---|---|
apiVersion | ODCS version declaration, pinned per Kirimana release |
kind | Always DataContract |
id | Stable contract ID — UUID or reverse-DNS (com.acme.crm.customer) |
name | Human-readable name; must match the filename stem |
version | Semver — MAJOR.MINOR.PATCH |
status | draft / active / deprecated / retired |
domain | Business domain (crm, finance, sales) |
description | Purpose and limitations — write what this dataset is not for |
owner | Accountable person or team; email required |
schema[] | Tables and their properties[] (columns) |
quality[] | Contract-level data-quality rules (SQL / library / custom) |
sla[] | Freshness, availability, retention |
servers[] | Where the data physically lives |
On top of that, kiri.* customProperties carry the governance and
runtime semantics ODCS doesn’t encode. The ones you need first:
kiri.classification— one ofpublic·internal·confidential·restricted. Mandatory on every contract, and on any column whose sensitivity differs from the contract level.kiri.pii.categories+kiri.pii.direct_identifier— per-column PII taxonomy (name,email,phone,national_id,health,location, …).kiri.lifecycle.state— the medallion state (draft/bronze/silver/gold/deprecated); advanced withkiri contract promote.kiri.sla.*— freshness (4h,1d,15m), a quality-threshold pass rate, and breach routing (severity + a route likeslack:#data-platform-alerts).kiri.ai_policy.*— whether the data may be used for AI training or inference, and where it may egress. Egress runs strictest first:internal_only(local models only),in_platform(also the data platform this contract’s data already lives on, never a third-party API),approved_providers(a named list, which may include a vendor),unrestricted. When absent, the default is the most restrictive:forbidden,internal_only. Enterprise-safe by default.kiri.semantic.*— technique-neutral business meaning:entity,business_keys,grain. The same declaration projects into a Data Vault hub key, a Kimball natural key, or a flat unique constraint.
A missing namespace is a legal default, not an error. Unlisted custom properties round-trip untouched but are never relied on by the core.
A complete bronze contract
Bronze is the landing layer: 1:1 with the source, same columns, no
business logic, insert-only with provenance stamps. Scaffold one with
kiri contract new (interactive) or kiri contract scaffold
(flag-driven), or write it by hand:
apiVersion: 3.0.0
kind: DataContract
id: io.acme.sales.bronze.customer_raw
name: customer_raw
version: 0.1.0
status: draft
domain: sales
description: >
Bronze landing for CRM accounts. 1:1 with the source — same columns,
same types, no business logic. Insert-only with a load_date +
record_source stamp so we always know where each row came from.
NOT FOR: marketing segmentation or finance reconciliation.
owner: data-platform@acme.example
schema:
- name: customer_raw
logicalType: table
properties:
- name: accountid
logicalType: string
primaryKey: true
required: true
description: Source business key from the CRM
- name: name
logicalType: string
required: true
- name: primary_contact_email
logicalType: string
description: PII — handled per pii + ai_policy below
quality:
- type: library
rule: regex
parameters: {pattern: "^[^@]+@[^@]+\\.[^@]+$"}
customProperties:
- property: kiri.pii.categories
value: [email]
- property: kiri.pii.direct_identifier
value: true
- name: load_date
logicalType: timestamp
required: true
description: Wall-clock time the row landed in bronze
- name: record_source
logicalType: string
required: true
description: Provenance tag (e.g. "crm.accounts")
quality:
- type: sql
description: accountid is unique
query: "SELECT count(*) - count(DISTINCT accountid) FROM {{ table }}"
mustBe: 0
customProperties:
- property: kiri.classification
value: confidential
- property: kiri.source_urn
value: urn:kirimana:source:crm:accounts
- property: kiri.lifecycle.state
value: bronze
- property: kiri.medallion.active
value: true
- property: kiri.sla.freshness
value: 1d
- property: kiri.sla.on_breach.severity
value: high
- property: kiri.sla.on_breach.route
value: slack:#data-platform-alerts
- property: kiri.ai_policy.training_data
value: forbidden
- property: kiri.ai_policy.inference_allowed
value: true
- property: kiri.ai_policy.egress
value: approved_providers
- property: kiri.ai_policy.approved_providers
value: [local, anthropic]
Ownership and classification are not optional
Two governance rules bite early, on purpose:
Ownership must be real. kiri contract lint refuses placeholder
owners — TODO, FIXME, an empty string, or any example.com
address raise an error-severity owner-placeholder finding. Team
handles (analytics-team) and real emails pass; a malformed email
does not. There is no way to merge a contract nobody answers for.
Sensitive data cannot be quietly authored as harmless. Columns
whose names look like PII (email, phone, ssn, birth_date, …)
without an explicit kiri.pii.* annotation raise a pii-hint
finding, backed by fail-closed authoring guardrails that never
silently stamp a default verdict. Classification proposals from
kiri inventory pre-classify merge by a strict precedence ladder in
which a human operator override wins every contest — and every
stamped decision writes one structured audit row. Downgrading an
inferred-PII column to public therefore only happens as an explicit,
audited operator decision, never as a default.
Semantic validation adds more cross-field rules: every column carrying
kiri.pii.categories requires contract-level kiri.gdpr.* metadata,
egress: approved_providers requires a non-empty provider list, and
lifecycle transitions follow a state machine.
Lint, validate, diff — at PR time
Contracts live in git, so governance runs where the change happens:
# Structural (ODCS JSON Schema) + semantic validation of one contract
kiri contract validate contracts/customer_raw.yml --strict
# Governance lint over the whole project; markdown for a PR comment
kiri contract lint --format pr
# Semantic-aware diff of two contract versions
kiri diff contracts/customer_raw.yml /tmp/customer_raw.proposed.yml
lint exits 0 on warnings only, 1 on any error-severity finding —
wire it into CI so an unowned or unclassified contract can’t reach
main. kiri contract approval-count computes how many approvers a
change needs (a diff that raises anything to restricted needs
more), and kiri contract codeowners derives a CODEOWNERS file from
the contract ownership so review routing follows the metadata.
From contract to Unity Catalog
The contract is the source of truth; the platform catalog is a materialisation of it. On Databricks:
kiri applycreates the physical tables and stamps the governance metadata declared in the contract.kiri catalog push --target unitywrites classifications, tags, and lineage annotations to Unity Catalog (dry-run by default — inspect the NDJSON, then--no-dry-run).kiri contract verify-live --catalog <uc-catalog> --schema <schema>diffs the live table’s schema against the contract and fails CI on drift;kiri contract sync-from-live --proposeemits a reviewable patch when the live table is the truth you want to adopt.
Edits flow one way: change the YAML, open a PR, apply. A classification edited directly in the catalog is drift, and Kirimana surfaces it rather than pulling it back.
Where next
Promote the contract along the medallion path with
kiri contract promote <name> --to silver, declare business keys via
kiri.semantic.*, and let kiri contract emit generate the framework
artifacts that build it.