diff --git a/README.md b/README.md index 0c66dd9..b65edb4 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,12 @@ First-party, versioned data contracts and transactional type packs for mdbase collections. +Published contract schemas are immutable interoperability boundaries. A +catalog-listed starter type contains an inline snapshot of its starting schema +instead of inheriting that boundary. Once installed, the type belongs to the +collection: users can edit its fields and update the explicit contract mapping +without changing the published contract. + This repository is the canonical source. Its deterministic `dist/` output is published at `https://mdbase.dev/contracts/`. A catalog entry is only a discovery aid: every installable pack contains an exact manifest, embedded @@ -48,4 +54,3 @@ A standards-oriented contract must identify its normative references and state its profile scope. An mdbase contract is not presented as an official schema from the referenced standards body unless that body actually publishes it as such. - diff --git a/dist/artifacts/types/jscontact-card/2.md b/dist/artifacts/types/jscontact-card/2.md new file mode 100644 index 0000000..4af1f65 --- /dev/null +++ b/dist/artifacts/types/jscontact-card/2.md @@ -0,0 +1,233 @@ +--- +kind: mdbase.type +name: jscontact-card +version: 2 +description: An editable Markdown-backed JSContact 2.0 core Card +match: + where: + /@type: Card + version: "2.0" +schema: + dialect: json-schema-2020-12 + value: + $schema: https://json-schema.org/draft/2020-12/schema + title: JSContact Card 2.0 core profile + description: A local starting shape for an IETF JSContact 2.0 Card. + type: object + required: + - "@type" + - version + properties: + "@type": + const: Card + version: + const: "2.0" + uid: + type: string + minLength: 1 + kind: + enum: + - individual + - group + - org + - location + - device + - application + name: + $ref: "#/$defs/name" + emails: + type: object + propertyNames: + $ref: "#/$defs/id" + additionalProperties: + $ref: "#/$defs/emailAddress" + phones: + type: object + propertyNames: + $ref: "#/$defs/id" + additionalProperties: + $ref: "#/$defs/phone" + organizations: + type: object + propertyNames: + $ref: "#/$defs/id" + additionalProperties: + $ref: "#/$defs/organization" + notes: + type: object + propertyNames: + $ref: "#/$defs/id" + additionalProperties: + $ref: "#/$defs/note" + additionalProperties: false + $defs: + id: + type: string + minLength: 1 + trueSet: + type: object + additionalProperties: + const: true + name: + type: object + properties: + "@type": + const: Name + components: + type: array + minItems: 1 + contains: + type: object + required: + - kind + properties: + kind: + not: + const: separator + items: + $ref: "#/$defs/nameComponent" + full: + type: string + minLength: 1 + isOrdered: + type: boolean + defaultSeparator: + type: string + anyOf: + - required: + - components + - required: + - full + additionalProperties: false + nameComponent: + type: object + required: + - kind + - value + properties: + "@type": + const: NameComponent + kind: + enum: + - credential + - generation + - given + - given2 + - separator + - surname + - surname2 + - title + value: + type: string + additionalProperties: false + emailAddress: + type: object + required: + - address + properties: + "@type": + const: EmailAddress + address: + type: string + format: email + contexts: + $ref: "#/$defs/trueSet" + pref: + type: integer + minimum: 1 + label: + type: string + additionalProperties: false + phone: + type: object + required: + - number + properties: + "@type": + const: Phone + number: + type: string + minLength: 1 + features: + $ref: "#/$defs/trueSet" + contexts: + $ref: "#/$defs/trueSet" + pref: + type: integer + minimum: 1 + label: + type: string + additionalProperties: false + organization: + type: object + properties: + "@type": + const: Organization + name: + type: string + minLength: 1 + units: + type: array + minItems: 1 + items: + $ref: "#/$defs/orgUnit" + sortAs: + type: string + contexts: + $ref: "#/$defs/trueSet" + anyOf: + - required: + - name + - required: + - units + additionalProperties: false + orgUnit: + type: object + required: + - name + properties: + "@type": + const: OrgUnit + name: + type: string + minLength: 1 + additionalProperties: false + note: + type: object + required: + - note + properties: + "@type": + const: Note + note: + type: string + created: + type: string + format: date-time + additionalProperties: false +collection: + display: + name_field: /name/full +implements: + - contract: mdbase.jscontact.card + version: 2.0.0 + fields: + /@type: /@type + version: version + uid: uid + kind: kind + name: name + emails: emails + phones: phones + organizations: organizations + notes: notes +--- + +# JSContact Card + +This starter type begins as a local copy of the JSContact core profile. Its +fields belong to the collection and can be added, renamed, or reshaped. Update +the explicit contract mapping whenever a compatible field moves. + +The separately installed contract schema remains the portable boundary. Local +fields that are not mapped into that boundary remain collection-specific. diff --git a/dist/catalog.json b/dist/catalog.json index 6289a1c..f1c0f02 100644 --- a/dist/catalog.json +++ b/dist/catalog.json @@ -33,11 +33,11 @@ "packs": [ { "id": "mdbase.jscontact", - "version": "2.0.0", + "version": "2.0.1", "name": "JSContact 2.0 starter", - "description": "The mdbase JSContact Card core contract and a direct Markdown implementation.", - "digest": "sha256:a037e46891244018b0cb6c8269bbba56e800d272f0d7591cf525f3661db877b3", - "provision": "./packs/mdbase.jscontact/2.0.0.json", + "description": "The mdbase JSContact Card core contract and an editable Markdown implementation.", + "digest": "sha256:c277f278b20acbd0b182d88e2c3c018220c2808aa57f73fa2da1b413383ed21c", + "provision": "./packs/mdbase.jscontact/2.0.1.json", "provides": [ { "id": "mdbase.jscontact.card", diff --git a/dist/packs/mdbase.jscontact/2.0.1.json b/dist/packs/mdbase.jscontact/2.0.1.json new file mode 100644 index 0000000..39a389a --- /dev/null +++ b/dist/packs/mdbase.jscontact/2.0.1.json @@ -0,0 +1,49 @@ +{ + "manifest": { + "kind": "mdbase.type-pack", + "id": "mdbase.jscontact", + "version": "2.0.1", + "name": "JSContact 2.0 starter", + "description": "The mdbase JSContact Card core contract and an editable Markdown implementation.", + "resources": [ + { + "kind": "schema", + "source": "schemas/mdbase.jscontact.card/2.0.0.schema.json", + "target": "schemas/mdbase.jscontact.card/2.0.0.schema.json", + "digest": "sha256:e2831b1203303075767aefd5e4892c81793d4b85c45db706ef189aa6124f8e26" + }, + { + "kind": "contract", + "source": "contracts/mdbase.jscontact.card/2.0.0.md", + "target": "_contracts/mdbase.jscontact.card/2.0.0.md", + "digest": "sha256:c56c5acbf7992155d6890294842a261fe80a933ec75a226ea875b97ba57dcaa2" + }, + { + "kind": "type", + "source": "types/jscontact-card/2.md", + "target": "_types/jscontact-card.md", + "digest": "sha256:daa85fda0de4b95e51b01ca6de7fef17041fadb416b6fc540dd35d7da2627d6e" + } + ] + }, + "resources": [ + { + "source": "schemas/mdbase.jscontact.card/2.0.0.schema.json", + "document": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://mdbase.dev/contracts/schemas/mdbase.jscontact.card/2.0.0.schema.json\",\n \"title\": \"JSContact Card 2.0 core profile\",\n \"description\": \"A strict mdbase core profile of an IETF JSContact 2.0 Card.\",\n \"type\": \"object\",\n \"required\": [\"@type\", \"version\"],\n \"properties\": {\n \"@type\": { \"const\": \"Card\" },\n \"version\": { \"const\": \"2.0\" },\n \"uid\": { \"type\": \"string\", \"minLength\": 1 },\n \"kind\": {\n \"enum\": [\"individual\", \"group\", \"org\", \"location\", \"device\", \"application\"]\n },\n \"name\": { \"$ref\": \"#/$defs/name\" },\n \"emails\": {\n \"type\": \"object\",\n \"propertyNames\": { \"$ref\": \"#/$defs/id\" },\n \"additionalProperties\": { \"$ref\": \"#/$defs/emailAddress\" }\n },\n \"phones\": {\n \"type\": \"object\",\n \"propertyNames\": { \"$ref\": \"#/$defs/id\" },\n \"additionalProperties\": { \"$ref\": \"#/$defs/phone\" }\n },\n \"organizations\": {\n \"type\": \"object\",\n \"propertyNames\": { \"$ref\": \"#/$defs/id\" },\n \"additionalProperties\": { \"$ref\": \"#/$defs/organization\" }\n },\n \"notes\": {\n \"type\": \"object\",\n \"propertyNames\": { \"$ref\": \"#/$defs/id\" },\n \"additionalProperties\": { \"$ref\": \"#/$defs/note\" }\n }\n },\n \"additionalProperties\": false,\n \"$defs\": {\n \"id\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"trueSet\": {\n \"type\": \"object\",\n \"additionalProperties\": { \"const\": true }\n },\n \"name\": {\n \"type\": \"object\",\n \"properties\": {\n \"@type\": { \"const\": \"Name\" },\n \"components\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"contains\": {\n \"type\": \"object\",\n \"required\": [\"kind\"],\n \"properties\": {\n \"kind\": { \"not\": { \"const\": \"separator\" } }\n }\n },\n \"items\": { \"$ref\": \"#/$defs/nameComponent\" }\n },\n \"full\": { \"type\": \"string\", \"minLength\": 1 },\n \"isOrdered\": { \"type\": \"boolean\" },\n \"defaultSeparator\": { \"type\": \"string\" }\n },\n \"anyOf\": [\n { \"required\": [\"components\"] },\n { \"required\": [\"full\"] }\n ],\n \"additionalProperties\": false\n },\n \"nameComponent\": {\n \"type\": \"object\",\n \"required\": [\"kind\", \"value\"],\n \"properties\": {\n \"@type\": { \"const\": \"NameComponent\" },\n \"kind\": {\n \"enum\": [\n \"credential\",\n \"generation\",\n \"given\",\n \"given2\",\n \"separator\",\n \"surname\",\n \"surname2\",\n \"title\"\n ]\n },\n \"value\": { \"type\": \"string\" }\n },\n \"additionalProperties\": false\n },\n \"emailAddress\": {\n \"type\": \"object\",\n \"required\": [\"address\"],\n \"properties\": {\n \"@type\": { \"const\": \"EmailAddress\" },\n \"address\": { \"type\": \"string\", \"format\": \"email\" },\n \"contexts\": { \"$ref\": \"#/$defs/trueSet\" },\n \"pref\": { \"type\": \"integer\", \"minimum\": 1 },\n \"label\": { \"type\": \"string\" }\n },\n \"additionalProperties\": false\n },\n \"phone\": {\n \"type\": \"object\",\n \"required\": [\"number\"],\n \"properties\": {\n \"@type\": { \"const\": \"Phone\" },\n \"number\": { \"type\": \"string\", \"minLength\": 1 },\n \"features\": { \"$ref\": \"#/$defs/trueSet\" },\n \"contexts\": { \"$ref\": \"#/$defs/trueSet\" },\n \"pref\": { \"type\": \"integer\", \"minimum\": 1 },\n \"label\": { \"type\": \"string\" }\n },\n \"additionalProperties\": false\n },\n \"organization\": {\n \"type\": \"object\",\n \"properties\": {\n \"@type\": { \"const\": \"Organization\" },\n \"name\": { \"type\": \"string\", \"minLength\": 1 },\n \"units\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": { \"$ref\": \"#/$defs/orgUnit\" }\n },\n \"sortAs\": { \"type\": \"string\" },\n \"contexts\": { \"$ref\": \"#/$defs/trueSet\" }\n },\n \"anyOf\": [\n { \"required\": [\"name\"] },\n { \"required\": [\"units\"] }\n ],\n \"additionalProperties\": false\n },\n \"orgUnit\": {\n \"type\": \"object\",\n \"required\": [\"name\"],\n \"properties\": {\n \"@type\": { \"const\": \"OrgUnit\" },\n \"name\": { \"type\": \"string\", \"minLength\": 1 }\n },\n \"additionalProperties\": false\n },\n \"note\": {\n \"type\": \"object\",\n \"required\": [\"note\"],\n \"properties\": {\n \"@type\": { \"const\": \"Note\" },\n \"note\": { \"type\": \"string\" },\n \"created\": { \"type\": \"string\", \"format\": \"date-time\" }\n },\n \"additionalProperties\": false\n }\n }\n}\n\n" + }, + { + "source": "contracts/mdbase.jscontact.card/2.0.0.md", + "document": "---\nkind: mdbase.contract\ncontract_type: record\nid: mdbase.jscontact.card\nversion: 2.0.0\nname: JSContact Card 2.0 core\ndescription: A strict, portable core profile of an IETF JSContact 2.0 Card.\nrecord_schema:\n dialect: json-schema-2020-12\n ref: ../../schemas/mdbase.jscontact.card/2.0.0.schema.json\nx-standard:\n name: JSContact\n version: \"2.0\"\n scope: Strict core profile covering the Card envelope, names, email addresses, phones, organizations, and notes.\n references:\n - https://www.rfc-editor.org/rfc/rfc9553.html\n - https://www.rfc-editor.org/rfc/rfc9982.html\n---\n\n# JSContact Card 2.0 core\n\nThis contract provides a deliberately bounded JSContact Card representation\nfor interoperable contact records. It follows JSContact 2.0, including the\noptional `uid` introduced by RFC 9982.\n\nThe profile accepts a strict subset of JSContact properties. It is not an\nofficial IETF JSON Schema and does not claim to validate every JSContact\nextension or conditional semantic rule.\n\n" + }, + { + "source": "types/jscontact-card/2.md", + "document": "---\nkind: mdbase.type\nname: jscontact-card\nversion: 2\ndescription: An editable Markdown-backed JSContact 2.0 core Card\nmatch:\n where:\n /@type: Card\n version: \"2.0\"\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n title: JSContact Card 2.0 core profile\n description: A local starting shape for an IETF JSContact 2.0 Card.\n type: object\n required:\n - \"@type\"\n - version\n properties:\n \"@type\":\n const: Card\n version:\n const: \"2.0\"\n uid:\n type: string\n minLength: 1\n kind:\n enum:\n - individual\n - group\n - org\n - location\n - device\n - application\n name:\n $ref: \"#/$defs/name\"\n emails:\n type: object\n propertyNames:\n $ref: \"#/$defs/id\"\n additionalProperties:\n $ref: \"#/$defs/emailAddress\"\n phones:\n type: object\n propertyNames:\n $ref: \"#/$defs/id\"\n additionalProperties:\n $ref: \"#/$defs/phone\"\n organizations:\n type: object\n propertyNames:\n $ref: \"#/$defs/id\"\n additionalProperties:\n $ref: \"#/$defs/organization\"\n notes:\n type: object\n propertyNames:\n $ref: \"#/$defs/id\"\n additionalProperties:\n $ref: \"#/$defs/note\"\n additionalProperties: false\n $defs:\n id:\n type: string\n minLength: 1\n trueSet:\n type: object\n additionalProperties:\n const: true\n name:\n type: object\n properties:\n \"@type\":\n const: Name\n components:\n type: array\n minItems: 1\n contains:\n type: object\n required:\n - kind\n properties:\n kind:\n not:\n const: separator\n items:\n $ref: \"#/$defs/nameComponent\"\n full:\n type: string\n minLength: 1\n isOrdered:\n type: boolean\n defaultSeparator:\n type: string\n anyOf:\n - required:\n - components\n - required:\n - full\n additionalProperties: false\n nameComponent:\n type: object\n required:\n - kind\n - value\n properties:\n \"@type\":\n const: NameComponent\n kind:\n enum:\n - credential\n - generation\n - given\n - given2\n - separator\n - surname\n - surname2\n - title\n value:\n type: string\n additionalProperties: false\n emailAddress:\n type: object\n required:\n - address\n properties:\n \"@type\":\n const: EmailAddress\n address:\n type: string\n format: email\n contexts:\n $ref: \"#/$defs/trueSet\"\n pref:\n type: integer\n minimum: 1\n label:\n type: string\n additionalProperties: false\n phone:\n type: object\n required:\n - number\n properties:\n \"@type\":\n const: Phone\n number:\n type: string\n minLength: 1\n features:\n $ref: \"#/$defs/trueSet\"\n contexts:\n $ref: \"#/$defs/trueSet\"\n pref:\n type: integer\n minimum: 1\n label:\n type: string\n additionalProperties: false\n organization:\n type: object\n properties:\n \"@type\":\n const: Organization\n name:\n type: string\n minLength: 1\n units:\n type: array\n minItems: 1\n items:\n $ref: \"#/$defs/orgUnit\"\n sortAs:\n type: string\n contexts:\n $ref: \"#/$defs/trueSet\"\n anyOf:\n - required:\n - name\n - required:\n - units\n additionalProperties: false\n orgUnit:\n type: object\n required:\n - name\n properties:\n \"@type\":\n const: OrgUnit\n name:\n type: string\n minLength: 1\n additionalProperties: false\n note:\n type: object\n required:\n - note\n properties:\n \"@type\":\n const: Note\n note:\n type: string\n created:\n type: string\n format: date-time\n additionalProperties: false\ncollection:\n display:\n name_field: /name/full\nimplements:\n - contract: mdbase.jscontact.card\n version: 2.0.0\n fields:\n /@type: /@type\n version: version\n uid: uid\n kind: kind\n name: name\n emails: emails\n phones: phones\n organizations: organizations\n notes: notes\n---\n\n# JSContact Card\n\nThis starter type begins as a local copy of the JSContact core profile. Its\nfields belong to the collection and can be added, renamed, or reshaped. Update\nthe explicit contract mapping whenever a compatible field moves.\n\nThe separately installed contract schema remains the portable boundary. Local\nfields that are not mapped into that boundary remain collection-specific.\n" + } + ], + "provides": [ + { + "id": "mdbase.jscontact.card", + "version": "2.0.0" + } + ] +} diff --git a/packs/mdbase.jscontact/2.0.0.pack.yaml b/packs/mdbase.jscontact/2.0.0.pack.yaml index c0a88f6..8a8aa08 100644 --- a/packs/mdbase.jscontact/2.0.0.pack.yaml +++ b/packs/mdbase.jscontact/2.0.0.pack.yaml @@ -4,6 +4,7 @@ version: 2.0.0 name: JSContact 2.0 starter description: The mdbase JSContact Card core contract and a direct Markdown implementation. featured: true +catalog: false provides: - id: mdbase.jscontact.card version: 2.0.0 @@ -17,4 +18,3 @@ resources: - kind: type source: types/jscontact-card/1.md target: _types/jscontact-card.md - diff --git a/packs/mdbase.jscontact/2.0.1.pack.yaml b/packs/mdbase.jscontact/2.0.1.pack.yaml new file mode 100644 index 0000000..2d1f31a --- /dev/null +++ b/packs/mdbase.jscontact/2.0.1.pack.yaml @@ -0,0 +1,19 @@ +kind: mdbase.catalog-pack +id: mdbase.jscontact +version: 2.0.1 +name: JSContact 2.0 starter +description: The mdbase JSContact Card core contract and an editable Markdown implementation. +featured: true +provides: + - id: mdbase.jscontact.card + version: 2.0.0 +resources: + - kind: schema + source: schemas/mdbase.jscontact.card/2.0.0.schema.json + target: schemas/mdbase.jscontact.card/2.0.0.schema.json + - kind: contract + source: contracts/mdbase.jscontact.card/2.0.0.md + target: _contracts/mdbase.jscontact.card/2.0.0.md + - kind: type + source: types/jscontact-card/2.md + target: _types/jscontact-card.md diff --git a/scripts/build.mjs b/scripts/build.mjs index 62a89ab..d2029de 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -69,6 +69,9 @@ for (const packFile of packFiles) { if (resource.kind === "contract") { registerContract(resource.source, document, resourceDigest); } + if (resource.kind === "type" && definition.catalog !== false) { + validateEditableType(resource.source, document); + } } const provision = { @@ -88,17 +91,19 @@ for (const packFile of packFiles) { await mkdir(dirname(join(dist, provisionPath)), { recursive: true }); await writeFile(join(dist, provisionPath), provisionDocument); - packs.push({ - id: definition.id, - version: definition.version, - name: definition.name, - description: definition.description, - digest: digest(provisionDocument), - provision: `./${provisionPath}`, - provides: definition.provides, - resource_count: definition.resources.length, - featured: definition.featured, - }); + if (definition.catalog !== false) { + packs.push({ + id: definition.id, + version: definition.version, + name: definition.name, + description: definition.description, + digest: digest(provisionDocument), + provision: `./${provisionPath}`, + provides: definition.provides, + resource_count: definition.resources.length, + featured: definition.featured, + }); + } } const catalog = { @@ -144,6 +149,25 @@ function registerContract(source, document, resourceDigest) { contracts.set(identity, entry); } +function validateEditableType(source, document) { + const frontmatter = matter(document).data; + if (frontmatter.kind !== "mdbase.type") { + fail(`Type resource ${source} is not an mdbase.type document.`); + } + if ( + !frontmatter.schema + || typeof frontmatter.schema !== "object" + || !frontmatter.schema.value + || typeof frontmatter.schema.value !== "object" + || Array.isArray(frontmatter.schema.value) + ) { + fail(`Catalog type resource ${source} must contain an editable schema.value snapshot.`); + } + if (frontmatter.schema.ref !== undefined) { + fail(`Catalog type resource ${source} must not inherit a referenced schema.`); + } +} + function validatePackDefinition(value, label) { if (!value || typeof value !== "object" || value.kind !== "mdbase.catalog-pack") { fail(`${label} must be an mdbase.catalog-pack object.`); @@ -154,6 +178,9 @@ function validatePackDefinition(value, label) { } } if (typeof value.featured !== "boolean") fail(`${label} must declare featured.`); + if (value.catalog !== undefined && typeof value.catalog !== "boolean") { + fail(`${label} catalog must be a boolean.`); + } if (!Array.isArray(value.provides) || value.provides.length === 0) { fail(`${label} must provide at least one contract.`); } diff --git a/types/jscontact-card/2.md b/types/jscontact-card/2.md new file mode 100644 index 0000000..4af1f65 --- /dev/null +++ b/types/jscontact-card/2.md @@ -0,0 +1,233 @@ +--- +kind: mdbase.type +name: jscontact-card +version: 2 +description: An editable Markdown-backed JSContact 2.0 core Card +match: + where: + /@type: Card + version: "2.0" +schema: + dialect: json-schema-2020-12 + value: + $schema: https://json-schema.org/draft/2020-12/schema + title: JSContact Card 2.0 core profile + description: A local starting shape for an IETF JSContact 2.0 Card. + type: object + required: + - "@type" + - version + properties: + "@type": + const: Card + version: + const: "2.0" + uid: + type: string + minLength: 1 + kind: + enum: + - individual + - group + - org + - location + - device + - application + name: + $ref: "#/$defs/name" + emails: + type: object + propertyNames: + $ref: "#/$defs/id" + additionalProperties: + $ref: "#/$defs/emailAddress" + phones: + type: object + propertyNames: + $ref: "#/$defs/id" + additionalProperties: + $ref: "#/$defs/phone" + organizations: + type: object + propertyNames: + $ref: "#/$defs/id" + additionalProperties: + $ref: "#/$defs/organization" + notes: + type: object + propertyNames: + $ref: "#/$defs/id" + additionalProperties: + $ref: "#/$defs/note" + additionalProperties: false + $defs: + id: + type: string + minLength: 1 + trueSet: + type: object + additionalProperties: + const: true + name: + type: object + properties: + "@type": + const: Name + components: + type: array + minItems: 1 + contains: + type: object + required: + - kind + properties: + kind: + not: + const: separator + items: + $ref: "#/$defs/nameComponent" + full: + type: string + minLength: 1 + isOrdered: + type: boolean + defaultSeparator: + type: string + anyOf: + - required: + - components + - required: + - full + additionalProperties: false + nameComponent: + type: object + required: + - kind + - value + properties: + "@type": + const: NameComponent + kind: + enum: + - credential + - generation + - given + - given2 + - separator + - surname + - surname2 + - title + value: + type: string + additionalProperties: false + emailAddress: + type: object + required: + - address + properties: + "@type": + const: EmailAddress + address: + type: string + format: email + contexts: + $ref: "#/$defs/trueSet" + pref: + type: integer + minimum: 1 + label: + type: string + additionalProperties: false + phone: + type: object + required: + - number + properties: + "@type": + const: Phone + number: + type: string + minLength: 1 + features: + $ref: "#/$defs/trueSet" + contexts: + $ref: "#/$defs/trueSet" + pref: + type: integer + minimum: 1 + label: + type: string + additionalProperties: false + organization: + type: object + properties: + "@type": + const: Organization + name: + type: string + minLength: 1 + units: + type: array + minItems: 1 + items: + $ref: "#/$defs/orgUnit" + sortAs: + type: string + contexts: + $ref: "#/$defs/trueSet" + anyOf: + - required: + - name + - required: + - units + additionalProperties: false + orgUnit: + type: object + required: + - name + properties: + "@type": + const: OrgUnit + name: + type: string + minLength: 1 + additionalProperties: false + note: + type: object + required: + - note + properties: + "@type": + const: Note + note: + type: string + created: + type: string + format: date-time + additionalProperties: false +collection: + display: + name_field: /name/full +implements: + - contract: mdbase.jscontact.card + version: 2.0.0 + fields: + /@type: /@type + version: version + uid: uid + kind: kind + name: name + emails: emails + phones: phones + organizations: organizations + notes: notes +--- + +# JSContact Card + +This starter type begins as a local copy of the JSContact core profile. Its +fields belong to the collection and can be added, renamed, or reshaped. Update +the explicit contract mapping whenever a compatible field moves. + +The separately installed contract schema remains the portable boundary. Local +fields that are not mapped into that boundary remain collection-specific.