Skip to content

Latest commit

 

History

History
497 lines (404 loc) · 21.9 KB

File metadata and controls

497 lines (404 loc) · 21.9 KB

05A. First-Class Contracts

Why Data Contracts Exist

A contract describes one portable interface independently from the local type or application that implements it.

For example, personal_task, work_task, and task can all implement tasknotes.task. Their filenames, additional fields, matching rules, and local presentation can differ. An application can discover the shared contract, understand each type's field mapping, and operate without requiring every collection to use one canonical type name.

mdbase.contract is the shared identity and JSON Schema substrate for passive record views, events, and actions. contract_type discriminates their subject-specific schema fields. This chapter defines the shared artifact and record implementation rules. The optional event/action interoperability profile defines executable source/provider declarations and message exchange. Contracts never grant permission.

Three Portable Artifacts

The complete collection contract model has three intentionally small parts:

  1. An mdbase.contract artifact defines a versioned subject-specific interface using JSON Schema 2020-12.
  2. For a record contract, a type's implements entry maps that interface to the type and supplies contract-specific binding data.
  3. An optional managed mdbase.type-pack groups contracts, types, and their referenced schemas for transactional installation and evolution.

Event sources and action providers make runtime declarations because they are executable, instance-specific implementations rather than record types. Requirements, authorization grants, and transports are not collection contract artifacts.

Designing Record Contracts

A record contract SHOULD describe a compact, application-facing semantic interface. Its properties name values that independent consumers can rely on, while an implementing type remains free to choose local field names, matching rules, additional fields, and presentation.

Record contracts SHOULD:

  • keep the unconditionally required surface as small as the shared behavior permits
  • use optional properties for semantics that not every implementing type can provide
  • use binding_schema for implementation-specific vocabularies and behavior, such as which local task statuses count as completed
  • be shared by applications that need the same semantics rather than duplicated under application-specific IDs

A record contract SHOULD NOT reproduce an external interchange or storage format merely so applications can request that serialization. An importer, exporter, or application adapter can translate a semantic record view to JSContact, vCard, or another wire format. A wire-format-shaped record contract remains valid when the implementing type intentionally stores and exposes that exact shape.

Event and action contracts are different: their schemas describe complete messages at an interoperability boundary, so a transport-shaped schema is usually appropriate.

Contract Files

Contract files are Markdown files under the configured contracts folder, default _contracts/. Their frontmatter has kind: mdbase.contract and validates against schemas/v0.3/data-contract.schema.json.

---
kind: mdbase.contract
contract_type: record
id: example.task
version: 1.0.0
name: Example task
description: A small portable task interface.

record_schema:
  dialect: json-schema-2020-12
  value:
    $schema: "https://json-schema.org/draft/2020-12/schema"
    type: object
    required: [title, status]
    additionalProperties: false
    properties:
      title: { type: string, minLength: 1 }
      status: { type: string, minLength: 1 }
      due: { type: string, format: date }

binding_schema:
  dialect: json-schema-2020-12
  value:
    $schema: "https://json-schema.org/draft/2020-12/schema"
    type: object
    required: [completed_values]
    additionalProperties: false
    properties:
      completed_values:
        type: array
        minItems: 1
        uniqueItems: true
        items: { type: string }
---

# Example task

The body explains the interface to people. Portable behavior is defined by the
frontmatter schemas.

id is a lower-case namespaced identifier. version is an exact semantic version. A type implementation never names a version range.

record_schema validates the normalized contract view produced from a record. binding_schema, when present, validates implementation-specific semantic configuration. Both use the same JSON Schema profile and reference rules as type schemas.

An event contract instead requires data_schema and may declare source_schema. An action contract requires input_schema and may declare output_schema, error_schema, provider_schema, and behavior. Subject fields belonging to another contract_type are invalid.

Contract files are control files, not records. They do not participate in ordinary record scans, queries, links, or runtime workflow discovery.

Contract Registry

During collection load, a data-contract-aware implementation:

  1. scans the configured contracts folder recursively
  2. validates every candidate against the built-in data-contract schema
  3. resolves and compiles the schemas selected by contract_type
  4. registers each contract by the exact pair (id, version)
  5. computes its contract digest
  6. validates every type implements entry against the resulting registry

Several versions of one contract ID may coexist. Two artifacts with the same ID and version are valid only when their contract digests are identical. Different content for the same ID and version is data_contract_conflict.

Resolution is collection-local and offline. Core implementations MUST NOT fetch a missing contract from the network. Applications and installers carry the contract files they require, usually in a type pack.

Type Implementations

implements belongs in the type file because a record implementation is a claim about each record that matches that one type. A type can implement only a record contract. Event sources and action providers declare implementations through the interoperability profile instead.

implements:
  - contract: example.task
    version: 1.0.0
    fields:
      title: title
      status: workflow_state
      due: due_date
      "/@type": "/card/@type"
    binding:
      completed_values: [done, cancelled]

Each implementation contains:

Key Meaning
contract exact contract ID
version exact contract semantic version
fields contract field reference to record field reference mapping
binding optional configuration validated by the contract's binding_schema

Both sides of fields use the field-reference syntax from Chapter 07. Existing field paths remain valid. RFC 6901 JSON Pointer is the exact form for keys that field paths cannot represent, so /@type addresses an @type property and /a~1b addresses an a/b property. The left side addresses the normalized contract view. The right side addresses effective record frontmatter. Mapping is direct: core does not rename values, coerce values, run expressions, or apply hidden transforms.

The binding object does not transform projected record values. It supplies validated semantic policy that a contract-aware application can interpret. For example, a task implementation can expose the local status value unchanged while declaring several values in binding.completed_values. This preserves the user's vocabulary and avoids inventing an ambiguous reverse mapping.

A type MUST NOT contain two implementations of the same contract ID and version. Field mappings MUST address fields declared by the resolved record_schema and the resolved type schema. Every unconditional top-level field named by the contract's record_schema.required array MUST be mapped, either by the matching one-segment field path or by the matching one-token JSON Pointer.

When a contract has a binding_schema, the implementation's binding value, or an empty object when omitted, MUST validate against it. When a contract has no binding_schema, binding MUST be absent or empty.

Private application metadata may remain under x-*, but an x-* object has no contract-discovery, conformance, or authorization meaning.

Contract Views And Record Validation

To construct a contract view, a tool starts with a record's effective frontmatter and copies every mapped value to its contract field reference. Missing optional values remain missing. The resulting object is validated against the contract's record_schema.

Contract validation complements rather than replaces type validation:

  • the type schema validates raw persisted frontmatter
  • collection semantics construct the effective record
  • the field map constructs a normalized contract view
  • the contract's record_schema validates that view

A record can therefore satisfy its type schema and still produce data_contract_record_invalid for one declared implementation. Implementations MUST surface that diagnostic whenever they expose the record through that contract.

Static checks at collection load SHOULD diagnose obviously incompatible mapped schema types early. Runtime contract-view validation remains authoritative when JSON Schema composition makes static implication impractical.

Stable Digests

Digests let a consumer distinguish an approved implementation from a later change that happens to retain the same name.

The contract digest is SHA-256 over RFC 8785 JSON Canonicalization Scheme bytes for this object, using the fully resolved JSON Schema values rather than their storage wrappers or reference paths:

{
  "kind": "mdbase.contract",
  "contract_type": "record",
  "id": "...",
  "version": "...",
  "record_schema": {},
  "binding_schema": {}
}

The digest object contains the subject-specific schema keys selected by contract_type: record_schema and binding_schema; data_schema and source_schema; or input_schema, output_schema, error_schema, provider_schema, and behavior. Absent optional members are omitted. Human-facing name, description, Markdown body, x-* metadata, schema wrapper dialects, and local ref paths do not affect portable identity. Consequently, an inline schema and a local referenced schema with identical resolved JSON values have the same contract digest, while changing the bytes at a stable reference path changes the digest.

The implementation digest is SHA-256 over RFC 8785 bytes for:

{
  "contract_digest": "sha256:...",
  "type": {
    "name": "...",
    "version": 1,
    "match": {},
    "schema": {},
    "collection": {},
    "lifecycle": {}
  },
  "implementation": {}
}

Absent optional members are omitted. This deliberately includes membership, shape, defaults, links, paths, and lifecycle behavior. A consumer that pinned an implementation can detect any portable change that may alter the records or values it observes.

Digest strings use sha256: followed by 64 lower-case hexadecimal characters.

Multiple Implementations

A contract lookup returns a set of conforming type implementations, never an arbitrarily selected provider.

Multiple applications may consume the same type implementation. Implementing a contract does not create an owner, lease, or exclusive provider relationship.

When several types implement one compatible contract requirement:

  • read and list experiences SHOULD initially offer their explicit union
  • user-facing approval MUST show every included type
  • an existing approval MUST pin the exact type names, contract digest, and implementation digests
  • a later implementation MUST NOT silently join that approval
  • creation MUST use one explicitly selected implementing type

A product may let a user select a subset instead of the union. It MUST NOT hide the selection or silently choose the first filesystem entry.

These rules separate interoperability from authorization. A type's implements claim says what it can mean; it does not say which application may read or mutate it.

Contract Access And Whole Records

The portable contract view contains only mapped contract fields. A gateway that grants access "through a contract" MUST expose only that view plus the minimum record identity needed by its protocol. If a record has several approved views and the operation does not identify one unambiguously, the gateway MUST require an explicit contract ID, exact version, and implementing type rather than merge or guess.

Access to unmapped frontmatter, the Markdown body, or arbitrary records is whole-record or whole-collection access and MUST be requested and presented explicitly. Merely implementing a contract never grants either form of access.

Core collection APIs remain authorization-neutral. This distinction is normative for gateways and application protocols that use data contracts as an authorization boundary.

Type Packs

A type pack is a directory or archive with an mdbase-pack.yaml manifest that validates against schemas/v0.3/type-pack.schema.json.

kind: mdbase.type-pack
id: example.tasks
version: 1.0.0
name: Example task types
resources:
  - kind: contract
    mode: managed
    source: contracts/example.task.md
    target: _contracts/example.task.md
    digest: sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
  - kind: type
    mode: managed
    source: types/task.md
    target: _types/task.md
    digest: sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789

mode is required. A managed resource remains owned by the pack and may be updated or retired by a later pack version only while its live bytes still match the installed digest. A seed resource is created only when its target is absent and becomes user-owned immediately; later pack versions neither replace nor delete it.

Resource digests are SHA-256 over the exact resource bytes. Source and target paths are relative, forward-slash paths without traversal. A directory, archive, repository, or package that distributes a pack MUST preserve those bytes exactly, including line endings. Rewriting a text resource from LF to CRLF therefore creates a different resource and MUST fail digest verification. This repository fixes text checkouts to LF so its example pack has identical bytes on every supported platform.

Type packs are installation units, not record types and not permission grants. A pack may include several contracts, several implementing or auxiliary types, and local JSON Schemas referenced by those artifacts.

Pack Identity And Portable Provenance

The pack digest is SHA-256 over RFC 8785 JSON Canonicalization Scheme bytes for the complete validated manifest, prefixed with sha256:. Because every resource digest is part of the manifest, the pack digest identifies the exact manifest and exact resource bytes without incorporating their distribution location.

Managed pack state is stored in mdbase.lock.yaml at the collection root and validates against schemas/v0.3/type-pack-lock.schema.json. The lock is portable collection source, not derived cache state. It records each installed pack's exact ID, version, pack digest, stable installer identity, and the kind, mode, canonical source, resolved target, and installed digest of every resource. Tools write it deterministically and users MAY inspect or version it. Tools MUST NOT infer ownership from filenames, application names, or x-* metadata.

Full collection snapshots, authority transfers, and unscoped synchronization MUST carry mdbase.lock.yaml as a lock resource when it exists. A scoped application projection MAY omit it to avoid disclosing unrelated pack metadata. The lock is connector-generated and protocol-bounded; a hosted provider MAY count its bytes toward aggregate storage, but MUST NOT reject it against a user-authored per-document size quota that the collection owner cannot remediate.

An absent lock means no resource is pack-managed, including resources created by older one-shot installers. Adopting existing files into managed ownership is a separate explicit operation and MUST verify their exact digests.

Assessment And Transactional Apply

Pack evolution has two public phases: assess_type_pack is read-only and apply_type_pack consumes a reviewed assessment. A caller supplies the desired manifest, exact resources, and a stable reverse-domain installed_by application or tool identity. Optional target_overrides map canonical manifest targets to collection-specific paths without changing the publisher-owned pack identity. Overrides are validated as safe collection paths, are included in the assessment digest, and the resolved targets are written to the receipt.

Assessment reports one of current, install, upgrade, downgrade, reconfigure, or conflict, the current and desired pack identities, and every resource in a stable exact diff. Resource actions are create, update, delete, adopt, unchanged, preserve, or conflict. adopt records an existing byte-identical managed resource without rewriting it. preserve is used for seed resources and for seed resources retired from a newer pack. A conflicting assessment is not applicable. The assessment also reports the planned mdbase.lock.yaml action (create, update, or unchanged) and its resulting digest, so storage adapters can persist the complete authority transaction without inferring hidden engine writes.

reconfigure means the exact same immutable pack is being resolved to different collection targets. Managed resources are relocated only when their installed bytes are unchanged; modified resources conflict. Seed resources are never moved or deleted automatically. A caller may name seed targets in preserve_seed_targets to record an intentional omission, such as when a user maps the contract to an existing type instead of accepting the starter. Unknown or non-seed preservation targets MUST be rejected.

When guided setup maps a provided contract to an existing user-owned type, the reviewed contract_setups choices are part of the same assessment and apply. The assessment reports their exact type-resource diff and binds the selected type revisions, field mappings, and bindings into assessment_digest. Apply MUST stage those edits together with the managed resources and lock, validate the combined collection, and commit them in the same transaction. A stale type revision therefore leaves both the user-owned type and every pack resource unchanged. A connector MUST NOT install the pack first and apply the reviewed mapping as a later independent mutation.

The assessment includes a deterministic assessment_digest over the desired pack identity, current lock entry, and every relevant live target digest. Apply MUST recompute it inside the collection mutation boundary and fail with concurrent_modification before writing when it differs. Downgrades require an explicit allow_downgrade decision.

An unmanaged target with different bytes is a conflict unless the assessment request contains an explicit adoption decision naming that target and its exact current digest. An accepted adoption may plan update, records the adopted digest in the diff, and is covered by assessment_digest. This is the safe upgrade bridge for collections created before pack receipts existed: tools can show the files that will become package-managed, require informed approval, and remain race-safe. Unknown targets, seed targets, and stale adoption digests MUST be rejected. Adoption is never inferred from application access or install intent.

A pack-aware apply implementation MUST:

  1. validate the manifest, safe paths, source bytes, and resource digests
  2. stage every resource and reviewed existing-type setup without changing the live collection
  3. resolve the complete staged contract and type registries
  4. validate every contract, implementation, type, reference, and affected existing record
  5. compute and present the exact resource diff and assessment digest
  6. acquire its collection mutation boundary and recheck the assessment
  7. commit all resource changes and the lock as one recoverable transaction
  8. reopen the collection and verify the committed registry and lock

An invalid resource aborts before any live write. A conflict or concurrent change aborts with the live collection unchanged. Implementations may use an atomic directory exchange or a durable backup journal with rollback. After a crash, recovery MUST restore either the complete pre-install state or the complete committed state before normal collection operations resume.

Revoking an application's access does not uninstall its type pack. Uninstall is a separate, explicitly requested operation because records may still depend on the installed types.

Applying a current pack is valid and reports every managed resource as unchanged; it MUST NOT create a new logical collection revision. Applying an upgrade deletes a retired managed target only when its live digest still matches the lock. User-modified managed resources produce conflict and no live write. Successful apply reports the committed receipt, exact resource diff, and cleanup_deferred when transaction-journal cleanup must be retried.

Diagnostics

Data-contract-aware tools use these codes:

Code Meaning
invalid_data_contract contract frontmatter or schema is invalid
data_contract_not_found an implementation references no local exact contract
data_contract_conflict one ID and version resolve to different contract digests
data_contract_version_mismatch a consumer requirement has no compatible exact version
data_contract_binding_invalid implementation binding fails its binding schema
data_contract_field_invalid a mapped contract or record field is missing or incompatible
data_contract_record_invalid a projected contract view fails the contract schema
invalid_type_pack a pack manifest, lock, resource, path, or digest is invalid
type_pack_conflict ownership, live bytes, or an untracked target prevents a safe apply
type_pack_apply_failed transactional commit or recovery did not complete normally

Diagnostics use the canonical shape from Chapter 16 and identify the contract ID, exact version, type name, and relevant field mapping in details.