From 240c0ad7dbad2b762b050cfc1aeee810fc6f1b32 Mon Sep 17 00:00:00 2001 From: callumalpass Date: Wed, 29 Jul 2026 14:51:49 +1000 Subject: [PATCH] Clarify semantic contracts and multi-contract types --- 00-overview.md | 8 +- 01-concepts.md | 6 +- 05-data-contracts.md | 35 ++++++ .../assets/suite/fixtures/catalog.json | 62 +++++++++- .../scenarios/core.shared-type-contracts.json | 87 ++++++++++++++ packages/testbed/test/testbed.test.mjs | 4 +- testbed/v0.1/fixtures/catalog.json | 62 +++++++++- .../scenarios/core.shared-type-contracts.json | 87 ++++++++++++++ tests/v0.3/data-contracts/data-contracts.yaml | 111 ++++++++++++++++++ 9 files changed, 453 insertions(+), 9 deletions(-) create mode 100644 packages/testbed/assets/suite/scenarios/core.shared-type-contracts.json create mode 100644 testbed/v0.1/scenarios/core.shared-type-contracts.json diff --git a/00-overview.md b/00-overview.md index b3b0eb7..ce9f003 100644 --- a/00-overview.md +++ b/00-overview.md @@ -175,10 +175,12 @@ read defaults, links, uniqueness, and path policy. Types can implement exact, versioned data contracts stored under `_contracts/`. A contract uses JSON Schema for its normalized record interface and optional binding. The type's `implements` entry maps contract fields to -record fields. +record fields. Record contracts normally describe compact application +semantics rather than storage layouts or external wire formats. -Several types can implement one contract, and several applications can consume -the same type. Discovery returns the complete implementation set; it never +Several types can implement one contract, one type can implement several +contracts, and several applications can consume the same contract view. +Discovery returns the complete implementation set; it never silently picks a provider. Data contracts are passive interoperability and do not grant access. diff --git a/01-concepts.md b/01-concepts.md index 760c56e..49b4cfc 100644 --- a/01-concepts.md +++ b/01-concepts.md @@ -35,12 +35,14 @@ providers and workflows supply executable behavior. A data contract is a versioned `mdbase.contract` control file that defines a portable record interface and optional implementation binding using JSON Schema -2020-12. +2020-12. A record contract normally names shared application semantics; event +and action contracts describe complete messages. A type implements a data contract through its top-level `implements` section. The contract does not replace the type, select a provider, assign ownership, or grant access. Several types can implement one contract, and several -applications can consume the same implementing type. +applications can consume the same implementing type. One type can contain +separate implementations of several contracts. ## Schema diff --git a/05-data-contracts.md b/05-data-contracts.md index ce65dbe..1f0ed0a 100644 --- a/05-data-contracts.md +++ b/05-data-contracts.md @@ -35,6 +35,35 @@ 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, @@ -156,6 +185,12 @@ 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 diff --git a/packages/testbed/assets/suite/fixtures/catalog.json b/packages/testbed/assets/suite/fixtures/catalog.json index 7d5507f..2b7747c 100644 --- a/packages/testbed/assets/suite/fixtures/catalog.json +++ b/packages/testbed/assets/suite/fixtures/catalog.json @@ -23,6 +23,28 @@ } } }, + "contract.example-summary": { + "kind": "contract", + "value": { + "kind": "mdbase.contract", + "contract_type": "record", + "id": "example.summary", + "version": "1.0.0", + "name": "Portable example summary", + "record_schema": { + "dialect": "json-schema-2020-12", + "value": { + "type": "object", + "required": ["label"], + "additionalProperties": false, + "properties": { + "label": { "type": "string", "minLength": 1 }, + "category": { "type": "string", "minLength": 1 } + } + } + } + } + }, "type.shared-note": { "kind": "type", "value": { @@ -51,11 +73,49 @@ ] } }, + "type.multi-contract-note": { + "kind": "type", + "value": { + "kind": "mdbase.type", + "name": "shared_note", + "version": 1, + "schema": { + "dialect": "json-schema-2020-12", + "value": { + "type": "object", + "required": ["type", "headline", "category"], + "properties": { + "type": { "const": "shared_note" }, + "headline": { "type": "string", "minLength": 1 }, + "category": { "type": "string", "minLength": 1 } + } + } + }, + "implements": [ + { + "contract": "example.note", + "version": "1.0.0", + "fields": { + "title": "headline" + } + }, + { + "contract": "example.summary", + "version": "1.0.0", + "fields": { + "label": "headline", + "category": "category" + } + } + ] + } + }, "record.shared-note": { "kind": "record", "value": { "type": "shared_note", - "headline": "One record, two consumers" + "headline": "One record, two consumers", + "category": "reference" } }, "contract.record-changed": { diff --git a/packages/testbed/assets/suite/scenarios/core.shared-type-contracts.json b/packages/testbed/assets/suite/scenarios/core.shared-type-contracts.json new file mode 100644 index 0000000..5a66017 --- /dev/null +++ b/packages/testbed/assets/suite/scenarios/core.shared-type-contracts.json @@ -0,0 +1,87 @@ +{ + "kind": "mdbase.testbed.scenario", + "protocol_version": "0.1", + "id": "core.shared-type-contracts", + "name": "One type exposes two independent contract views", + "description": "One local type implements two record contracts and each consumer observes only the normalized view it selected.", + "ring": "contract", + "profile": "core_read", + "roles": ["contract_store", "record_consumer"], + "operation": "contract.multiple-views", + "fixtures": [ + "contract.example-note", + "contract.example-summary", + "type.multi-contract-note", + "record.shared-note" + ], + "parameters": { + "consumers": ["note-consumer", "summary-consumer"] + }, + "covers": [ + "core_read.data_contract_discovery", + "core_read.data_contract_implementation_validation", + "core_read.data_contract_projection" + ], + "expect": { + "entries": [ + { + "sequence": 1, + "phase": "arrange", + "actor": "contract-store", + "operation": "contract.load", + "outcome": "succeeded", + "facts": { + "contracts": ["example.note", "example.summary"] + } + }, + { + "sequence": 2, + "phase": "arrange", + "actor": "contract-store", + "operation": "type.load", + "outcome": "succeeded", + "facts": { + "type": "shared_note", + "implements": ["example.note", "example.summary"] + } + }, + { + "sequence": 3, + "phase": "act", + "actor": "note-consumer", + "operation": "contract-view.read", + "outcome": "succeeded", + "facts": { + "contract": "example.note", + "view": { + "title": "One record, two consumers" + } + } + }, + { + "sequence": 4, + "phase": "act", + "actor": "summary-consumer", + "operation": "contract-view.read", + "outcome": "succeeded", + "facts": { + "contract": "example.summary", + "view": { + "label": "One record, two consumers" + } + } + }, + { + "sequence": 5, + "phase": "observe", + "actor": "testbed", + "operation": "contract-view.compare", + "outcome": "succeeded", + "facts": { + "same_record": true, + "views_are_distinct": true + } + } + ] + } +} diff --git a/packages/testbed/test/testbed.test.mjs b/packages/testbed/test/testbed.test.mjs index f7ce7b4..f9f3957 100644 --- a/packages/testbed/test/testbed.test.mjs +++ b/packages/testbed/test/testbed.test.mjs @@ -13,8 +13,8 @@ import { test("validates the complete neutral scenario inventory", () => { const result = validateTestbed(); assert.deepEqual(result, { - fixtures: 7, - scenarios: 15, + fixtures: 9, + scenarios: 16, profiles: [ "core_read", "event_action_interop/0.1", diff --git a/testbed/v0.1/fixtures/catalog.json b/testbed/v0.1/fixtures/catalog.json index 7d5507f..2b7747c 100644 --- a/testbed/v0.1/fixtures/catalog.json +++ b/testbed/v0.1/fixtures/catalog.json @@ -23,6 +23,28 @@ } } }, + "contract.example-summary": { + "kind": "contract", + "value": { + "kind": "mdbase.contract", + "contract_type": "record", + "id": "example.summary", + "version": "1.0.0", + "name": "Portable example summary", + "record_schema": { + "dialect": "json-schema-2020-12", + "value": { + "type": "object", + "required": ["label"], + "additionalProperties": false, + "properties": { + "label": { "type": "string", "minLength": 1 }, + "category": { "type": "string", "minLength": 1 } + } + } + } + } + }, "type.shared-note": { "kind": "type", "value": { @@ -51,11 +73,49 @@ ] } }, + "type.multi-contract-note": { + "kind": "type", + "value": { + "kind": "mdbase.type", + "name": "shared_note", + "version": 1, + "schema": { + "dialect": "json-schema-2020-12", + "value": { + "type": "object", + "required": ["type", "headline", "category"], + "properties": { + "type": { "const": "shared_note" }, + "headline": { "type": "string", "minLength": 1 }, + "category": { "type": "string", "minLength": 1 } + } + } + }, + "implements": [ + { + "contract": "example.note", + "version": "1.0.0", + "fields": { + "title": "headline" + } + }, + { + "contract": "example.summary", + "version": "1.0.0", + "fields": { + "label": "headline", + "category": "category" + } + } + ] + } + }, "record.shared-note": { "kind": "record", "value": { "type": "shared_note", - "headline": "One record, two consumers" + "headline": "One record, two consumers", + "category": "reference" } }, "contract.record-changed": { diff --git a/testbed/v0.1/scenarios/core.shared-type-contracts.json b/testbed/v0.1/scenarios/core.shared-type-contracts.json new file mode 100644 index 0000000..5a66017 --- /dev/null +++ b/testbed/v0.1/scenarios/core.shared-type-contracts.json @@ -0,0 +1,87 @@ +{ + "kind": "mdbase.testbed.scenario", + "protocol_version": "0.1", + "id": "core.shared-type-contracts", + "name": "One type exposes two independent contract views", + "description": "One local type implements two record contracts and each consumer observes only the normalized view it selected.", + "ring": "contract", + "profile": "core_read", + "roles": ["contract_store", "record_consumer"], + "operation": "contract.multiple-views", + "fixtures": [ + "contract.example-note", + "contract.example-summary", + "type.multi-contract-note", + "record.shared-note" + ], + "parameters": { + "consumers": ["note-consumer", "summary-consumer"] + }, + "covers": [ + "core_read.data_contract_discovery", + "core_read.data_contract_implementation_validation", + "core_read.data_contract_projection" + ], + "expect": { + "entries": [ + { + "sequence": 1, + "phase": "arrange", + "actor": "contract-store", + "operation": "contract.load", + "outcome": "succeeded", + "facts": { + "contracts": ["example.note", "example.summary"] + } + }, + { + "sequence": 2, + "phase": "arrange", + "actor": "contract-store", + "operation": "type.load", + "outcome": "succeeded", + "facts": { + "type": "shared_note", + "implements": ["example.note", "example.summary"] + } + }, + { + "sequence": 3, + "phase": "act", + "actor": "note-consumer", + "operation": "contract-view.read", + "outcome": "succeeded", + "facts": { + "contract": "example.note", + "view": { + "title": "One record, two consumers" + } + } + }, + { + "sequence": 4, + "phase": "act", + "actor": "summary-consumer", + "operation": "contract-view.read", + "outcome": "succeeded", + "facts": { + "contract": "example.summary", + "view": { + "label": "One record, two consumers" + } + } + }, + { + "sequence": 5, + "phase": "observe", + "actor": "testbed", + "operation": "contract-view.compare", + "outcome": "succeeded", + "facts": { + "same_record": true, + "views_are_distinct": true + } + } + ] + } +} diff --git a/tests/v0.3/data-contracts/data-contracts.yaml b/tests/v0.3/data-contracts/data-contracts.yaml index 889de7c..48996ee 100644 --- a/tests/v0.3/data-contracts/data-contracts.yaml +++ b/tests/v0.3/data-contracts/data-contracts.yaml @@ -214,3 +214,114 @@ groups: expect: view: title: Work note + + - name: "one type implementing several contracts" + setup: + config: | + spec_version: "0.3.0" + settings: + types_folder: _types + contracts_folder: _contracts + explicit_type_keys: [type] + contracts: + example.note.md: | + --- + kind: mdbase.contract + contract_type: record + id: example.note + version: 1.0.0 + record_schema: + dialect: json-schema-2020-12 + value: + type: object + required: [title] + properties: + title: { type: string } + --- + example.summary.md: | + --- + kind: mdbase.contract + contract_type: record + id: example.summary + version: 1.0.0 + record_schema: + dialect: json-schema-2020-12 + value: + type: object + required: [label] + properties: + label: { type: string } + category: { type: string } + --- + types: + shared-note.md: | + --- + kind: mdbase.type + name: shared_note + version: 1 + schema: + dialect: json-schema-2020-12 + value: + type: object + required: [type, headline] + properties: + type: { const: shared_note } + headline: { type: string } + category: { type: string } + implements: + - contract: example.note + version: 1.0.0 + fields: + title: headline + - contract: example.summary + version: 1.0.0 + fields: + label: headline + category: category + --- + files: + shared.md: | + --- + type: shared_note + headline: One record, two interfaces + category: reference + --- + tests: + - name: "the type is discoverable through its first contract" + operation: get_data_contracts + input: + contract: example.note + version: 1.0.0 + expect: + implementations: + - type: shared_note + + - name: "the same type is discoverable through its second contract" + operation: get_data_contracts + input: + contract: example.summary + version: 1.0.0 + expect: + implementations: + - type: shared_note + + - name: "the first implementation exposes its own normalized view" + operation: get_contract_view + input: + path: shared.md + contract: example.note + version: 1.0.0 + expect: + view: + title: One record, two interfaces + + - name: "the second implementation exposes a different normalized view" + operation: get_contract_view + input: + path: shared.md + contract: example.summary + version: 1.0.0 + expect: + view: + label: One record, two interfaces + category: reference