From 6df96d31cce19b1d60fe44d68967a4206cc59d55 Mon Sep 17 00:00:00 2001 From: Madraka <8898640+Madraka@users.noreply.github.com> Date: Wed, 11 Nov 2026 03:45:48 +0300 Subject: [PATCH 1/4] =?UTF-8?q?feat(contracts):=20reference=20and=20repeat?= =?UTF-8?q?er=20field=20types=20(RFC-0045=20=C2=A71,=20#114)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the ReferenceField and RepeaterField variants to FieldDefinition, the SubFieldDefinition union (nine *SubField variants, scalar values only, no recursion), and FieldMigration's reference/repeater fromType and sub. The content entry's fields description names the new values. Fixtures: one valid stage-4 site, twelve invalid ones. TS and Rust regenerated. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com> --- .../src/generated/content_entry.rs | 2 +- crates/cloudhal-schema/src/generated/site.rs | 198 + .../site/field-migration-from-blocks.json | 97 + .../site/field-migration-unknown-type.json | 2 +- .../field-reference-duplicate-collection.json | 37 + .../site/field-reference-max-items-51.json | 39 + .../site/field-reference-six-collections.json | 37 + .../field-reference-without-collections.json | 36 + .../field-repeater-datetime-sub-field.json | 102 + .../field-repeater-eleven-sub-fields.json | 139 + .../site/field-repeater-max-items-51.json | 93 + .../site/field-repeater-nested-repeater.json | 113 + .../field-repeater-sub-field-localized.json | 103 + .../field-repeater-sub-field-multiple.json | 111 + .../site/field-repeater-without-fields.json | 39 + .../valid/site/collection-stage4.json | 225 + .../schemas/content-entry.schema.json | 2 +- .../core-contracts/schemas/site.schema.json | 353 +- .../src/generated/content-entry.ts | 2 +- .../core-contracts/src/generated/index.ts | 2 +- .../core-contracts/src/generated/schemas.ts | 427 +- packages/core-contracts/src/generated/site.ts | 963 ++- .../src/generated/validators.js | 5873 ++++++++++++----- 23 files changed, 6893 insertions(+), 2102 deletions(-) create mode 100644 packages/core-contracts/fixtures/invalid/site/field-migration-from-blocks.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-reference-duplicate-collection.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-reference-max-items-51.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-reference-six-collections.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-reference-without-collections.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-repeater-datetime-sub-field.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-repeater-eleven-sub-fields.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-repeater-max-items-51.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-repeater-nested-repeater.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-repeater-sub-field-localized.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-repeater-sub-field-multiple.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-repeater-without-fields.json create mode 100644 packages/core-contracts/fixtures/valid/site/collection-stage4.json diff --git a/crates/cloudhal-schema/src/generated/content_entry.rs b/crates/cloudhal-schema/src/generated/content_entry.rs index cd081298..be42c657 100644 --- a/crates/cloudhal-schema/src/generated/content_entry.rs +++ b/crates/cloudhal-schema/src/generated/content_entry.rs @@ -502,7 +502,7 @@ pub struct ContentEntry { pub cover_image: ::std::option::Option, #[serde(rename = "createdAt")] pub created_at: Timestamp, - #[doc = "Collection-specific values, keyed by field. A collection with typed fields (RFC-0041, Site.collections..fields) checks each value against its definition on write; otherwise any JSON. An absent key means no value."] + #[doc = "Collection-specific values, keyed by field. A collection with typed fields (RFC-0041, Site.collections..fields) checks each value against its definition on write; otherwise any JSON. An absent key means no value. A reference value is a translation group id or a list of them, a repeater value a list of flat objects (RFC-0045)."] pub fields: ::serde_json::Map<::std::string::String, ::serde_json::Value>, pub id: Id, pub locale: Locale, diff --git a/crates/cloudhal-schema/src/generated/site.rs b/crates/cloudhal-schema/src/generated/site.rs index 6b5f8aef..dc6c0179 100644 --- a/crates/cloudhal-schema/src/generated/site.rs +++ b/crates/cloudhal-schema/src/generated/site.rs @@ -2772,6 +2772,70 @@ pub enum FieldDefinition { )] required_to_publish: ::std::option::Option, }, + #[doc = "ReferenceField\n\nRFC-0045: entries of other (or this) collections of the same site, by translation group id (entry_…): the id of the group's original, which every language version and edition carries in translation.group."] + #[serde(rename = "reference")] + Reference { + #[doc = "The collections a target may belong to: declared collections of this site (checked in code)."] + collections: ::std::vec::Vec, + key: FieldKey, + labels: FieldLabels, + #[doc = "true: each language version picks its own targets; false: only the translation group's original holds them, and every version resolves them in its own language."] + localized: bool, + #[doc = "With multiple: at most this many targets. Absent: 20."] + #[serde( + rename = "maxItems", + skip_serializing_if = "::std::option::Option::is_none" + )] + max_items: ::std::option::Option<::std::num::NonZeroU64>, + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + migration: ::std::option::Option, + #[doc = "The value is an ordered list of targets. Absent: false."] + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + multiple: ::std::option::Option, + #[doc = "An entry in review or published needs at least one target. Absent: false."] + #[serde( + rename = "requiredToPublish", + skip_serializing_if = "::std::option::Option::is_none" + )] + required_to_publish: ::std::option::Option, + #[doc = "Only with one target collection: conditions on its list fields that a newly chosen target must meet (no `now`; checked in code). Narrows the picker; not re-checked when the target changes later."] + #[serde( + rename = "where", + default, + skip_serializing_if = "::std::vec::Vec::is_empty" + )] + where_: ::std::vec::Vec, + }, + #[doc = "RepeaterField\n\nRFC-0045: an ordered list of items, each a flat object of sub-fields. A repeater cannot contain a repeater."] + #[serde(rename = "repeater")] + Repeater { + #[doc = "The sub-fields of every item, in form order. Unique keys (checked in code)."] + fields: ::std::vec::Vec, + #[doc = "The text, select or reference sub-field that names an item in Studio and approval prompts. Absent: the first such sub-field."] + #[serde( + rename = "itemLabel", + skip_serializing_if = "::std::option::Option::is_none" + )] + item_label: ::std::option::Option, + key: FieldKey, + labels: FieldLabels, + #[doc = "true: each language version has its own list (missing reads the original's); false: only the original holds the list."] + localized: bool, + #[doc = "At most this many items. Absent: 20."] + #[serde( + rename = "maxItems", + skip_serializing_if = "::std::option::Option::is_none" + )] + max_items: ::std::option::Option<::std::num::NonZeroU64>, + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + migration: ::std::option::Option, + #[doc = "An entry in review or published needs at least one item. Absent: false."] + #[serde( + rename = "requiredToPublish", + skip_serializing_if = "::std::option::Option::is_none" + )] + required_to_publish: ::std::option::Option, + }, } #[doc = "The field's key in ContentEntry.fields: lower camelCase, a JavaScript identifier. Unique in its collection; built-in entry property names are reserved (checked in code)."] #[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] @@ -2979,6 +3043,9 @@ pub struct FieldMigration { pub from_type: ::std::option::Option, #[doc = "The operation's \"before\" snapshot."] pub id: Id, + #[doc = "RFC-0045: the repeater's sub-field the operation rewrites (its key once the operation is done); fromKey and fromType then describe the sub-field."] + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + pub sub: ::std::option::Option, } #[doc = "`FieldMigrationFromType`"] #[derive( @@ -3012,6 +3079,10 @@ pub enum FieldMigrationFromType { Image, #[serde(rename = "select")] Select, + #[serde(rename = "reference")] + Reference, + #[serde(rename = "repeater")] + Repeater, } impl ::std::fmt::Display for FieldMigrationFromType { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { @@ -3025,6 +3096,8 @@ impl ::std::fmt::Display for FieldMigrationFromType { Self::Url => f.write_str("url"), Self::Image => f.write_str("image"), Self::Select => f.write_str("select"), + Self::Reference => f.write_str("reference"), + Self::Repeater => f.write_str("repeater"), } } } @@ -3041,6 +3114,8 @@ impl ::std::str::FromStr for FieldMigrationFromType { "url" => Ok(Self::Url), "image" => Ok(Self::Image), "select" => Ok(Self::Select), + "reference" => Ok(Self::Reference), + "repeater" => Ok(Self::Repeater), _ => Err("invalid value".into()), } } @@ -5452,6 +5527,129 @@ impl<'de> ::serde::Deserialize<'de> for StructuredDataSourceValue { }) } } +#[doc = "RFC-0045: one sub-field of a repeater item, discriminated by `type`. Only types whose value is a scalar: no datetime, no multiple select or reference, no repeater. No localized and no migration: the repeater's apply."] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, PartialEq)] +#[serde(tag = "type", deny_unknown_fields)] +pub enum SubFieldDefinition { + #[doc = "TextSubField"] + #[serde(rename = "text")] + Text { + key: FieldKey, + labels: FieldLabels, + #[doc = "Absent: 5000."] + #[serde( + rename = "maxLength", + skip_serializing_if = "::std::option::Option::is_none" + )] + max_length: ::std::option::Option<::std::num::NonZeroU64>, + #[doc = "Line breaks allowed. Absent: false."] + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + multiline: ::std::option::Option, + #[doc = "Every item of an entry in review or published needs a value. Absent: false."] + #[serde( + rename = "requiredToPublish", + skip_serializing_if = "::std::option::Option::is_none" + )] + required_to_publish: ::std::option::Option, + }, + #[doc = "RichTextSubField"] + #[serde(rename = "richText")] + RichText { + key: FieldKey, + labels: FieldLabels, + #[doc = "Every item of an entry in review or published needs a value. Absent: false."] + #[serde( + rename = "requiredToPublish", + skip_serializing_if = "::std::option::Option::is_none" + )] + required_to_publish: ::std::option::Option, + }, + #[doc = "NumberSubField"] + #[serde(rename = "number")] + Number { + #[doc = "Whole numbers only. Absent: false."] + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + integer: ::std::option::Option, + key: FieldKey, + labels: FieldLabels, + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + maximum: ::std::option::Option, + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + minimum: ::std::option::Option, + #[doc = "Every item of an entry in review or published needs a value. Absent: false."] + #[serde( + rename = "requiredToPublish", + skip_serializing_if = "::std::option::Option::is_none" + )] + required_to_publish: ::std::option::Option, + }, + #[doc = "BooleanSubField"] + #[serde(rename = "boolean")] + Boolean { key: FieldKey, labels: FieldLabels }, + #[doc = "DateSubField"] + #[serde(rename = "date")] + Date { + key: FieldKey, + labels: FieldLabels, + #[doc = "Every item of an entry in review or published needs a value. Absent: false."] + #[serde( + rename = "requiredToPublish", + skip_serializing_if = "::std::option::Option::is_none" + )] + required_to_publish: ::std::option::Option, + }, + #[doc = "UrlSubField"] + #[serde(rename = "url")] + Url { + key: FieldKey, + labels: FieldLabels, + #[doc = "Every item of an entry in review or published needs a value. Absent: false."] + #[serde( + rename = "requiredToPublish", + skip_serializing_if = "::std::option::Option::is_none" + )] + required_to_publish: ::std::option::Option, + }, + #[doc = "ImageSubField"] + #[serde(rename = "image")] + Image { + key: FieldKey, + labels: FieldLabels, + #[doc = "Every item of an entry in review or published needs a value. Absent: false."] + #[serde( + rename = "requiredToPublish", + skip_serializing_if = "::std::option::Option::is_none" + )] + required_to_publish: ::std::option::Option, + }, + #[doc = "SelectSubField"] + #[serde(rename = "select")] + Select { + key: FieldKey, + labels: FieldLabels, + options: ::std::vec::Vec, + #[doc = "Every item of an entry in review or published needs a value. Absent: false."] + #[serde( + rename = "requiredToPublish", + skip_serializing_if = "::std::option::Option::is_none" + )] + required_to_publish: ::std::option::Option, + }, + #[doc = "ReferenceSubField"] + #[serde(rename = "reference")] + Reference { + #[doc = "The collections a target may belong to (checked in code)."] + collections: ::std::vec::Vec, + key: FieldKey, + labels: FieldLabels, + #[doc = "Every item of an entry in review or published needs a value. Absent: false."] + #[serde( + rename = "requiredToPublish", + skip_serializing_if = "::std::option::Option::is_none" + )] + required_to_publish: ::std::option::Option, + }, +} #[doc = "One element of a generative UI surface (RFC-0020 §1), in a flat list: `component` names a catalog entry (a PascalCase built-in primitive such as `Stack`, or a registry block type such as `callout`), `children` refer to other ids of the same surface, and `props` follow the entry's schema, which the store checks. Field names follow A2UI v0.9."] #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, PartialEq)] #[serde(deny_unknown_fields)] diff --git a/packages/core-contracts/fixtures/invalid/site/field-migration-from-blocks.json b/packages/core-contracts/fixtures/invalid/site/field-migration-from-blocks.json new file mode 100644 index 00000000..5eee2472 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-migration-from-blocks.json @@ -0,0 +1,97 @@ +{ + "reason": "a migration's fromType is one of the eleven types", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-11-11T09:00:00Z", + "updatedAt": "2026-11-11T09:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "speakers", + "type": "repeater", + "labels": { + "en": { + "label": "Speakers" + } + }, + "localized": false, + "maxItems": 20, + "itemLabel": "person", + "fields": [ + { + "key": "person", + "type": "reference", + "labels": { + "en": { + "label": "Person" + } + }, + "requiredToPublish": true, + "collections": ["people"] + }, + { + "key": "role", + "type": "select", + "labels": { + "en": { + "label": "Role" + } + }, + "options": [ + { + "value": "host", + "labels": { + "en": "Host" + } + }, + { + "value": "speaker", + "labels": { + "en": "Speaker" + } + } + ] + }, + { + "key": "photo", + "type": "image", + "labels": { + "en": { + "label": "Photo" + } + } + }, + { + "key": "talk", + "type": "text", + "labels": { + "en": { + "label": "Talk" + } + }, + "maxLength": 200 + } + ], + "migration": { + "id": "snap_01HZX8K3Q9MG", + "fromType": "blocks" + } + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-migration-unknown-type.json b/packages/core-contracts/fixtures/invalid/site/field-migration-unknown-type.json index be54012c..cf110278 100644 --- a/packages/core-contracts/fixtures/invalid/site/field-migration-unknown-type.json +++ b/packages/core-contracts/fixtures/invalid/site/field-migration-unknown-type.json @@ -1,5 +1,5 @@ { - "reason": "a migration's fromType is one of the nine types", + "reason": "a migration's fromType is one of the eleven types", "data": { "schemaVersion": 1, "id": "site_01HZX8K3Q9T1", diff --git a/packages/core-contracts/fixtures/invalid/site/field-reference-duplicate-collection.json b/packages/core-contracts/fixtures/invalid/site/field-reference-duplicate-collection.json new file mode 100644 index 00000000..79258952 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-reference-duplicate-collection.json @@ -0,0 +1,37 @@ +{ + "reason": "a reference field names each target collection once", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-11-11T09:00:00Z", + "updatedAt": "2026-11-11T09:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "venue", + "type": "reference", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": false, + "collections": ["places", "places"] + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-reference-max-items-51.json b/packages/core-contracts/fixtures/invalid/site/field-reference-max-items-51.json new file mode 100644 index 00000000..4ae9a3af --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-reference-max-items-51.json @@ -0,0 +1,39 @@ +{ + "reason": "a reference field holds at most fifty targets", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-11-11T09:00:00Z", + "updatedAt": "2026-11-11T09:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "venue", + "type": "reference", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": false, + "collections": ["places"], + "multiple": true, + "maxItems": 51 + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-reference-six-collections.json b/packages/core-contracts/fixtures/invalid/site/field-reference-six-collections.json new file mode 100644 index 00000000..82a285c0 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-reference-six-collections.json @@ -0,0 +1,37 @@ +{ + "reason": "a reference field allows at most five target collections", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-11-11T09:00:00Z", + "updatedAt": "2026-11-11T09:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "venue", + "type": "reference", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": false, + "collections": ["a", "b", "c", "d", "e", "f"] + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-reference-without-collections.json b/packages/core-contracts/fixtures/invalid/site/field-reference-without-collections.json new file mode 100644 index 00000000..08659281 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-reference-without-collections.json @@ -0,0 +1,36 @@ +{ + "reason": "a reference field names its target collections", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-11-11T09:00:00Z", + "updatedAt": "2026-11-11T09:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "venue", + "type": "reference", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": false + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-repeater-datetime-sub-field.json b/packages/core-contracts/fixtures/invalid/site/field-repeater-datetime-sub-field.json new file mode 100644 index 00000000..48fcfe2d --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-repeater-datetime-sub-field.json @@ -0,0 +1,102 @@ +{ + "reason": "a repeater sub-field is a scalar: no datetime", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-11-11T09:00:00Z", + "updatedAt": "2026-11-11T09:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "speakers", + "type": "repeater", + "labels": { + "en": { + "label": "Speakers" + } + }, + "localized": false, + "maxItems": 20, + "itemLabel": "person", + "fields": [ + { + "key": "person", + "type": "reference", + "labels": { + "en": { + "label": "Person" + } + }, + "requiredToPublish": true, + "collections": ["people"] + }, + { + "key": "role", + "type": "select", + "labels": { + "en": { + "label": "Role" + } + }, + "options": [ + { + "value": "host", + "labels": { + "en": "Host" + } + }, + { + "value": "speaker", + "labels": { + "en": "Speaker" + } + } + ] + }, + { + "key": "photo", + "type": "image", + "labels": { + "en": { + "label": "Photo" + } + } + }, + { + "key": "talk", + "type": "text", + "labels": { + "en": { + "label": "Talk" + } + }, + "maxLength": 200 + }, + { + "key": "at", + "type": "datetime", + "labels": { + "en": { + "label": "At" + } + } + } + ] + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-repeater-eleven-sub-fields.json b/packages/core-contracts/fixtures/invalid/site/field-repeater-eleven-sub-fields.json new file mode 100644 index 00000000..e9e548e9 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-repeater-eleven-sub-fields.json @@ -0,0 +1,139 @@ +{ + "reason": "a repeater has at most ten sub-fields", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-11-11T09:00:00Z", + "updatedAt": "2026-11-11T09:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "speakers", + "type": "repeater", + "labels": { + "en": { + "label": "Speakers" + } + }, + "localized": false, + "maxItems": 20, + "itemLabel": "person", + "fields": [ + { + "key": "sub0", + "type": "text", + "labels": { + "en": { + "label": "Sub 0" + } + } + }, + { + "key": "sub1", + "type": "text", + "labels": { + "en": { + "label": "Sub 1" + } + } + }, + { + "key": "sub2", + "type": "text", + "labels": { + "en": { + "label": "Sub 2" + } + } + }, + { + "key": "sub3", + "type": "text", + "labels": { + "en": { + "label": "Sub 3" + } + } + }, + { + "key": "sub4", + "type": "text", + "labels": { + "en": { + "label": "Sub 4" + } + } + }, + { + "key": "sub5", + "type": "text", + "labels": { + "en": { + "label": "Sub 5" + } + } + }, + { + "key": "sub6", + "type": "text", + "labels": { + "en": { + "label": "Sub 6" + } + } + }, + { + "key": "sub7", + "type": "text", + "labels": { + "en": { + "label": "Sub 7" + } + } + }, + { + "key": "sub8", + "type": "text", + "labels": { + "en": { + "label": "Sub 8" + } + } + }, + { + "key": "sub9", + "type": "text", + "labels": { + "en": { + "label": "Sub 9" + } + } + }, + { + "key": "sub10", + "type": "text", + "labels": { + "en": { + "label": "Sub 10" + } + } + } + ] + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-repeater-max-items-51.json b/packages/core-contracts/fixtures/invalid/site/field-repeater-max-items-51.json new file mode 100644 index 00000000..d5716257 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-repeater-max-items-51.json @@ -0,0 +1,93 @@ +{ + "reason": "a repeater holds at most fifty items", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-11-11T09:00:00Z", + "updatedAt": "2026-11-11T09:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "speakers", + "type": "repeater", + "labels": { + "en": { + "label": "Speakers" + } + }, + "localized": false, + "maxItems": 51, + "itemLabel": "person", + "fields": [ + { + "key": "person", + "type": "reference", + "labels": { + "en": { + "label": "Person" + } + }, + "requiredToPublish": true, + "collections": ["people"] + }, + { + "key": "role", + "type": "select", + "labels": { + "en": { + "label": "Role" + } + }, + "options": [ + { + "value": "host", + "labels": { + "en": "Host" + } + }, + { + "value": "speaker", + "labels": { + "en": "Speaker" + } + } + ] + }, + { + "key": "photo", + "type": "image", + "labels": { + "en": { + "label": "Photo" + } + } + }, + { + "key": "talk", + "type": "text", + "labels": { + "en": { + "label": "Talk" + } + }, + "maxLength": 200 + } + ] + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-repeater-nested-repeater.json b/packages/core-contracts/fixtures/invalid/site/field-repeater-nested-repeater.json new file mode 100644 index 00000000..fb2feb9a --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-repeater-nested-repeater.json @@ -0,0 +1,113 @@ +{ + "reason": "a repeater cannot contain a repeater", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-11-11T09:00:00Z", + "updatedAt": "2026-11-11T09:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "speakers", + "type": "repeater", + "labels": { + "en": { + "label": "Speakers" + } + }, + "localized": false, + "maxItems": 20, + "itemLabel": "person", + "fields": [ + { + "key": "person", + "type": "reference", + "labels": { + "en": { + "label": "Person" + } + }, + "requiredToPublish": true, + "collections": ["people"] + }, + { + "key": "role", + "type": "select", + "labels": { + "en": { + "label": "Role" + } + }, + "options": [ + { + "value": "host", + "labels": { + "en": "Host" + } + }, + { + "value": "speaker", + "labels": { + "en": "Speaker" + } + } + ] + }, + { + "key": "photo", + "type": "image", + "labels": { + "en": { + "label": "Photo" + } + } + }, + { + "key": "talk", + "type": "text", + "labels": { + "en": { + "label": "Talk" + } + }, + "maxLength": 200 + }, + { + "key": "inner", + "type": "repeater", + "labels": { + "en": { + "label": "Inner" + } + }, + "fields": [ + { + "key": "x", + "type": "text", + "labels": { + "en": { + "label": "X" + } + } + } + ] + } + ] + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-repeater-sub-field-localized.json b/packages/core-contracts/fixtures/invalid/site/field-repeater-sub-field-localized.json new file mode 100644 index 00000000..1dc7f096 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-repeater-sub-field-localized.json @@ -0,0 +1,103 @@ +{ + "reason": "a sub-field has no localized of its own: the repeater's applies", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-11-11T09:00:00Z", + "updatedAt": "2026-11-11T09:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "speakers", + "type": "repeater", + "labels": { + "en": { + "label": "Speakers" + } + }, + "localized": false, + "maxItems": 20, + "itemLabel": "person", + "fields": [ + { + "key": "person", + "type": "reference", + "labels": { + "en": { + "label": "Person" + } + }, + "requiredToPublish": true, + "collections": ["people"] + }, + { + "key": "role", + "type": "select", + "labels": { + "en": { + "label": "Role" + } + }, + "options": [ + { + "value": "host", + "labels": { + "en": "Host" + } + }, + { + "value": "speaker", + "labels": { + "en": "Speaker" + } + } + ] + }, + { + "key": "photo", + "type": "image", + "labels": { + "en": { + "label": "Photo" + } + } + }, + { + "key": "talk", + "type": "text", + "labels": { + "en": { + "label": "Talk" + } + }, + "maxLength": 200 + }, + { + "key": "bio", + "type": "text", + "labels": { + "en": { + "label": "Bio" + } + }, + "localized": true + } + ] + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-repeater-sub-field-multiple.json b/packages/core-contracts/fixtures/invalid/site/field-repeater-sub-field-multiple.json new file mode 100644 index 00000000..fad2aa60 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-repeater-sub-field-multiple.json @@ -0,0 +1,111 @@ +{ + "reason": "a sub-field cannot be a multiple select", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-11-11T09:00:00Z", + "updatedAt": "2026-11-11T09:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "speakers", + "type": "repeater", + "labels": { + "en": { + "label": "Speakers" + } + }, + "localized": false, + "maxItems": 20, + "itemLabel": "person", + "fields": [ + { + "key": "person", + "type": "reference", + "labels": { + "en": { + "label": "Person" + } + }, + "requiredToPublish": true, + "collections": ["people"] + }, + { + "key": "role", + "type": "select", + "labels": { + "en": { + "label": "Role" + } + }, + "options": [ + { + "value": "host", + "labels": { + "en": "Host" + } + }, + { + "value": "speaker", + "labels": { + "en": "Speaker" + } + } + ] + }, + { + "key": "photo", + "type": "image", + "labels": { + "en": { + "label": "Photo" + } + } + }, + { + "key": "talk", + "type": "text", + "labels": { + "en": { + "label": "Talk" + } + }, + "maxLength": 200 + }, + { + "key": "tags", + "type": "select", + "labels": { + "en": { + "label": "Tags" + } + }, + "multiple": true, + "options": [ + { + "value": "a", + "labels": { + "en": "A" + } + } + ] + } + ] + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-repeater-without-fields.json b/packages/core-contracts/fixtures/invalid/site/field-repeater-without-fields.json new file mode 100644 index 00000000..06e51887 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-repeater-without-fields.json @@ -0,0 +1,39 @@ +{ + "reason": "a repeater has at least one sub-field", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-11-11T09:00:00Z", + "updatedAt": "2026-11-11T09:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "speakers", + "type": "repeater", + "labels": { + "en": { + "label": "Speakers" + } + }, + "localized": false, + "maxItems": 20, + "itemLabel": "person", + "fields": [] + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/valid/site/collection-stage4.json b/packages/core-contracts/fixtures/valid/site/collection-stage4.json new file mode 100644 index 00000000..f7ecd172 --- /dev/null +++ b/packages/core-contracts/fixtures/valid/site/collection-stage4.json @@ -0,0 +1,225 @@ +{ + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-11-11T09:00:00Z", + "updatedAt": "2026-11-11T09:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "venue", + "type": "reference", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": false, + "collections": ["places"] + }, + { + "key": "partners", + "type": "reference", + "labels": { + "en": { + "label": "Partners" + } + }, + "localized": true, + "collections": ["places", "people"], + "multiple": true, + "maxItems": 50, + "requiredToPublish": true + }, + { + "key": "host", + "type": "reference", + "labels": { + "en": { + "label": "Host" + } + }, + "localized": false, + "collections": ["people"], + "where": [ + { + "field": "role", + "op": "eq", + "value": "speaker" + } + ] + }, + { + "key": "speakers", + "type": "repeater", + "labels": { + "en": { + "label": "Speakers" + } + }, + "localized": false, + "maxItems": 20, + "itemLabel": "person", + "fields": [ + { + "key": "person", + "type": "reference", + "labels": { + "en": { + "label": "Person" + } + }, + "requiredToPublish": true, + "collections": ["people"] + }, + { + "key": "role", + "type": "select", + "labels": { + "en": { + "label": "Role" + } + }, + "options": [ + { + "value": "host", + "labels": { + "en": "Host" + } + }, + { + "value": "speaker", + "labels": { + "en": "Speaker" + } + } + ] + }, + { + "key": "photo", + "type": "image", + "labels": { + "en": { + "label": "Photo" + } + } + }, + { + "key": "talk", + "type": "text", + "labels": { + "en": { + "label": "Talk" + } + }, + "maxLength": 200 + } + ] + }, + { + "key": "agenda", + "type": "repeater", + "labels": { + "en": { + "label": "Agenda" + } + }, + "localized": true, + "fields": [ + { + "key": "title", + "type": "text", + "labels": { + "en": { + "label": "Title" + } + }, + "multiline": true, + "requiredToPublish": true + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "Notes" + } + } + }, + { + "key": "minutes", + "type": "number", + "labels": { + "en": { + "label": "Minutes" + } + }, + "integer": true, + "minimum": 0.5, + "maximum": 240.5 + }, + { + "key": "break", + "type": "boolean", + "labels": { + "en": { + "label": "Break" + } + } + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "Day" + } + } + }, + { + "key": "slides", + "type": "url", + "labels": { + "en": { + "label": "Slides" + } + } + } + ], + "migration": { + "id": "snap_01HZX8K3Q9MG", + "fromKey": "program", + "fromType": "text", + "sub": "title" + } + } + ] + }, + "places": { + "fields": [ + { + "key": "street", + "type": "text", + "labels": { + "en": { + "label": "Street" + } + }, + "localized": false + } + ] + }, + "people": {} + } +} diff --git a/packages/core-contracts/schemas/content-entry.schema.json b/packages/core-contracts/schemas/content-entry.schema.json index 09035ea4..c4b7e782 100644 --- a/packages/core-contracts/schemas/content-entry.schema.json +++ b/packages/core-contracts/schemas/content-entry.schema.json @@ -35,7 +35,7 @@ "body": { "$ref": "#/$defs/Body" }, "fields": { "type": "object", - "description": "Collection-specific values, keyed by field. A collection with typed fields (RFC-0041, Site.collections..fields) checks each value against its definition on write; otherwise any JSON. An absent key means no value.", + "description": "Collection-specific values, keyed by field. A collection with typed fields (RFC-0041, Site.collections..fields) checks each value against its definition on write; otherwise any JSON. An absent key means no value. A reference value is a translation group id or a list of them, a repeater value a list of flat objects (RFC-0045).", "additionalProperties": true }, "tags": { diff --git a/packages/core-contracts/schemas/site.schema.json b/packages/core-contracts/schemas/site.schema.json index c58400bc..8f156565 100644 --- a/packages/core-contracts/schemas/site.schema.json +++ b/packages/core-contracts/schemas/site.schema.json @@ -579,6 +579,351 @@ "description": "The value is a list of option values. Absent: false." } } + }, + { + "title": "ReferenceField", + "type": "object", + "description": "RFC-0045: entries of other (or this) collections of the same site, by translation group id (entry_…): the id of the group's original, which every language version and edition carries in translation.group.", + "additionalProperties": false, + "required": ["key", "type", "labels", "localized", "collections"], + "properties": { + "key": { + "$ref": "#/$defs/FieldKey" + }, + "type": { + "type": "string", + "enum": ["reference"] + }, + "labels": { + "$ref": "#/$defs/FieldLabels" + }, + "localized": { + "type": "boolean", + "description": "true: each language version picks its own targets; false: only the translation group's original holds them, and every version resolves them in its own language." + }, + "requiredToPublish": { + "type": "boolean", + "description": "An entry in review or published needs at least one target. Absent: false." + }, + "migration": { + "$ref": "#/$defs/FieldMigration" + }, + "collections": { + "type": "array", + "description": "The collections a target may belong to: declared collections of this site (checked in code).", + "minItems": 1, + "maxItems": 5, + "uniqueItems": true, + "items": { + "$ref": "common.schema.json#/$defs/Slug" + } + }, + "multiple": { + "type": "boolean", + "description": "The value is an ordered list of targets. Absent: false." + }, + "maxItems": { + "type": "integer", + "description": "With multiple: at most this many targets. Absent: 20.", + "minimum": 1, + "maximum": 50 + }, + "where": { + "type": "array", + "description": "Only with one target collection: conditions on its list fields that a newly chosen target must meet (no `now`; checked in code). Narrows the picker; not re-checked when the target changes later.", + "minItems": 1, + "maxItems": 4, + "items": { + "$ref": "common.schema.json#/$defs/FieldCondition" + } + } + } + }, + { + "title": "RepeaterField", + "type": "object", + "description": "RFC-0045: an ordered list of items, each a flat object of sub-fields. A repeater cannot contain a repeater.", + "additionalProperties": false, + "required": ["key", "type", "labels", "localized", "fields"], + "properties": { + "key": { + "$ref": "#/$defs/FieldKey" + }, + "type": { + "type": "string", + "enum": ["repeater"] + }, + "labels": { + "$ref": "#/$defs/FieldLabels" + }, + "localized": { + "type": "boolean", + "description": "true: each language version has its own list (missing reads the original's); false: only the original holds the list." + }, + "requiredToPublish": { + "type": "boolean", + "description": "An entry in review or published needs at least one item. Absent: false." + }, + "migration": { + "$ref": "#/$defs/FieldMigration" + }, + "fields": { + "type": "array", + "description": "The sub-fields of every item, in form order. Unique keys (checked in code).", + "minItems": 1, + "maxItems": 10, + "items": { + "$ref": "#/$defs/SubFieldDefinition" + } + }, + "maxItems": { + "type": "integer", + "description": "At most this many items. Absent: 20.", + "minimum": 1, + "maximum": 50 + }, + "itemLabel": { + "$ref": "#/$defs/FieldKey", + "description": "The text, select or reference sub-field that names an item in Studio and approval prompts. Absent: the first such sub-field." + } + } + } + ] + }, + "SubFieldDefinition": { + "description": "RFC-0045: one sub-field of a repeater item, discriminated by `type`. Only types whose value is a scalar: no datetime, no multiple select or reference, no repeater. No localized and no migration: the repeater's apply.", + "oneOf": [ + { + "title": "TextSubField", + "type": "object", + "additionalProperties": false, + "required": ["key", "type", "labels"], + "properties": { + "key": { + "$ref": "#/$defs/FieldKey" + }, + "type": { + "type": "string", + "enum": ["text"] + }, + "labels": { + "$ref": "#/$defs/FieldLabels" + }, + "requiredToPublish": { + "type": "boolean", + "description": "Every item of an entry in review or published needs a value. Absent: false." + }, + "maxLength": { + "type": "integer", + "description": "Absent: 5000.", + "minimum": 1, + "maximum": 5000 + }, + "multiline": { + "type": "boolean", + "description": "Line breaks allowed. Absent: false." + } + } + }, + { + "title": "RichTextSubField", + "type": "object", + "additionalProperties": false, + "required": ["key", "type", "labels"], + "properties": { + "key": { + "$ref": "#/$defs/FieldKey" + }, + "type": { + "type": "string", + "enum": ["richText"] + }, + "labels": { + "$ref": "#/$defs/FieldLabels" + }, + "requiredToPublish": { + "type": "boolean", + "description": "Every item of an entry in review or published needs a value. Absent: false." + } + } + }, + { + "title": "NumberSubField", + "type": "object", + "additionalProperties": false, + "required": ["key", "type", "labels"], + "properties": { + "key": { + "$ref": "#/$defs/FieldKey" + }, + "type": { + "type": "string", + "enum": ["number"] + }, + "labels": { + "$ref": "#/$defs/FieldLabels" + }, + "requiredToPublish": { + "type": "boolean", + "description": "Every item of an entry in review or published needs a value. Absent: false." + }, + "integer": { + "type": "boolean", + "description": "Whole numbers only. Absent: false." + }, + "minimum": { + "type": "number" + }, + "maximum": { + "type": "number" + } + } + }, + { + "title": "BooleanSubField", + "type": "object", + "additionalProperties": false, + "required": ["key", "type", "labels"], + "properties": { + "key": { + "$ref": "#/$defs/FieldKey" + }, + "type": { + "type": "string", + "enum": ["boolean"] + }, + "labels": { + "$ref": "#/$defs/FieldLabels" + } + } + }, + { + "title": "DateSubField", + "type": "object", + "additionalProperties": false, + "required": ["key", "type", "labels"], + "properties": { + "key": { + "$ref": "#/$defs/FieldKey" + }, + "type": { + "type": "string", + "enum": ["date"] + }, + "labels": { + "$ref": "#/$defs/FieldLabels" + }, + "requiredToPublish": { + "type": "boolean", + "description": "Every item of an entry in review or published needs a value. Absent: false." + } + } + }, + { + "title": "UrlSubField", + "type": "object", + "additionalProperties": false, + "required": ["key", "type", "labels"], + "properties": { + "key": { + "$ref": "#/$defs/FieldKey" + }, + "type": { + "type": "string", + "enum": ["url"] + }, + "labels": { + "$ref": "#/$defs/FieldLabels" + }, + "requiredToPublish": { + "type": "boolean", + "description": "Every item of an entry in review or published needs a value. Absent: false." + } + } + }, + { + "title": "ImageSubField", + "type": "object", + "additionalProperties": false, + "required": ["key", "type", "labels"], + "properties": { + "key": { + "$ref": "#/$defs/FieldKey" + }, + "type": { + "type": "string", + "enum": ["image"] + }, + "labels": { + "$ref": "#/$defs/FieldLabels" + }, + "requiredToPublish": { + "type": "boolean", + "description": "Every item of an entry in review or published needs a value. Absent: false." + } + } + }, + { + "title": "SelectSubField", + "type": "object", + "additionalProperties": false, + "required": ["key", "type", "labels", "options"], + "properties": { + "key": { + "$ref": "#/$defs/FieldKey" + }, + "type": { + "type": "string", + "enum": ["select"] + }, + "labels": { + "$ref": "#/$defs/FieldLabels" + }, + "requiredToPublish": { + "type": "boolean", + "description": "Every item of an entry in review or published needs a value. Absent: false." + }, + "options": { + "type": "array", + "minItems": 1, + "maxItems": 50, + "items": { + "$ref": "#/$defs/FieldOption" + } + } + } + }, + { + "title": "ReferenceSubField", + "type": "object", + "additionalProperties": false, + "required": ["key", "type", "labels", "collections"], + "properties": { + "key": { + "$ref": "#/$defs/FieldKey" + }, + "type": { + "type": "string", + "enum": ["reference"] + }, + "labels": { + "$ref": "#/$defs/FieldLabels" + }, + "requiredToPublish": { + "type": "boolean", + "description": "Every item of an entry in review or published needs a value. Absent: false." + }, + "collections": { + "type": "array", + "description": "The collections a target may belong to (checked in code).", + "minItems": 1, + "maxItems": 5, + "uniqueItems": true, + "items": { + "$ref": "common.schema.json#/$defs/Slug" + } + } + } } ] }, @@ -662,11 +1007,17 @@ "datetime", "url", "image", - "select" + "select", + "reference", + "repeater" ] }, "fromLocalized": { "type": "boolean" + }, + "sub": { + "$ref": "#/$defs/FieldKey", + "description": "RFC-0045: the repeater's sub-field the operation rewrites (its key once the operation is done); fromKey and fromType then describe the sub-field." } } }, diff --git a/packages/core-contracts/src/generated/content-entry.ts b/packages/core-contracts/src/generated/content-entry.ts index bae213b8..87d45511 100644 --- a/packages/core-contracts/src/generated/content-entry.ts +++ b/packages/core-contracts/src/generated/content-entry.ts @@ -100,7 +100,7 @@ export interface ContentEntry { summary?: string; body: Body; /** - * Collection-specific values, keyed by field. A collection with typed fields (RFC-0041, Site.collections..fields) checks each value against its definition on write; otherwise any JSON. An absent key means no value. + * Collection-specific values, keyed by field. A collection with typed fields (RFC-0041, Site.collections..fields) checks each value against its definition on write; otherwise any JSON. An absent key means no value. A reference value is a translation group id or a list of them, a repeater value a list of flat objects (RFC-0045). */ fields: { [k: string]: unknown | undefined; diff --git a/packages/core-contracts/src/generated/index.ts b/packages/core-contracts/src/generated/index.ts index df939360..b8e8a2a4 100644 --- a/packages/core-contracts/src/generated/index.ts +++ b/packages/core-contracts/src/generated/index.ts @@ -6,7 +6,7 @@ */ export type { SchemaVersion, Id, Slug, Revision, Timestamp, Project, Metadata } from "./project.ts"; -export type { Locale, FieldDefinition, FieldKey, StructuredDataSource, EntryValueProperty, SiteValueProperty, Section, BodyProfile, ActorKind, BodyBlocks, SurfaceComponentId, Path, Section1, TimeZone, Site, Domain, Localization, CollectionSettings, CollectionLabel, TextField, FieldLabels, FieldLabel, FieldMigration, RichTextField, NumberField, BooleanField, DateField, DateTimeField, UrlField, ImageField, SelectField, FieldOption, CollectionExpiry, StructuredDataPreset, FieldValueSource, EntryValueSource, SiteValueSource, ConstantValueSource, ThemeRef, Design, Brand, ImageRef, SocialLink, Chrome, ChromeHeader, HeaderAction, ChromeFooter, FooterColumn, HeroSection, Link, RichTextSection, BodyBlock, Actor, ImageSection, ContentListSection, FieldCondition, FieldOrder, ComponentSection, SurfaceSection, SurfaceComponent, Announcement, Consent } from "./site.ts"; +export type { Locale, FieldDefinition, FieldKey, SubFieldDefinition, StructuredDataSource, EntryValueProperty, SiteValueProperty, Section, BodyProfile, ActorKind, BodyBlocks, SurfaceComponentId, Path, Section1, TimeZone, Site, Domain, Localization, CollectionSettings, CollectionLabel, TextField, FieldLabels, FieldLabel, FieldMigration, RichTextField, NumberField, BooleanField, DateField, DateTimeField, UrlField, ImageField, SelectField, FieldOption, ReferenceField, FieldCondition, RepeaterField, TextSubField, RichTextSubField, NumberSubField, BooleanSubField, DateSubField, UrlSubField, ImageSubField, SelectSubField, ReferenceSubField, CollectionExpiry, StructuredDataPreset, FieldValueSource, EntryValueSource, SiteValueSource, ConstantValueSource, ThemeRef, Design, Brand, ImageRef, SocialLink, Chrome, ChromeHeader, HeaderAction, ChromeFooter, FooterColumn, HeroSection, Link, RichTextSection, BodyBlock, Actor, ImageSection, ContentListSection, FieldOrder, ComponentSection, SurfaceSection, SurfaceComponent, Announcement, Consent } from "./site.ts"; export type { PublishStatus, TranslationMethod, MotionLevel, Page, Seo, Availability, Translation } from "./page.ts"; export type { ContentEntry, Body, TermAssignment, Correction } from "./content-entry.ts"; export type { Snapshot, NodeRef, ChangeSummary } from "./snapshot.ts"; diff --git a/packages/core-contracts/src/generated/schemas.ts b/packages/core-contracts/src/generated/schemas.ts index dad84ff9..36527b44 100644 --- a/packages/core-contracts/src/generated/schemas.ts +++ b/packages/core-contracts/src/generated/schemas.ts @@ -1165,6 +1165,130 @@ export const SiteSchema = { "description": "The value is a list of option values. Absent: false." } } + }, + { + "title": "ReferenceField", + "type": "object", + "description": "RFC-0045: entries of other (or this) collections of the same site, by translation group id (entry_…): the id of the group's original, which every language version and edition carries in translation.group.", + "additionalProperties": false, + "required": [ + "key", + "type", + "labels", + "localized", + "collections" + ], + "properties": { + "key": { + "$ref": "#/$defs/FieldKey" + }, + "type": { + "type": "string", + "enum": [ + "reference" + ] + }, + "labels": { + "$ref": "#/$defs/FieldLabels" + }, + "localized": { + "type": "boolean", + "description": "true: each language version picks its own targets; false: only the translation group's original holds them, and every version resolves them in its own language." + }, + "requiredToPublish": { + "type": "boolean", + "description": "An entry in review or published needs at least one target. Absent: false." + }, + "migration": { + "$ref": "#/$defs/FieldMigration" + }, + "collections": { + "type": "array", + "description": "The collections a target may belong to: declared collections of this site (checked in code).", + "minItems": 1, + "maxItems": 5, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/Slug" + } + }, + "multiple": { + "type": "boolean", + "description": "The value is an ordered list of targets. Absent: false." + }, + "maxItems": { + "type": "integer", + "description": "With multiple: at most this many targets. Absent: 20.", + "minimum": 1, + "maximum": 50 + }, + "where": { + "type": "array", + "description": "Only with one target collection: conditions on its list fields that a newly chosen target must meet (no `now`; checked in code). Narrows the picker; not re-checked when the target changes later.", + "minItems": 1, + "maxItems": 4, + "items": { + "$ref": "#/$defs/FieldCondition" + } + } + } + }, + { + "title": "RepeaterField", + "type": "object", + "description": "RFC-0045: an ordered list of items, each a flat object of sub-fields. A repeater cannot contain a repeater.", + "additionalProperties": false, + "required": [ + "key", + "type", + "labels", + "localized", + "fields" + ], + "properties": { + "key": { + "$ref": "#/$defs/FieldKey" + }, + "type": { + "type": "string", + "enum": [ + "repeater" + ] + }, + "labels": { + "$ref": "#/$defs/FieldLabels" + }, + "localized": { + "type": "boolean", + "description": "true: each language version has its own list (missing reads the original's); false: only the original holds the list." + }, + "requiredToPublish": { + "type": "boolean", + "description": "An entry in review or published needs at least one item. Absent: false." + }, + "migration": { + "$ref": "#/$defs/FieldMigration" + }, + "fields": { + "type": "array", + "description": "The sub-fields of every item, in form order. Unique keys (checked in code).", + "minItems": 1, + "maxItems": 10, + "items": { + "$ref": "#/$defs/SubFieldDefinition" + } + }, + "maxItems": { + "type": "integer", + "description": "At most this many items. Absent: 20.", + "minimum": 1, + "maximum": 50 + }, + "itemLabel": { + "$ref": "#/$defs/FieldKey", + "description": "The text, select or reference sub-field that names an item in Studio and approval prompts. Absent: the first such sub-field." + } + } } ] }, @@ -1228,11 +1352,17 @@ export const SiteSchema = { "datetime", "url", "image", - "select" + "select", + "reference", + "repeater" ] }, "fromLocalized": { "type": "boolean" + }, + "sub": { + "$ref": "#/$defs/FieldKey", + "description": "RFC-0045: the repeater's sub-field the operation rewrites (its key once the operation is done); fromKey and fromType then describe the sub-field." } } }, @@ -1905,6 +2035,299 @@ export const SiteSchema = { } ] }, + "SubFieldDefinition": { + "description": "RFC-0045: one sub-field of a repeater item, discriminated by `type`. Only types whose value is a scalar: no datetime, no multiple select or reference, no repeater. No localized and no migration: the repeater's apply.", + "oneOf": [ + { + "title": "TextSubField", + "type": "object", + "additionalProperties": false, + "required": [ + "key", + "type", + "labels" + ], + "properties": { + "key": { + "$ref": "#/$defs/FieldKey" + }, + "type": { + "type": "string", + "enum": [ + "text" + ] + }, + "labels": { + "$ref": "#/$defs/FieldLabels" + }, + "requiredToPublish": { + "type": "boolean", + "description": "Every item of an entry in review or published needs a value. Absent: false." + }, + "maxLength": { + "type": "integer", + "description": "Absent: 5000.", + "minimum": 1, + "maximum": 5000 + }, + "multiline": { + "type": "boolean", + "description": "Line breaks allowed. Absent: false." + } + } + }, + { + "title": "RichTextSubField", + "type": "object", + "additionalProperties": false, + "required": [ + "key", + "type", + "labels" + ], + "properties": { + "key": { + "$ref": "#/$defs/FieldKey" + }, + "type": { + "type": "string", + "enum": [ + "richText" + ] + }, + "labels": { + "$ref": "#/$defs/FieldLabels" + }, + "requiredToPublish": { + "type": "boolean", + "description": "Every item of an entry in review or published needs a value. Absent: false." + } + } + }, + { + "title": "NumberSubField", + "type": "object", + "additionalProperties": false, + "required": [ + "key", + "type", + "labels" + ], + "properties": { + "key": { + "$ref": "#/$defs/FieldKey" + }, + "type": { + "type": "string", + "enum": [ + "number" + ] + }, + "labels": { + "$ref": "#/$defs/FieldLabels" + }, + "requiredToPublish": { + "type": "boolean", + "description": "Every item of an entry in review or published needs a value. Absent: false." + }, + "integer": { + "type": "boolean", + "description": "Whole numbers only. Absent: false." + }, + "minimum": { + "type": "number" + }, + "maximum": { + "type": "number" + } + } + }, + { + "title": "BooleanSubField", + "type": "object", + "additionalProperties": false, + "required": [ + "key", + "type", + "labels" + ], + "properties": { + "key": { + "$ref": "#/$defs/FieldKey" + }, + "type": { + "type": "string", + "enum": [ + "boolean" + ] + }, + "labels": { + "$ref": "#/$defs/FieldLabels" + } + } + }, + { + "title": "DateSubField", + "type": "object", + "additionalProperties": false, + "required": [ + "key", + "type", + "labels" + ], + "properties": { + "key": { + "$ref": "#/$defs/FieldKey" + }, + "type": { + "type": "string", + "enum": [ + "date" + ] + }, + "labels": { + "$ref": "#/$defs/FieldLabels" + }, + "requiredToPublish": { + "type": "boolean", + "description": "Every item of an entry in review or published needs a value. Absent: false." + } + } + }, + { + "title": "UrlSubField", + "type": "object", + "additionalProperties": false, + "required": [ + "key", + "type", + "labels" + ], + "properties": { + "key": { + "$ref": "#/$defs/FieldKey" + }, + "type": { + "type": "string", + "enum": [ + "url" + ] + }, + "labels": { + "$ref": "#/$defs/FieldLabels" + }, + "requiredToPublish": { + "type": "boolean", + "description": "Every item of an entry in review or published needs a value. Absent: false." + } + } + }, + { + "title": "ImageSubField", + "type": "object", + "additionalProperties": false, + "required": [ + "key", + "type", + "labels" + ], + "properties": { + "key": { + "$ref": "#/$defs/FieldKey" + }, + "type": { + "type": "string", + "enum": [ + "image" + ] + }, + "labels": { + "$ref": "#/$defs/FieldLabels" + }, + "requiredToPublish": { + "type": "boolean", + "description": "Every item of an entry in review or published needs a value. Absent: false." + } + } + }, + { + "title": "SelectSubField", + "type": "object", + "additionalProperties": false, + "required": [ + "key", + "type", + "labels", + "options" + ], + "properties": { + "key": { + "$ref": "#/$defs/FieldKey" + }, + "type": { + "type": "string", + "enum": [ + "select" + ] + }, + "labels": { + "$ref": "#/$defs/FieldLabels" + }, + "requiredToPublish": { + "type": "boolean", + "description": "Every item of an entry in review or published needs a value. Absent: false." + }, + "options": { + "type": "array", + "minItems": 1, + "maxItems": 50, + "items": { + "$ref": "#/$defs/FieldOption" + } + } + } + }, + { + "title": "ReferenceSubField", + "type": "object", + "additionalProperties": false, + "required": [ + "key", + "type", + "labels", + "collections" + ], + "properties": { + "key": { + "$ref": "#/$defs/FieldKey" + }, + "type": { + "type": "string", + "enum": [ + "reference" + ] + }, + "labels": { + "$ref": "#/$defs/FieldLabels" + }, + "requiredToPublish": { + "type": "boolean", + "description": "Every item of an entry in review or published needs a value. Absent: false." + }, + "collections": { + "type": "array", + "description": "The collections a target may belong to (checked in code).", + "minItems": 1, + "maxItems": 5, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/Slug" + } + } + } + } + ] + }, "SurfaceComponent": { "type": "object", "description": "One element of a generative UI surface (RFC-0020 §1), in a flat list: `component` names a catalog entry (a PascalCase built-in primitive such as `Stack`, or a registry block type such as `callout`), `children` refer to other ids of the same surface, and `props` follow the entry's schema, which the store checks. Field names follow A2UI v0.9.", @@ -3381,7 +3804,7 @@ export const ContentEntrySchema = { }, "fields": { "type": "object", - "description": "Collection-specific values, keyed by field. A collection with typed fields (RFC-0041, Site.collections..fields) checks each value against its definition on write; otherwise any JSON. An absent key means no value.", + "description": "Collection-specific values, keyed by field. A collection with typed fields (RFC-0041, Site.collections..fields) checks each value against its definition on write; otherwise any JSON. An absent key means no value. A reference value is a translation group id or a list of them, a repeater value a list of flat objects (RFC-0045).", "additionalProperties": true }, "tags": { diff --git a/packages/core-contracts/src/generated/site.ts b/packages/core-contracts/src/generated/site.ts index a0df6956..93fd9eed 100644 --- a/packages/core-contracts/src/generated/site.ts +++ b/packages/core-contracts/src/generated/site.ts @@ -48,7 +48,9 @@ export type FieldDefinition = | DateTimeField | UrlField | ImageField - | SelectField; + | SelectField + | ReferenceField + | RepeaterField; /** * The field's key in ContentEntry.fields: lower camelCase, a JavaScript identifier. Unique in its collection; built-in entry property names are reserved (checked in code). * @@ -56,6 +58,22 @@ export type FieldDefinition = * via the `definition` "FieldKey". */ export type FieldKey = string; +/** + * RFC-0045: one sub-field of a repeater item, discriminated by `type`. Only types whose value is a scalar: no datetime, no multiple select or reference, no repeater. No localized and no migration: the repeater's apply. + * + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "SubFieldDefinition". + */ +export type SubFieldDefinition = + | TextSubField + | RichTextSubField + | NumberSubField + | BooleanSubField + | DateSubField + | UrlSubField + | ImageSubField + | SelectSubField + | ReferenceSubField; /** * RFC-0042: where one JSON-LD property's value comes from, discriminated by `kind`. * @@ -365,8 +383,22 @@ export interface FieldMigration { id: string; fromKey?: FieldKey; fromType?: - "text" | "richText" | "number" | "boolean" | "date" | "datetime" | "url" | "image" | "select"; + | "text" + | "richText" + | "number" + | "boolean" + | "date" + | "datetime" + | "url" + | "image" + | "select" + | "reference" + | "repeater"; fromLocalized?: boolean; + /** + * The field's key in ContentEntry.fields: lower camelCase, a JavaScript identifier. Unique in its collection; built-in entry property names are reserved (checked in code). + */ + sub?: string; } /** * Cloudhal Markdown v1 (ADR-0028), at most 50 000 characters. @@ -535,365 +567,69 @@ export interface FieldOption { }; } /** - * RFC-0042: a time gate every entry derives from one of its date or datetime fields, checked at render time like Availability.until. A date ends at the start of the next day in the site's time zone. - * - * This interface was referenced by `Site`'s JSON-Schema - * via the `definition` "CollectionExpiry". - */ -export interface CollectionExpiry { - field: FieldKey; - /** - * unlist: the page stays, the entry leaves listings, feeds, the sitemap and search. hide: every public route answers 404. - */ - action: "unlist" | "hide"; - /** - * Grace after the field's instant. Absent: 0. - */ - afterMinutes?: number; -} -/** - * RFC-0042: the JSON-LD type an entry page emits in place of BlogPosting, with its properties mapped to value sources. Paths, fitting field types and required properties are a closed table in code (@cloudhal/core-ts/structured-data). - * - * This interface was referenced by `Site`'s JSON-Schema - * via the `definition` "StructuredDataPreset". + * RFC-0045: entries of other (or this) collections of the same site, by translation group id (entry_…): the id of the group's original, which every language version and edition carries in translation.group. */ -export interface StructuredDataPreset { - type: "Event" | "JobPosting" | "Product" | "Recipe"; +export interface ReferenceField { + key: FieldKey; + type: "reference"; + labels: FieldLabels; /** - * Keyed by property path, e.g. startDate or location.address.addressLocality. + * true: each language version picks its own targets; false: only the translation group's original holds them, and every version resolves them in its own language. */ - properties: { - [k: string]: StructuredDataSource | undefined; - }; -} -/** - * A typed field of this collection, converted by its type. - */ -export interface FieldValueSource { - kind: "field"; - field: FieldKey; -} -/** - * A built-in property of the entry. - */ -export interface EntryValueSource { - kind: "entry"; - property: EntryValueProperty; -} -/** - * A property of the site. - */ -export interface SiteValueSource { - kind: "site"; - property: SiteValueProperty; -} -/** - * A plain-text constant: no control characters, no markup meaning; vocabulary-checked on enum paths (in code). - */ -export interface ConstantValueSource { - kind: "constant"; - value: string; -} -/** - * Theme (a.k.a. Experience) applied by the renderer. Config is theme-defined and validated by the theme's own schema, not here. - * - * This interface was referenced by `Site`'s JSON-Schema - * via the `definition` "ThemeRef". - */ -export interface ThemeRef { - name: Slug; - version?: string; - config?: { - [k: string]: unknown | undefined; - }; -} -/** - * The site's design rules for people and agents (RFC-0021): Markdown that agents read with the theme's DESIGN.md before they build. Guidance only: it changes neither rendering nor permissions. - * - * This interface was referenced by `Site`'s JSON-Schema - * via the `definition` "Design". - */ -export interface Design { - markdown: string; -} -/** - * The site's logo and social links (RFC-0025 §1), shared by the header, the footer and SEO. - * - * This interface was referenced by `Site`'s JSON-Schema - * via the `definition` "Brand". - */ -export interface Brand { - logo?: ImageRef; - logoDark?: ImageRef; - logoHeight?: "s" | "m" | "l"; + localized: boolean; /** - * @maxItems 8 + * An entry in review or published needs at least one target. Absent: false. */ - social?: - | [] - | [SocialLink] - | [SocialLink, SocialLink] - | [SocialLink, SocialLink, SocialLink] - | [SocialLink, SocialLink, SocialLink, SocialLink] - | [SocialLink, SocialLink, SocialLink, SocialLink, SocialLink] - | [SocialLink, SocialLink, SocialLink, SocialLink, SocialLink, SocialLink] - | [SocialLink, SocialLink, SocialLink, SocialLink, SocialLink, SocialLink, SocialLink] - | [ - SocialLink, - SocialLink, - SocialLink, - SocialLink, - SocialLink, - SocialLink, - SocialLink, - SocialLink, - ]; -} -/** - * Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before. - * - * This interface was referenced by `Site`'s JSON-Schema - * via the `definition` "ImageRef". - */ -export interface ImageRef { + requiredToPublish?: boolean; + migration?: FieldMigration; /** - * Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles. + * The collections a target may belong to: declared collections of this site (checked in code). + * + * @minItems 1 + * @maxItems 5 */ - assetId?: string; - src: string; - alt: string; - width?: number; - height?: number; -} -/** - * This interface was referenced by `Site`'s JSON-Schema - * via the `definition` "SocialLink". - */ -export interface SocialLink { - network: - | "x" - | "bluesky" - | "mastodon" - | "github" - | "linkedin" - | "instagram" - | "facebook" - | "youtube" - | "tiktok" - | "threads" - | "rss" - | "email"; - href: string; -} -/** - * The site's header, footer and bars as data (RFC-0025): enums, menu keys and short texts; the theme renders them with landmarks, focus and contrast guaranteed. - * - * This interface was referenced by `Site`'s JSON-Schema - * via the `definition` "Chrome". - */ -export interface Chrome { - header?: ChromeHeader; - footer?: ChromeFooter; - announcement?: Announcement; - consent?: Consent; -} -/** - * This interface was referenced by `Site`'s JSON-Schema - * via the `definition` "ChromeHeader". - */ -export interface ChromeHeader { - layout?: "logo-left" | "logo-left-nav-center" | "centered" | "split" | "minimal"; - behavior?: "static" | "sticky" | "hide-on-scroll" | "fixed"; - shrink?: boolean; - overHero?: "solid" | "transparent"; - width?: "content" | "wide" | "full"; - surface?: "plain" | "muted" | "raised" | "inverse" | "brand"; - mobileStyle?: "dropdown" | "drawer-start" | "drawer-end" | "fullscreen"; - collapseAt?: "sm" | "md" | "lg" | "always"; + collections: + | [Slug] + | [Slug, Slug] + | [Slug, Slug, Slug] + | [Slug, Slug, Slug, Slug] + | [Slug, Slug, Slug, Slug, Slug]; /** - * Lower-case URL segment. + * The value is an ordered list of targets. Absent: false. */ - nav?: string; + multiple?: boolean; /** - * Lower-case URL segment. + * With multiple: at most this many targets. Absent: 20. */ - utility?: string; + maxItems?: number; /** - * @maxItems 3 + * Only with one target collection: conditions on its list fields that a newly chosen target must meet (no `now`; checked in code). Narrows the picker; not re-checked when the target changes later. + * + * @minItems 1 + * @maxItems 4 */ - actions?: - [] | [HeaderAction] | [HeaderAction, HeaderAction] | [HeaderAction, HeaderAction, HeaderAction]; -} -/** - * This interface was referenced by `Site`'s JSON-Schema - * via the `definition` "HeaderAction". - */ -export interface HeaderAction { - kind: "button" | "search" | "locale" | "theme-toggle" | "motion-toggle"; - label?: string; - href?: string; - variant?: "primary" | "secondary"; + where?: + | [FieldCondition] + | [FieldCondition, FieldCondition] + | [FieldCondition, FieldCondition, FieldCondition] + | [FieldCondition, FieldCondition, FieldCondition, FieldCondition]; } /** + * RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`. + * * This interface was referenced by `Site`'s JSON-Schema - * via the `definition` "ChromeFooter". + * via the `definition` "FieldCondition". */ -export interface ChromeFooter { - layout?: "simple" | "columns" | "cta-band" | "minimal"; - surface?: "plain" | "muted" | "inverse"; - tagline?: string; - /** - * @maxItems 5 - */ - columns?: - | [] - | [FooterColumn] - | [FooterColumn, FooterColumn] - | [FooterColumn, FooterColumn, FooterColumn] - | [FooterColumn, FooterColumn, FooterColumn, FooterColumn] - | [FooterColumn, FooterColumn, FooterColumn, FooterColumn, FooterColumn]; - ctaHeading?: string; - ctaLabel?: string; - ctaHref?: string; - social?: boolean; - /** - * The legal line; {year} and {site} are the only placeholders. - */ - copyright?: string; +export interface FieldCondition { /** - * Lower-case URL segment. + * A field key (site.schema.json FieldKey's pattern, repeated). */ - legal?: string; + field: string; + op: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "set" | "unset"; + value?: string; /** - * @maxItems 3 - */ - extras?: - | [] - | ["locale" | "theme-toggle" | "motion-toggle"] - | ["locale" | "theme-toggle" | "motion-toggle", "locale" | "theme-toggle" | "motion-toggle"] - | [ - "locale" | "theme-toggle" | "motion-toggle", - "locale" | "theme-toggle" | "motion-toggle", - "locale" | "theme-toggle" | "motion-toggle", - ]; - credit?: boolean; - region?: Section; -} -/** - * This interface was referenced by `Site`'s JSON-Schema - * via the `definition` "FooterColumn". - */ -export interface FooterColumn { - heading: string; - /** - * Lower-case URL segment. - */ - menu: string; -} -export interface HeroSection { - id: Id; - kind: "hero"; - heading: string; - subheading?: string; - image?: ImageRef; - cta?: Link; -} -/** - * This interface was referenced by `Site`'s JSON-Schema - * via the `definition` "Link". - */ -export interface Link { - label: string; - href: string; - external?: boolean; -} -export interface RichTextSection { - id: Id; - kind: "richText"; - format: "markdown" | "html"; - body: string; - profile?: BodyProfile; - blocks?: BodyBlocks; -} -/** - * One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed. - * - * This interface was referenced by `Site`'s JSON-Schema - * via the `definition` "BodyBlock". - */ -export interface BodyBlock { - /** - * Stable across writes that keep the block's content or its place among unchanged blocks. - */ - id: string; - /** - * SHA-256 (hex) of the block's markdown without trailing whitespace. - */ - hash: string; - by?: Actor; - /** - * The channel of that change. - */ - via?: "studio" | "mcp" | "api" | "import" | "ai-command" | "proposal"; - at?: Timestamp; -} -/** - * This interface was referenced by `Site`'s JSON-Schema - * via the `definition` "Actor". - */ -export interface Actor { - kind: ActorKind; - id: Id; - name?: string; -} -export interface ImageSection { - id: Id; - kind: "image"; - image: ImageRef; - caption?: string; -} -/** - * Renders the newest entries of a content collection (e.g. blog posts). - */ -export interface ContentListSection { - id: Id; - kind: "contentList"; - collection: Slug; - limit: number; - sort: "newest" | "oldest" | "title"; - /** - * Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'. - */ - filter?: string; - /** - * RFC-0042: conditions on the collection's list fields, ANDed. - * - * @minItems 1 - * @maxItems 4 - */ - where?: - | [FieldCondition] - | [FieldCondition, FieldCondition] - | [FieldCondition, FieldCondition, FieldCondition] - | [FieldCondition, FieldCondition, FieldCondition, FieldCondition]; - orderBy?: FieldOrder; -} -/** - * RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`. - * - * This interface was referenced by `Site`'s JSON-Schema - * via the `definition` "FieldCondition". - */ -export interface FieldCondition { - /** - * A field key (site.schema.json FieldKey's pattern, repeated). - */ - field: string; - op: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "set" | "unset"; - value?: string; - /** - * @minItems 1 - * @maxItems 20 + * @minItems 1 + * @maxItems 20 */ values?: | [string] @@ -1070,6 +806,551 @@ export interface FieldCondition { string, ]; } +/** + * RFC-0045: an ordered list of items, each a flat object of sub-fields. A repeater cannot contain a repeater. + */ +export interface RepeaterField { + key: FieldKey; + type: "repeater"; + labels: FieldLabels; + /** + * true: each language version has its own list (missing reads the original's); false: only the original holds the list. + */ + localized: boolean; + /** + * An entry in review or published needs at least one item. Absent: false. + */ + requiredToPublish?: boolean; + migration?: FieldMigration; + /** + * The sub-fields of every item, in form order. Unique keys (checked in code). + * + * @minItems 1 + * @maxItems 10 + */ + fields: + | [SubFieldDefinition] + | [SubFieldDefinition, SubFieldDefinition] + | [SubFieldDefinition, SubFieldDefinition, SubFieldDefinition] + | [SubFieldDefinition, SubFieldDefinition, SubFieldDefinition, SubFieldDefinition] + | [ + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + ] + | [ + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + ] + | [ + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + ] + | [ + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + ] + | [ + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + ] + | [ + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + SubFieldDefinition, + ]; + /** + * At most this many items. Absent: 20. + */ + maxItems?: number; + /** + * The field's key in ContentEntry.fields: lower camelCase, a JavaScript identifier. Unique in its collection; built-in entry property names are reserved (checked in code). + */ + itemLabel?: string; +} +export interface TextSubField { + key: FieldKey; + type: "text"; + labels: FieldLabels; + /** + * Every item of an entry in review or published needs a value. Absent: false. + */ + requiredToPublish?: boolean; + /** + * Absent: 5000. + */ + maxLength?: number; + /** + * Line breaks allowed. Absent: false. + */ + multiline?: boolean; +} +export interface RichTextSubField { + key: FieldKey; + type: "richText"; + labels: FieldLabels; + /** + * Every item of an entry in review or published needs a value. Absent: false. + */ + requiredToPublish?: boolean; +} +export interface NumberSubField { + key: FieldKey; + type: "number"; + labels: FieldLabels; + /** + * Every item of an entry in review or published needs a value. Absent: false. + */ + requiredToPublish?: boolean; + /** + * Whole numbers only. Absent: false. + */ + integer?: boolean; + minimum?: number; + maximum?: number; +} +export interface BooleanSubField { + key: FieldKey; + type: "boolean"; + labels: FieldLabels; +} +export interface DateSubField { + key: FieldKey; + type: "date"; + labels: FieldLabels; + /** + * Every item of an entry in review or published needs a value. Absent: false. + */ + requiredToPublish?: boolean; +} +export interface UrlSubField { + key: FieldKey; + type: "url"; + labels: FieldLabels; + /** + * Every item of an entry in review or published needs a value. Absent: false. + */ + requiredToPublish?: boolean; +} +export interface ImageSubField { + key: FieldKey; + type: "image"; + labels: FieldLabels; + /** + * Every item of an entry in review or published needs a value. Absent: false. + */ + requiredToPublish?: boolean; +} +export interface SelectSubField { + key: FieldKey; + type: "select"; + labels: FieldLabels; + /** + * Every item of an entry in review or published needs a value. Absent: false. + */ + requiredToPublish?: boolean; + /** + * @minItems 1 + * @maxItems 50 + */ + options: [FieldOption, ...FieldOption[]]; +} +export interface ReferenceSubField { + key: FieldKey; + type: "reference"; + labels: FieldLabels; + /** + * Every item of an entry in review or published needs a value. Absent: false. + */ + requiredToPublish?: boolean; + /** + * The collections a target may belong to (checked in code). + * + * @minItems 1 + * @maxItems 5 + */ + collections: + | [Slug] + | [Slug, Slug] + | [Slug, Slug, Slug] + | [Slug, Slug, Slug, Slug] + | [Slug, Slug, Slug, Slug, Slug]; +} +/** + * RFC-0042: a time gate every entry derives from one of its date or datetime fields, checked at render time like Availability.until. A date ends at the start of the next day in the site's time zone. + * + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "CollectionExpiry". + */ +export interface CollectionExpiry { + field: FieldKey; + /** + * unlist: the page stays, the entry leaves listings, feeds, the sitemap and search. hide: every public route answers 404. + */ + action: "unlist" | "hide"; + /** + * Grace after the field's instant. Absent: 0. + */ + afterMinutes?: number; +} +/** + * RFC-0042: the JSON-LD type an entry page emits in place of BlogPosting, with its properties mapped to value sources. Paths, fitting field types and required properties are a closed table in code (@cloudhal/core-ts/structured-data). + * + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "StructuredDataPreset". + */ +export interface StructuredDataPreset { + type: "Event" | "JobPosting" | "Product" | "Recipe"; + /** + * Keyed by property path, e.g. startDate or location.address.addressLocality. + */ + properties: { + [k: string]: StructuredDataSource | undefined; + }; +} +/** + * A typed field of this collection, converted by its type. + */ +export interface FieldValueSource { + kind: "field"; + field: FieldKey; +} +/** + * A built-in property of the entry. + */ +export interface EntryValueSource { + kind: "entry"; + property: EntryValueProperty; +} +/** + * A property of the site. + */ +export interface SiteValueSource { + kind: "site"; + property: SiteValueProperty; +} +/** + * A plain-text constant: no control characters, no markup meaning; vocabulary-checked on enum paths (in code). + */ +export interface ConstantValueSource { + kind: "constant"; + value: string; +} +/** + * Theme (a.k.a. Experience) applied by the renderer. Config is theme-defined and validated by the theme's own schema, not here. + * + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "ThemeRef". + */ +export interface ThemeRef { + name: Slug; + version?: string; + config?: { + [k: string]: unknown | undefined; + }; +} +/** + * The site's design rules for people and agents (RFC-0021): Markdown that agents read with the theme's DESIGN.md before they build. Guidance only: it changes neither rendering nor permissions. + * + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "Design". + */ +export interface Design { + markdown: string; +} +/** + * The site's logo and social links (RFC-0025 §1), shared by the header, the footer and SEO. + * + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "Brand". + */ +export interface Brand { + logo?: ImageRef; + logoDark?: ImageRef; + logoHeight?: "s" | "m" | "l"; + /** + * @maxItems 8 + */ + social?: + | [] + | [SocialLink] + | [SocialLink, SocialLink] + | [SocialLink, SocialLink, SocialLink] + | [SocialLink, SocialLink, SocialLink, SocialLink] + | [SocialLink, SocialLink, SocialLink, SocialLink, SocialLink] + | [SocialLink, SocialLink, SocialLink, SocialLink, SocialLink, SocialLink] + | [SocialLink, SocialLink, SocialLink, SocialLink, SocialLink, SocialLink, SocialLink] + | [ + SocialLink, + SocialLink, + SocialLink, + SocialLink, + SocialLink, + SocialLink, + SocialLink, + SocialLink, + ]; +} +/** + * Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before. + * + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "ImageRef". + */ +export interface ImageRef { + /** + * Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles. + */ + assetId?: string; + src: string; + alt: string; + width?: number; + height?: number; +} +/** + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "SocialLink". + */ +export interface SocialLink { + network: + | "x" + | "bluesky" + | "mastodon" + | "github" + | "linkedin" + | "instagram" + | "facebook" + | "youtube" + | "tiktok" + | "threads" + | "rss" + | "email"; + href: string; +} +/** + * The site's header, footer and bars as data (RFC-0025): enums, menu keys and short texts; the theme renders them with landmarks, focus and contrast guaranteed. + * + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "Chrome". + */ +export interface Chrome { + header?: ChromeHeader; + footer?: ChromeFooter; + announcement?: Announcement; + consent?: Consent; +} +/** + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "ChromeHeader". + */ +export interface ChromeHeader { + layout?: "logo-left" | "logo-left-nav-center" | "centered" | "split" | "minimal"; + behavior?: "static" | "sticky" | "hide-on-scroll" | "fixed"; + shrink?: boolean; + overHero?: "solid" | "transparent"; + width?: "content" | "wide" | "full"; + surface?: "plain" | "muted" | "raised" | "inverse" | "brand"; + mobileStyle?: "dropdown" | "drawer-start" | "drawer-end" | "fullscreen"; + collapseAt?: "sm" | "md" | "lg" | "always"; + /** + * Lower-case URL segment. + */ + nav?: string; + /** + * Lower-case URL segment. + */ + utility?: string; + /** + * @maxItems 3 + */ + actions?: + [] | [HeaderAction] | [HeaderAction, HeaderAction] | [HeaderAction, HeaderAction, HeaderAction]; +} +/** + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "HeaderAction". + */ +export interface HeaderAction { + kind: "button" | "search" | "locale" | "theme-toggle" | "motion-toggle"; + label?: string; + href?: string; + variant?: "primary" | "secondary"; +} +/** + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "ChromeFooter". + */ +export interface ChromeFooter { + layout?: "simple" | "columns" | "cta-band" | "minimal"; + surface?: "plain" | "muted" | "inverse"; + tagline?: string; + /** + * @maxItems 5 + */ + columns?: + | [] + | [FooterColumn] + | [FooterColumn, FooterColumn] + | [FooterColumn, FooterColumn, FooterColumn] + | [FooterColumn, FooterColumn, FooterColumn, FooterColumn] + | [FooterColumn, FooterColumn, FooterColumn, FooterColumn, FooterColumn]; + ctaHeading?: string; + ctaLabel?: string; + ctaHref?: string; + social?: boolean; + /** + * The legal line; {year} and {site} are the only placeholders. + */ + copyright?: string; + /** + * Lower-case URL segment. + */ + legal?: string; + /** + * @maxItems 3 + */ + extras?: + | [] + | ["locale" | "theme-toggle" | "motion-toggle"] + | ["locale" | "theme-toggle" | "motion-toggle", "locale" | "theme-toggle" | "motion-toggle"] + | [ + "locale" | "theme-toggle" | "motion-toggle", + "locale" | "theme-toggle" | "motion-toggle", + "locale" | "theme-toggle" | "motion-toggle", + ]; + credit?: boolean; + region?: Section; +} +/** + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "FooterColumn". + */ +export interface FooterColumn { + heading: string; + /** + * Lower-case URL segment. + */ + menu: string; +} +export interface HeroSection { + id: Id; + kind: "hero"; + heading: string; + subheading?: string; + image?: ImageRef; + cta?: Link; +} +/** + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "Link". + */ +export interface Link { + label: string; + href: string; + external?: boolean; +} +export interface RichTextSection { + id: Id; + kind: "richText"; + format: "markdown" | "html"; + body: string; + profile?: BodyProfile; + blocks?: BodyBlocks; +} +/** + * One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed. + * + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "BodyBlock". + */ +export interface BodyBlock { + /** + * Stable across writes that keep the block's content or its place among unchanged blocks. + */ + id: string; + /** + * SHA-256 (hex) of the block's markdown without trailing whitespace. + */ + hash: string; + by?: Actor; + /** + * The channel of that change. + */ + via?: "studio" | "mcp" | "api" | "import" | "ai-command" | "proposal"; + at?: Timestamp; +} +/** + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "Actor". + */ +export interface Actor { + kind: ActorKind; + id: Id; + name?: string; +} +export interface ImageSection { + id: Id; + kind: "image"; + image: ImageRef; + caption?: string; +} +/** + * Renders the newest entries of a content collection (e.g. blog posts). + */ +export interface ContentListSection { + id: Id; + kind: "contentList"; + collection: Slug; + limit: number; + sort: "newest" | "oldest" | "title"; + /** + * Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'. + */ + filter?: string; + /** + * RFC-0042: conditions on the collection's list fields, ANDed. + * + * @minItems 1 + * @maxItems 4 + */ + where?: + | [FieldCondition] + | [FieldCondition, FieldCondition] + | [FieldCondition, FieldCondition, FieldCondition] + | [FieldCondition, FieldCondition, FieldCondition, FieldCondition]; + orderBy?: FieldOrder; +} /** * RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last. * diff --git a/packages/core-contracts/src/generated/validators.js b/packages/core-contracts/src/generated/validators.js index 0d017158..346dfa2a 100644 --- a/packages/core-contracts/src/generated/validators.js +++ b/packages/core-contracts/src/generated/validators.js @@ -530,20 +530,20 @@ return errors === 0; validate20.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; export const validateSite = validate21; -const schema41 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/site.schema.json","title":"Site","description":"Root node of a Site Graph: one deployable website with its locales, domains, theme and page index. Pages and content entries are separate nodes referenced by id so they can be versioned and locked independently.","type":"object","additionalProperties":false,"required":["schemaVersion","id","projectId","name","slug","defaultLocale","locales","domains","pageIds","theme","rev","createdAt","updatedAt"],"properties":{"schemaVersion":{"$ref":"#/$defs/SchemaVersion"},"id":{"$ref":"#/$defs/Id"},"projectId":{"$ref":"#/$defs/Id"},"name":{"type":"string","minLength":1,"maxLength":200},"slug":{"$ref":"#/$defs/Slug"},"defaultLocale":{"$ref":"#/$defs/Locale"},"locales":{"type":"array","items":{"$ref":"#/$defs/Locale"},"minItems":1,"uniqueItems":true},"domains":{"type":"array","description":"Hostnames serving this site. Exactly one should be primary: the address canonical links, feeds and previews use. Sites run on their own domains or their Cloudflare account's workers.dev address, never on a Cloudhal domain (ADR-0037).","items":{"$ref":"#/$defs/Domain"}},"pageIds":{"type":"array","items":{"$ref":"#/$defs/Id"},"uniqueItems":true},"homePageId":{"$ref":"#/$defs/Id"},"localization":{"$ref":"#/$defs/Localization"},"collections":{"type":"object","description":"RFC-0038, RFC-0040: the site's declared collections and their settings, keyed by collection (ContentEntry.collection). A key here declares the collection, even with no settings ({}). A collection with entries but no key is inferred and uses the defaults.","propertyNames":{"$ref":"#/$defs/Slug"},"maxProperties":50,"additionalProperties":{"$ref":"#/$defs/CollectionSettings"}},"theme":{"$ref":"#/$defs/ThemeRef"},"design":{"$ref":"#/$defs/Design"},"brand":{"$ref":"#/$defs/Brand"},"chrome":{"$ref":"#/$defs/Chrome"},"timeZone":{"$ref":"#/$defs/TimeZone","description":"The site's time zone (RFC-0008): Studio's calendar, schedules entered as wall-clock times and recurring automations use it. Absent means UTC."},"publishedSnapshotId":{"$ref":"#/$defs/Id","description":"Snapshot currently serving production. Rollback = pointing this at an older snapshot."},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"metadata":{"$ref":"#/$defs/Metadata"}},"$defs":{"Actor":{"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}},"ActorKind":{"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]},"Announcement":{"type":"object","additionalProperties":false,"required":["id","text"],"properties":{"id":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,40}$","description":"Change it when the text changes, so readers who dismissed the old one see the new one."},"text":{"type":"string","minLength":1,"maxLength":200},"label":{"type":"string","minLength":1,"maxLength":40},"href":{"type":"string","minLength":1,"maxLength":2048},"tone":{"type":"string","enum":["info","brand","warning"]},"dismissible":{"type":"boolean"}}},"BodyBlock":{"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}},"BodyBlocks":{"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}},"BodyProfile":{"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."},"Brand":{"type":"object","description":"The site's logo and social links (RFC-0025 §1), shared by the header, the footer and SEO.","additionalProperties":false,"properties":{"logo":{"$ref":"#/$defs/ImageRef"},"logoDark":{"$ref":"#/$defs/ImageRef"},"logoHeight":{"type":"string","enum":["s","m","l"]},"social":{"type":"array","maxItems":8,"items":{"$ref":"#/$defs/SocialLink"}}}},"Chrome":{"type":"object","description":"The site's header, footer and bars as data (RFC-0025): enums, menu keys and short texts; the theme renders them with landmarks, focus and contrast guaranteed.","additionalProperties":false,"properties":{"header":{"$ref":"#/$defs/ChromeHeader"},"footer":{"$ref":"#/$defs/ChromeFooter"},"announcement":{"$ref":"#/$defs/Announcement"},"consent":{"$ref":"#/$defs/Consent"}}},"ChromeFooter":{"type":"object","additionalProperties":false,"properties":{"layout":{"type":"string","enum":["simple","columns","cta-band","minimal"]},"surface":{"type":"string","enum":["plain","muted","inverse"]},"tagline":{"type":"string","minLength":1,"maxLength":200},"columns":{"type":"array","maxItems":5,"items":{"$ref":"#/$defs/FooterColumn"}},"ctaHeading":{"type":"string","minLength":1,"maxLength":120},"ctaLabel":{"type":"string","minLength":1,"maxLength":40},"ctaHref":{"type":"string","minLength":1,"maxLength":2048},"social":{"type":"boolean"},"copyright":{"type":"string","minLength":1,"maxLength":200,"description":"The legal line; {year} and {site} are the only placeholders."},"legal":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"extras":{"type":"array","maxItems":3,"uniqueItems":true,"items":{"type":"string","enum":["locale","theme-toggle","motion-toggle"]}},"credit":{"type":"boolean"},"region":{"$ref":"#/$defs/Section","description":"RFC-0025 §2: one optional surface (kind `surface`) above the footer's columns, built and checked like any surface; the store refuses other section kinds."}}},"ChromeHeader":{"type":"object","additionalProperties":false,"properties":{"layout":{"type":"string","enum":["logo-left","logo-left-nav-center","centered","split","minimal"]},"behavior":{"type":"string","enum":["static","sticky","hide-on-scroll","fixed"]},"shrink":{"type":"boolean"},"overHero":{"type":"string","enum":["solid","transparent"]},"width":{"type":"string","enum":["content","wide","full"]},"surface":{"type":"string","enum":["plain","muted","raised","inverse","brand"]},"mobileStyle":{"type":"string","enum":["dropdown","drawer-start","drawer-end","fullscreen"]},"collapseAt":{"type":"string","enum":["sm","md","lg","always"]},"nav":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"utility":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"actions":{"type":"array","maxItems":3,"items":{"$ref":"#/$defs/HeaderAction"}}}},"CollectionExpiry":{"type":"object","description":"RFC-0042: a time gate every entry derives from one of its date or datetime fields, checked at render time like Availability.until. A date ends at the start of the next day in the site's time zone.","additionalProperties":false,"required":["field","action"],"properties":{"field":{"$ref":"#/$defs/FieldKey"},"action":{"type":"string","description":"unlist: the page stays, the entry leaves listings, feeds, the sitemap and search. hide: every public route answers 404.","enum":["unlist","hide"]},"afterMinutes":{"type":"integer","description":"Grace after the field's instant. Absent: 0.","minimum":0,"maximum":10080}}},"CollectionLabel":{"type":"object","additionalProperties":false,"required":["singular","plural"],"properties":{"singular":{"type":"string","minLength":1,"maxLength":100},"plural":{"type":"string","minLength":1,"maxLength":100},"description":{"type":"string","maxLength":1000}}},"CollectionSettings":{"type":"object","additionalProperties":false,"properties":{"permalink":{"type":"string","description":"RFC-0038: where the collection's entries live, as a template with a closed token set. Version 1: one or more fixed lower-case path segments followed by /{slug}, e.g. /blog/{slug}. Absent means //{slug}.","pattern":"^(/[a-z0-9]+(-[a-z0-9]+)*)+/\\{slug\\}$"},"labels":{"type":"object","description":"RFC-0040: display names per locale (Studio, agents, themes). Absent or missing a locale: the default locale's, then the key.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"$ref":"#/$defs/CollectionLabel"}},"icon":{"type":"string","description":"RFC-0040: a name from Studio's closed icon set (e.g. calendar, briefcase, users). Unknown names show the default icon. Never a URL or markup.","pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","maxLength":40},"feed":{"type":"boolean","description":"RFC-0040: entries appear in the site's RSS feeds. Absent: true."},"sitemap":{"type":"boolean","description":"RFC-0040: published entries appear in sitemap.xml. Absent: true."},"sort":{"type":"string","description":"RFC-0040: default order where none is given (Studio's list, entry queries without sort, MCP list_entries). Absent: newest.","enum":["newest","oldest","title"]},"fields":{"type":"array","description":"RFC-0041: the collection's typed custom fields, in form order. Values live in ContentEntry.fields under each key. At most 200 per site and unique keys (checked in code).","minItems":1,"maxItems":30,"items":{"$ref":"#/$defs/FieldDefinition"}},"listFields":{"type":"array","description":"RFC-0042: fields that listings, queries, Studio columns and MCP may filter and sort by, in column order. Keys of this collection's fields (checked in code).","minItems":1,"maxItems":4,"uniqueItems":true,"items":{"$ref":"#/$defs/FieldKey"}},"expiry":{"$ref":"#/$defs/CollectionExpiry"},"structuredData":{"$ref":"#/$defs/StructuredDataPreset"}}},"Consent":{"type":"object","additionalProperties":false,"required":["mode"],"properties":{"mode":{"type":"string","enum":["none","notice","choices"]},"policy":{"$ref":"#/$defs/Path"}}},"Design":{"type":"object","description":"The site's design rules for people and agents (RFC-0021): Markdown that agents read with the theme's DESIGN.md before they build. Guidance only: it changes neither rendering nor permissions.","additionalProperties":false,"required":["markdown"],"properties":{"markdown":{"type":"string","minLength":1,"maxLength":24000}}},"Domain":{"type":"object","additionalProperties":false,"required":["hostname","primary"],"properties":{"hostname":{"type":"string","pattern":"^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$","maxLength":253},"primary":{"type":"boolean"}}},"EntryValueProperty":{"type":"string","description":"RFC-0042: entry properties a structured-data property may read.","enum":["title","summary","body","coverImage","url","publishedAt","updatedAt"]},"FieldCondition":{"type":"object","description":"RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`.","additionalProperties":false,"required":["field","op"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","in","set","unset"]},"value":{"type":"string","minLength":1,"maxLength":100},"values":{"type":"array","minItems":1,"maxItems":20,"uniqueItems":true,"items":{"type":"string","minLength":1,"maxLength":100}}}},"FieldDefinition":{"description":"RFC-0041: one typed custom field of a collection, discriminated by `type`.","oneOf":[{"title":"TextField","type":"object","description":"Plain text: a string of at most maxLength characters.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["text"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"maxLength":{"type":"integer","description":"Absent: 5000.","minimum":1,"maximum":5000},"multiline":{"type":"boolean","description":"Line breaks allowed. Absent: false."},"searchable":{"type":"boolean","description":"Its words go into site search. Absent: false."}}},{"title":"RichTextField","type":"object","description":"Cloudhal Markdown v1 (ADR-0028), at most 50 000 characters.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["richText"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"searchable":{"type":"boolean","description":"Its words go into site search. Absent: false."}}},{"title":"NumberField","type":"object","description":"A finite number.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["number"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"integer":{"type":"boolean","description":"Whole numbers only. Absent: false."},"minimum":{"type":"number"},"maximum":{"type":"number"}}},{"title":"BooleanField","type":"object","description":"true or false; absent reads as false, so it cannot be required to publish.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["boolean"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"DateField","type":"object","description":"A calendar day without a zone (YYYY-MM-DD).","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["date"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"DateTimeField","type":"object","description":"An instant with the zone it was entered in: {at, timeZone} (RFC-0008 §9.1).","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["datetime"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"UrlField","type":"object","description":"A link: https, http, mailto, tel, a site-relative path or a #fragment; at most 2048 characters.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["url"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"ImageField","type":"object","description":"An image Asset of the same site, by id (asset_…).","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["image"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"SelectField","type":"object","description":"One of a closed list of options, or several when multiple.","additionalProperties":false,"required":["key","type","labels","localized","options"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["select"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"options":{"type":"array","minItems":1,"maxItems":50,"items":{"$ref":"#/$defs/FieldOption"}},"multiple":{"type":"boolean","description":"The value is a list of option values. Absent: false."}}}]},"FieldKey":{"type":"string","description":"The field's key in ContentEntry.fields: lower camelCase, a JavaScript identifier. Unique in its collection; built-in entry property names are reserved (checked in code).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"FieldLabel":{"type":"object","additionalProperties":false,"required":["label"],"properties":{"label":{"type":"string","minLength":1,"maxLength":100},"help":{"type":"string","maxLength":300}}},"FieldLabels":{"type":"object","description":"The field's label and help per locale. Missing a locale: the default locale's, then the key.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"$ref":"#/$defs/FieldLabel"}},"FieldMigration":{"type":"object","description":"RFC-0041 §6: present only while a field operation rewrites entries.","additionalProperties":false,"required":["id"],"properties":{"id":{"$ref":"#/$defs/Id","description":"The operation's \"before\" snapshot."},"fromKey":{"$ref":"#/$defs/FieldKey"},"fromType":{"type":"string","enum":["text","richText","number","boolean","date","datetime","url","image","select"]},"fromLocalized":{"type":"boolean"}}},"FieldOption":{"type":"object","description":"One option of a select field. Values are unique in the field (checked in code).","additionalProperties":false,"required":["value","labels"],"properties":{"value":{"$ref":"#/$defs/Slug","description":"Stored in entries, never shown."},"labels":{"type":"object","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"type":"string","minLength":1,"maxLength":100}}}},"FieldOrder":{"type":"object","description":"RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last.","additionalProperties":false,"required":["field","direction"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"direction":{"type":"string","enum":["asc","desc"]}}},"FooterColumn":{"type":"object","additionalProperties":false,"required":["heading","menu"],"properties":{"heading":{"type":"string","minLength":1,"maxLength":60},"menu":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."}}},"HeaderAction":{"type":"object","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["button","search","locale","theme-toggle","motion-toggle"]},"label":{"type":"string","minLength":1,"maxLength":40},"href":{"type":"string","minLength":1,"maxLength":2048},"variant":{"type":"string","enum":["primary","secondary"]}}},"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"ImageRef":{"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}},"Link":{"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"Localization":{"type":"object","description":"How the site serves its locales (RFC-0033 §2). Absent fields mean: prefix-except-default routing, no fallback, no required review, the machine-translation notice until reviewed.","additionalProperties":false,"properties":{"routing":{"type":"string","description":"prefix-except-default: the default locale at the root, the others under //. prefix-all: every locale prefixed, / is a language chooser (x-default).","enum":["prefix-except-default","prefix-all"]},"domains":{"type":"object","description":"Optional hostname per locale; that locale is then served at the host's root.","propertyNames":{"$ref":"#/$defs/Locale"},"additionalProperties":{"type":"string","pattern":"^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$","maxLength":253}},"fallback":{"type":"string","description":"none: a missing translation is not found. default-with-notice: the default locale's page is served with a notice and noindex.","enum":["none","default-with-notice"]},"review":{"type":"object","description":"Collections whose machine translations need a person's review before they publish ('pages' stands for pages).","additionalProperties":false,"properties":{"required":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"uniqueItems":true}}},"label":{"type":"string","description":"When readers see the machine-translation notice: unreviewed (until a person reviews) or always.","enum":["unreviewed","always"]}}},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"Path":{"type":"string","description":"Absolute URL path of a route the site owns: starts with '/', lower-case slug segments, no trailing slash except root.","pattern":"^/(?:[a-z0-9]+(?:-[a-z0-9]+)*(?:/[a-z0-9]+(?:-[a-z0-9]+)*)*)?$","maxLength":1024},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Section":{"title":"Section","description":"A block inside a page, discriminated by `kind`. Variants are written inline (not via $ref) and `kind` is a single-value string enum so that TypeScript gets a literal union and Rust gets a serde internally-tagged enum.","oneOf":[{"type":"object","title":"HeroSection","additionalProperties":false,"required":["id","kind","heading"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["hero"]},"heading":{"type":"string","minLength":1,"maxLength":300},"subheading":{"type":"string","maxLength":1000},"image":{"$ref":"#/$defs/ImageRef"},"cta":{"$ref":"#/$defs/Link"}}},{"type":"object","title":"RichTextSection","additionalProperties":false,"required":["id","kind","format","body"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["richText"]},"format":{"type":"string","enum":["markdown","html"]},"body":{"type":"string","maxLength":200000},"profile":{"$ref":"#/$defs/BodyProfile"},"blocks":{"$ref":"#/$defs/BodyBlocks"}}},{"type":"object","title":"ImageSection","additionalProperties":false,"required":["id","kind","image"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["image"]},"image":{"$ref":"#/$defs/ImageRef"},"caption":{"type":"string","maxLength":1000}}},{"type":"object","title":"ContentListSection","description":"Renders the newest entries of a content collection (e.g. blog posts).","additionalProperties":false,"required":["id","kind","collection","limit","sort"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["contentList"]},"collection":{"$ref":"#/$defs/Slug"},"limit":{"type":"integer","minimum":1,"maximum":100},"sort":{"type":"string","enum":["newest","oldest","title"]},"filter":{"type":"string","description":"Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'.","minLength":1,"maxLength":512},"where":{"type":"array","description":"RFC-0042: conditions on the collection's list fields, ANDed.","minItems":1,"maxItems":4,"items":{"$ref":"#/$defs/FieldCondition"}},"orderBy":{"$ref":"#/$defs/FieldOrder"}}},{"type":"object","title":"ComponentSection","description":"A typed block from the block registry (RFC-0015): `type` names a registered block type (one global namespace of English names), `props` follow that type's props schema, which the store validates.","additionalProperties":false,"required":["id","kind","type","props"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["component"]},"type":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,40}$"},"props":{"type":"object","description":"Validated against the type's props schema by the store, not by this contract.","additionalProperties":true}}},{"type":"object","title":"SurfaceSection","description":"A generative UI surface (RFC-0020): a flat list of catalog components with a `root`, plus a data model their props may bind to. Written and streamed by agents as data, never code; the store checks the tree, the catalog and the limits.","additionalProperties":false,"required":["id","kind","catalog","components"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["surface"]},"catalog":{"type":"string","description":"The catalog the surface was written for: `@` (RFC-0018 manifest).","pattern":"^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"},"components":{"type":"array","minItems":1,"maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponent"}},"data":{"type":"object","description":"The surface's data model: JSON its components bind to with `{path}` (at most 64 KB serialized; the store enforces the size).","additionalProperties":true}}}]},"SiteValueProperty":{"type":"string","description":"RFC-0042: site properties a structured-data property may read.","enum":["name","url","logo"]},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"SocialLink":{"type":"object","additionalProperties":false,"required":["network","href"],"properties":{"network":{"type":"string","enum":["x","bluesky","mastodon","github","linkedin","instagram","facebook","youtube","tiktok","threads","rss","email"]},"href":{"type":"string","minLength":1,"maxLength":2048}}},"StructuredDataPreset":{"type":"object","description":"RFC-0042: the JSON-LD type an entry page emits in place of BlogPosting, with its properties mapped to value sources. Paths, fitting field types and required properties are a closed table in code (@cloudhal/core-ts/structured-data).","additionalProperties":false,"required":["type","properties"],"properties":{"type":{"type":"string","enum":["Event","JobPosting","Product","Recipe"]},"properties":{"type":"object","description":"Keyed by property path, e.g. startDate or location.address.addressLocality.","propertyNames":{"type":"string","pattern":"^[a-z][A-Za-z]{0,39}(\\.[a-z][A-Za-z]{0,39}){0,2}$"},"minProperties":1,"maxProperties":40,"additionalProperties":{"$ref":"#/$defs/StructuredDataSource"}}}},"StructuredDataSource":{"description":"RFC-0042: where one JSON-LD property's value comes from, discriminated by `kind`.","oneOf":[{"title":"FieldValueSource","type":"object","description":"A typed field of this collection, converted by its type.","additionalProperties":false,"required":["kind","field"],"properties":{"kind":{"type":"string","enum":["field"]},"field":{"$ref":"#/$defs/FieldKey"}}},{"title":"EntryValueSource","type":"object","description":"A built-in property of the entry.","additionalProperties":false,"required":["kind","property"],"properties":{"kind":{"type":"string","enum":["entry"]},"property":{"$ref":"#/$defs/EntryValueProperty"}}},{"title":"SiteValueSource","type":"object","description":"A property of the site.","additionalProperties":false,"required":["kind","property"],"properties":{"kind":{"type":"string","enum":["site"]},"property":{"$ref":"#/$defs/SiteValueProperty"}}},{"title":"ConstantValueSource","type":"object","description":"A plain-text constant: no control characters, no markup meaning; vocabulary-checked on enum paths (in code).","additionalProperties":false,"required":["kind","value"],"properties":{"kind":{"type":"string","enum":["constant"]},"value":{"type":"string","minLength":1,"maxLength":200,"pattern":"^[^\\x00-\\x1F\\x7F]+$"}}}]},"SurfaceComponent":{"type":"object","description":"One element of a generative UI surface (RFC-0020 §1), in a flat list: `component` names a catalog entry (a PascalCase built-in primitive such as `Stack`, or a registry block type such as `callout`), `children` refer to other ids of the same surface, and `props` follow the entry's schema, which the store checks. Field names follow A2UI v0.9.","additionalProperties":false,"required":["id","component"],"properties":{"id":{"$ref":"#/$defs/SurfaceComponentId"},"component":{"type":"string","pattern":"^(?:[A-Z][A-Za-z0-9]{1,40}|[a-z][a-z0-9-]{1,40})$"},"children":{"type":"array","maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponentId"}},"props":{"type":"object","description":"Literal values, `{path}` bindings into the surface's data, or `{call}` named functions; validated by the store against the catalog, not by this contract.","additionalProperties":true},"visible":{"type":"object","description":"A visibility condition over the surface's data (`eq`, `ne`, `gt`, `lt`, `and`, `or`, `not`); validated by the store.","additionalProperties":true},"motion":{"type":"object","description":"Motion presets for this component (RFC-0024 §2): `reveal`, `hover`, `speed`, `delay`, `stagger`, `replay` and later `scroll`, `loop`; enums and clamped numbers only, validated by the store against the catalog. The renderer turns them into CSS; agents never write CSS.","additionalProperties":true}}},"SurfaceComponentId":{"type":"string","description":"A component's id, unique within its surface; `root` is the tree's root (RFC-0020 §1).","pattern":"^[a-z][a-z0-9-]{0,40}$"},"ThemeRef":{"type":"object","description":"Theme (a.k.a. Experience) applied by the renderer. Config is theme-defined and validated by the theme's own schema, not here.","additionalProperties":false,"required":["name"],"properties":{"name":{"$ref":"#/$defs/Slug"},"version":{"type":"string","maxLength":64},"config":{"type":"object","additionalProperties":true}}},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"},"TimeZone":{"type":"string","description":"IANA time zone name from the tz database (RFC-0008), e.g. Europe/Istanbul, America/Argentina/Buenos_Aires, Etc/GMT+3 or UTC. Wall-clock times a person enters are kept with their zone, so the instant is recomputed when governments change the rules. The pattern bounds the shape only; writers also check the name against the runtime's Intl zone list.","pattern":"^(?:UTC|[A-Z][A-Za-z_-]+(?:/[A-Za-z0-9_+-]+){1,2})$","maxLength":64}}}; +const schema41 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/site.schema.json","title":"Site","description":"Root node of a Site Graph: one deployable website with its locales, domains, theme and page index. Pages and content entries are separate nodes referenced by id so they can be versioned and locked independently.","type":"object","additionalProperties":false,"required":["schemaVersion","id","projectId","name","slug","defaultLocale","locales","domains","pageIds","theme","rev","createdAt","updatedAt"],"properties":{"schemaVersion":{"$ref":"#/$defs/SchemaVersion"},"id":{"$ref":"#/$defs/Id"},"projectId":{"$ref":"#/$defs/Id"},"name":{"type":"string","minLength":1,"maxLength":200},"slug":{"$ref":"#/$defs/Slug"},"defaultLocale":{"$ref":"#/$defs/Locale"},"locales":{"type":"array","items":{"$ref":"#/$defs/Locale"},"minItems":1,"uniqueItems":true},"domains":{"type":"array","description":"Hostnames serving this site. Exactly one should be primary: the address canonical links, feeds and previews use. Sites run on their own domains or their Cloudflare account's workers.dev address, never on a Cloudhal domain (ADR-0037).","items":{"$ref":"#/$defs/Domain"}},"pageIds":{"type":"array","items":{"$ref":"#/$defs/Id"},"uniqueItems":true},"homePageId":{"$ref":"#/$defs/Id"},"localization":{"$ref":"#/$defs/Localization"},"collections":{"type":"object","description":"RFC-0038, RFC-0040: the site's declared collections and their settings, keyed by collection (ContentEntry.collection). A key here declares the collection, even with no settings ({}). A collection with entries but no key is inferred and uses the defaults.","propertyNames":{"$ref":"#/$defs/Slug"},"maxProperties":50,"additionalProperties":{"$ref":"#/$defs/CollectionSettings"}},"theme":{"$ref":"#/$defs/ThemeRef"},"design":{"$ref":"#/$defs/Design"},"brand":{"$ref":"#/$defs/Brand"},"chrome":{"$ref":"#/$defs/Chrome"},"timeZone":{"$ref":"#/$defs/TimeZone","description":"The site's time zone (RFC-0008): Studio's calendar, schedules entered as wall-clock times and recurring automations use it. Absent means UTC."},"publishedSnapshotId":{"$ref":"#/$defs/Id","description":"Snapshot currently serving production. Rollback = pointing this at an older snapshot."},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"metadata":{"$ref":"#/$defs/Metadata"}},"$defs":{"Actor":{"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}},"ActorKind":{"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]},"Announcement":{"type":"object","additionalProperties":false,"required":["id","text"],"properties":{"id":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,40}$","description":"Change it when the text changes, so readers who dismissed the old one see the new one."},"text":{"type":"string","minLength":1,"maxLength":200},"label":{"type":"string","minLength":1,"maxLength":40},"href":{"type":"string","minLength":1,"maxLength":2048},"tone":{"type":"string","enum":["info","brand","warning"]},"dismissible":{"type":"boolean"}}},"BodyBlock":{"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}},"BodyBlocks":{"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}},"BodyProfile":{"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."},"Brand":{"type":"object","description":"The site's logo and social links (RFC-0025 §1), shared by the header, the footer and SEO.","additionalProperties":false,"properties":{"logo":{"$ref":"#/$defs/ImageRef"},"logoDark":{"$ref":"#/$defs/ImageRef"},"logoHeight":{"type":"string","enum":["s","m","l"]},"social":{"type":"array","maxItems":8,"items":{"$ref":"#/$defs/SocialLink"}}}},"Chrome":{"type":"object","description":"The site's header, footer and bars as data (RFC-0025): enums, menu keys and short texts; the theme renders them with landmarks, focus and contrast guaranteed.","additionalProperties":false,"properties":{"header":{"$ref":"#/$defs/ChromeHeader"},"footer":{"$ref":"#/$defs/ChromeFooter"},"announcement":{"$ref":"#/$defs/Announcement"},"consent":{"$ref":"#/$defs/Consent"}}},"ChromeFooter":{"type":"object","additionalProperties":false,"properties":{"layout":{"type":"string","enum":["simple","columns","cta-band","minimal"]},"surface":{"type":"string","enum":["plain","muted","inverse"]},"tagline":{"type":"string","minLength":1,"maxLength":200},"columns":{"type":"array","maxItems":5,"items":{"$ref":"#/$defs/FooterColumn"}},"ctaHeading":{"type":"string","minLength":1,"maxLength":120},"ctaLabel":{"type":"string","minLength":1,"maxLength":40},"ctaHref":{"type":"string","minLength":1,"maxLength":2048},"social":{"type":"boolean"},"copyright":{"type":"string","minLength":1,"maxLength":200,"description":"The legal line; {year} and {site} are the only placeholders."},"legal":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"extras":{"type":"array","maxItems":3,"uniqueItems":true,"items":{"type":"string","enum":["locale","theme-toggle","motion-toggle"]}},"credit":{"type":"boolean"},"region":{"$ref":"#/$defs/Section","description":"RFC-0025 §2: one optional surface (kind `surface`) above the footer's columns, built and checked like any surface; the store refuses other section kinds."}}},"ChromeHeader":{"type":"object","additionalProperties":false,"properties":{"layout":{"type":"string","enum":["logo-left","logo-left-nav-center","centered","split","minimal"]},"behavior":{"type":"string","enum":["static","sticky","hide-on-scroll","fixed"]},"shrink":{"type":"boolean"},"overHero":{"type":"string","enum":["solid","transparent"]},"width":{"type":"string","enum":["content","wide","full"]},"surface":{"type":"string","enum":["plain","muted","raised","inverse","brand"]},"mobileStyle":{"type":"string","enum":["dropdown","drawer-start","drawer-end","fullscreen"]},"collapseAt":{"type":"string","enum":["sm","md","lg","always"]},"nav":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"utility":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"actions":{"type":"array","maxItems":3,"items":{"$ref":"#/$defs/HeaderAction"}}}},"CollectionExpiry":{"type":"object","description":"RFC-0042: a time gate every entry derives from one of its date or datetime fields, checked at render time like Availability.until. A date ends at the start of the next day in the site's time zone.","additionalProperties":false,"required":["field","action"],"properties":{"field":{"$ref":"#/$defs/FieldKey"},"action":{"type":"string","description":"unlist: the page stays, the entry leaves listings, feeds, the sitemap and search. hide: every public route answers 404.","enum":["unlist","hide"]},"afterMinutes":{"type":"integer","description":"Grace after the field's instant. Absent: 0.","minimum":0,"maximum":10080}}},"CollectionLabel":{"type":"object","additionalProperties":false,"required":["singular","plural"],"properties":{"singular":{"type":"string","minLength":1,"maxLength":100},"plural":{"type":"string","minLength":1,"maxLength":100},"description":{"type":"string","maxLength":1000}}},"CollectionSettings":{"type":"object","additionalProperties":false,"properties":{"permalink":{"type":"string","description":"RFC-0038: where the collection's entries live, as a template with a closed token set. Version 1: one or more fixed lower-case path segments followed by /{slug}, e.g. /blog/{slug}. Absent means //{slug}.","pattern":"^(/[a-z0-9]+(-[a-z0-9]+)*)+/\\{slug\\}$"},"labels":{"type":"object","description":"RFC-0040: display names per locale (Studio, agents, themes). Absent or missing a locale: the default locale's, then the key.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"$ref":"#/$defs/CollectionLabel"}},"icon":{"type":"string","description":"RFC-0040: a name from Studio's closed icon set (e.g. calendar, briefcase, users). Unknown names show the default icon. Never a URL or markup.","pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","maxLength":40},"feed":{"type":"boolean","description":"RFC-0040: entries appear in the site's RSS feeds. Absent: true."},"sitemap":{"type":"boolean","description":"RFC-0040: published entries appear in sitemap.xml. Absent: true."},"sort":{"type":"string","description":"RFC-0040: default order where none is given (Studio's list, entry queries without sort, MCP list_entries). Absent: newest.","enum":["newest","oldest","title"]},"fields":{"type":"array","description":"RFC-0041: the collection's typed custom fields, in form order. Values live in ContentEntry.fields under each key. At most 200 per site and unique keys (checked in code).","minItems":1,"maxItems":30,"items":{"$ref":"#/$defs/FieldDefinition"}},"listFields":{"type":"array","description":"RFC-0042: fields that listings, queries, Studio columns and MCP may filter and sort by, in column order. Keys of this collection's fields (checked in code).","minItems":1,"maxItems":4,"uniqueItems":true,"items":{"$ref":"#/$defs/FieldKey"}},"expiry":{"$ref":"#/$defs/CollectionExpiry"},"structuredData":{"$ref":"#/$defs/StructuredDataPreset"}}},"Consent":{"type":"object","additionalProperties":false,"required":["mode"],"properties":{"mode":{"type":"string","enum":["none","notice","choices"]},"policy":{"$ref":"#/$defs/Path"}}},"Design":{"type":"object","description":"The site's design rules for people and agents (RFC-0021): Markdown that agents read with the theme's DESIGN.md before they build. Guidance only: it changes neither rendering nor permissions.","additionalProperties":false,"required":["markdown"],"properties":{"markdown":{"type":"string","minLength":1,"maxLength":24000}}},"Domain":{"type":"object","additionalProperties":false,"required":["hostname","primary"],"properties":{"hostname":{"type":"string","pattern":"^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$","maxLength":253},"primary":{"type":"boolean"}}},"EntryValueProperty":{"type":"string","description":"RFC-0042: entry properties a structured-data property may read.","enum":["title","summary","body","coverImage","url","publishedAt","updatedAt"]},"FieldCondition":{"type":"object","description":"RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`.","additionalProperties":false,"required":["field","op"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","in","set","unset"]},"value":{"type":"string","minLength":1,"maxLength":100},"values":{"type":"array","minItems":1,"maxItems":20,"uniqueItems":true,"items":{"type":"string","minLength":1,"maxLength":100}}}},"FieldDefinition":{"description":"RFC-0041: one typed custom field of a collection, discriminated by `type`.","oneOf":[{"title":"TextField","type":"object","description":"Plain text: a string of at most maxLength characters.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["text"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"maxLength":{"type":"integer","description":"Absent: 5000.","minimum":1,"maximum":5000},"multiline":{"type":"boolean","description":"Line breaks allowed. Absent: false."},"searchable":{"type":"boolean","description":"Its words go into site search. Absent: false."}}},{"title":"RichTextField","type":"object","description":"Cloudhal Markdown v1 (ADR-0028), at most 50 000 characters.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["richText"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"searchable":{"type":"boolean","description":"Its words go into site search. Absent: false."}}},{"title":"NumberField","type":"object","description":"A finite number.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["number"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"integer":{"type":"boolean","description":"Whole numbers only. Absent: false."},"minimum":{"type":"number"},"maximum":{"type":"number"}}},{"title":"BooleanField","type":"object","description":"true or false; absent reads as false, so it cannot be required to publish.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["boolean"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"DateField","type":"object","description":"A calendar day without a zone (YYYY-MM-DD).","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["date"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"DateTimeField","type":"object","description":"An instant with the zone it was entered in: {at, timeZone} (RFC-0008 §9.1).","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["datetime"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"UrlField","type":"object","description":"A link: https, http, mailto, tel, a site-relative path or a #fragment; at most 2048 characters.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["url"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"ImageField","type":"object","description":"An image Asset of the same site, by id (asset_…).","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["image"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"SelectField","type":"object","description":"One of a closed list of options, or several when multiple.","additionalProperties":false,"required":["key","type","labels","localized","options"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["select"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"options":{"type":"array","minItems":1,"maxItems":50,"items":{"$ref":"#/$defs/FieldOption"}},"multiple":{"type":"boolean","description":"The value is a list of option values. Absent: false."}}},{"title":"ReferenceField","type":"object","description":"RFC-0045: entries of other (or this) collections of the same site, by translation group id (entry_…): the id of the group's original, which every language version and edition carries in translation.group.","additionalProperties":false,"required":["key","type","labels","localized","collections"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["reference"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version picks its own targets; false: only the translation group's original holds them, and every version resolves them in its own language."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs at least one target. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"collections":{"type":"array","description":"The collections a target may belong to: declared collections of this site (checked in code).","minItems":1,"maxItems":5,"uniqueItems":true,"items":{"$ref":"#/$defs/Slug"}},"multiple":{"type":"boolean","description":"The value is an ordered list of targets. Absent: false."},"maxItems":{"type":"integer","description":"With multiple: at most this many targets. Absent: 20.","minimum":1,"maximum":50},"where":{"type":"array","description":"Only with one target collection: conditions on its list fields that a newly chosen target must meet (no `now`; checked in code). Narrows the picker; not re-checked when the target changes later.","minItems":1,"maxItems":4,"items":{"$ref":"#/$defs/FieldCondition"}}}},{"title":"RepeaterField","type":"object","description":"RFC-0045: an ordered list of items, each a flat object of sub-fields. A repeater cannot contain a repeater.","additionalProperties":false,"required":["key","type","labels","localized","fields"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["repeater"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own list (missing reads the original's); false: only the original holds the list."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs at least one item. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"fields":{"type":"array","description":"The sub-fields of every item, in form order. Unique keys (checked in code).","minItems":1,"maxItems":10,"items":{"$ref":"#/$defs/SubFieldDefinition"}},"maxItems":{"type":"integer","description":"At most this many items. Absent: 20.","minimum":1,"maximum":50},"itemLabel":{"$ref":"#/$defs/FieldKey","description":"The text, select or reference sub-field that names an item in Studio and approval prompts. Absent: the first such sub-field."}}}]},"FieldKey":{"type":"string","description":"The field's key in ContentEntry.fields: lower camelCase, a JavaScript identifier. Unique in its collection; built-in entry property names are reserved (checked in code).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"FieldLabel":{"type":"object","additionalProperties":false,"required":["label"],"properties":{"label":{"type":"string","minLength":1,"maxLength":100},"help":{"type":"string","maxLength":300}}},"FieldLabels":{"type":"object","description":"The field's label and help per locale. Missing a locale: the default locale's, then the key.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"$ref":"#/$defs/FieldLabel"}},"FieldMigration":{"type":"object","description":"RFC-0041 §6: present only while a field operation rewrites entries.","additionalProperties":false,"required":["id"],"properties":{"id":{"$ref":"#/$defs/Id","description":"The operation's \"before\" snapshot."},"fromKey":{"$ref":"#/$defs/FieldKey"},"fromType":{"type":"string","enum":["text","richText","number","boolean","date","datetime","url","image","select","reference","repeater"]},"fromLocalized":{"type":"boolean"},"sub":{"$ref":"#/$defs/FieldKey","description":"RFC-0045: the repeater's sub-field the operation rewrites (its key once the operation is done); fromKey and fromType then describe the sub-field."}}},"FieldOption":{"type":"object","description":"One option of a select field. Values are unique in the field (checked in code).","additionalProperties":false,"required":["value","labels"],"properties":{"value":{"$ref":"#/$defs/Slug","description":"Stored in entries, never shown."},"labels":{"type":"object","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"type":"string","minLength":1,"maxLength":100}}}},"FieldOrder":{"type":"object","description":"RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last.","additionalProperties":false,"required":["field","direction"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"direction":{"type":"string","enum":["asc","desc"]}}},"FooterColumn":{"type":"object","additionalProperties":false,"required":["heading","menu"],"properties":{"heading":{"type":"string","minLength":1,"maxLength":60},"menu":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."}}},"HeaderAction":{"type":"object","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["button","search","locale","theme-toggle","motion-toggle"]},"label":{"type":"string","minLength":1,"maxLength":40},"href":{"type":"string","minLength":1,"maxLength":2048},"variant":{"type":"string","enum":["primary","secondary"]}}},"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"ImageRef":{"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}},"Link":{"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"Localization":{"type":"object","description":"How the site serves its locales (RFC-0033 §2). Absent fields mean: prefix-except-default routing, no fallback, no required review, the machine-translation notice until reviewed.","additionalProperties":false,"properties":{"routing":{"type":"string","description":"prefix-except-default: the default locale at the root, the others under //. prefix-all: every locale prefixed, / is a language chooser (x-default).","enum":["prefix-except-default","prefix-all"]},"domains":{"type":"object","description":"Optional hostname per locale; that locale is then served at the host's root.","propertyNames":{"$ref":"#/$defs/Locale"},"additionalProperties":{"type":"string","pattern":"^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$","maxLength":253}},"fallback":{"type":"string","description":"none: a missing translation is not found. default-with-notice: the default locale's page is served with a notice and noindex.","enum":["none","default-with-notice"]},"review":{"type":"object","description":"Collections whose machine translations need a person's review before they publish ('pages' stands for pages).","additionalProperties":false,"properties":{"required":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"uniqueItems":true}}},"label":{"type":"string","description":"When readers see the machine-translation notice: unreviewed (until a person reviews) or always.","enum":["unreviewed","always"]}}},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"Path":{"type":"string","description":"Absolute URL path of a route the site owns: starts with '/', lower-case slug segments, no trailing slash except root.","pattern":"^/(?:[a-z0-9]+(?:-[a-z0-9]+)*(?:/[a-z0-9]+(?:-[a-z0-9]+)*)*)?$","maxLength":1024},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Section":{"title":"Section","description":"A block inside a page, discriminated by `kind`. Variants are written inline (not via $ref) and `kind` is a single-value string enum so that TypeScript gets a literal union and Rust gets a serde internally-tagged enum.","oneOf":[{"type":"object","title":"HeroSection","additionalProperties":false,"required":["id","kind","heading"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["hero"]},"heading":{"type":"string","minLength":1,"maxLength":300},"subheading":{"type":"string","maxLength":1000},"image":{"$ref":"#/$defs/ImageRef"},"cta":{"$ref":"#/$defs/Link"}}},{"type":"object","title":"RichTextSection","additionalProperties":false,"required":["id","kind","format","body"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["richText"]},"format":{"type":"string","enum":["markdown","html"]},"body":{"type":"string","maxLength":200000},"profile":{"$ref":"#/$defs/BodyProfile"},"blocks":{"$ref":"#/$defs/BodyBlocks"}}},{"type":"object","title":"ImageSection","additionalProperties":false,"required":["id","kind","image"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["image"]},"image":{"$ref":"#/$defs/ImageRef"},"caption":{"type":"string","maxLength":1000}}},{"type":"object","title":"ContentListSection","description":"Renders the newest entries of a content collection (e.g. blog posts).","additionalProperties":false,"required":["id","kind","collection","limit","sort"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["contentList"]},"collection":{"$ref":"#/$defs/Slug"},"limit":{"type":"integer","minimum":1,"maximum":100},"sort":{"type":"string","enum":["newest","oldest","title"]},"filter":{"type":"string","description":"Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'.","minLength":1,"maxLength":512},"where":{"type":"array","description":"RFC-0042: conditions on the collection's list fields, ANDed.","minItems":1,"maxItems":4,"items":{"$ref":"#/$defs/FieldCondition"}},"orderBy":{"$ref":"#/$defs/FieldOrder"}}},{"type":"object","title":"ComponentSection","description":"A typed block from the block registry (RFC-0015): `type` names a registered block type (one global namespace of English names), `props` follow that type's props schema, which the store validates.","additionalProperties":false,"required":["id","kind","type","props"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["component"]},"type":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,40}$"},"props":{"type":"object","description":"Validated against the type's props schema by the store, not by this contract.","additionalProperties":true}}},{"type":"object","title":"SurfaceSection","description":"A generative UI surface (RFC-0020): a flat list of catalog components with a `root`, plus a data model their props may bind to. Written and streamed by agents as data, never code; the store checks the tree, the catalog and the limits.","additionalProperties":false,"required":["id","kind","catalog","components"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["surface"]},"catalog":{"type":"string","description":"The catalog the surface was written for: `@` (RFC-0018 manifest).","pattern":"^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"},"components":{"type":"array","minItems":1,"maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponent"}},"data":{"type":"object","description":"The surface's data model: JSON its components bind to with `{path}` (at most 64 KB serialized; the store enforces the size).","additionalProperties":true}}}]},"SiteValueProperty":{"type":"string","description":"RFC-0042: site properties a structured-data property may read.","enum":["name","url","logo"]},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"SocialLink":{"type":"object","additionalProperties":false,"required":["network","href"],"properties":{"network":{"type":"string","enum":["x","bluesky","mastodon","github","linkedin","instagram","facebook","youtube","tiktok","threads","rss","email"]},"href":{"type":"string","minLength":1,"maxLength":2048}}},"StructuredDataPreset":{"type":"object","description":"RFC-0042: the JSON-LD type an entry page emits in place of BlogPosting, with its properties mapped to value sources. Paths, fitting field types and required properties are a closed table in code (@cloudhal/core-ts/structured-data).","additionalProperties":false,"required":["type","properties"],"properties":{"type":{"type":"string","enum":["Event","JobPosting","Product","Recipe"]},"properties":{"type":"object","description":"Keyed by property path, e.g. startDate or location.address.addressLocality.","propertyNames":{"type":"string","pattern":"^[a-z][A-Za-z]{0,39}(\\.[a-z][A-Za-z]{0,39}){0,2}$"},"minProperties":1,"maxProperties":40,"additionalProperties":{"$ref":"#/$defs/StructuredDataSource"}}}},"StructuredDataSource":{"description":"RFC-0042: where one JSON-LD property's value comes from, discriminated by `kind`.","oneOf":[{"title":"FieldValueSource","type":"object","description":"A typed field of this collection, converted by its type.","additionalProperties":false,"required":["kind","field"],"properties":{"kind":{"type":"string","enum":["field"]},"field":{"$ref":"#/$defs/FieldKey"}}},{"title":"EntryValueSource","type":"object","description":"A built-in property of the entry.","additionalProperties":false,"required":["kind","property"],"properties":{"kind":{"type":"string","enum":["entry"]},"property":{"$ref":"#/$defs/EntryValueProperty"}}},{"title":"SiteValueSource","type":"object","description":"A property of the site.","additionalProperties":false,"required":["kind","property"],"properties":{"kind":{"type":"string","enum":["site"]},"property":{"$ref":"#/$defs/SiteValueProperty"}}},{"title":"ConstantValueSource","type":"object","description":"A plain-text constant: no control characters, no markup meaning; vocabulary-checked on enum paths (in code).","additionalProperties":false,"required":["kind","value"],"properties":{"kind":{"type":"string","enum":["constant"]},"value":{"type":"string","minLength":1,"maxLength":200,"pattern":"^[^\\x00-\\x1F\\x7F]+$"}}}]},"SubFieldDefinition":{"description":"RFC-0045: one sub-field of a repeater item, discriminated by `type`. Only types whose value is a scalar: no datetime, no multiple select or reference, no repeater. No localized and no migration: the repeater's apply.","oneOf":[{"title":"TextSubField","type":"object","additionalProperties":false,"required":["key","type","labels"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["text"]},"labels":{"$ref":"#/$defs/FieldLabels"},"requiredToPublish":{"type":"boolean","description":"Every item of an entry in review or published needs a value. Absent: false."},"maxLength":{"type":"integer","description":"Absent: 5000.","minimum":1,"maximum":5000},"multiline":{"type":"boolean","description":"Line breaks allowed. Absent: false."}}},{"title":"RichTextSubField","type":"object","additionalProperties":false,"required":["key","type","labels"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["richText"]},"labels":{"$ref":"#/$defs/FieldLabels"},"requiredToPublish":{"type":"boolean","description":"Every item of an entry in review or published needs a value. Absent: false."}}},{"title":"NumberSubField","type":"object","additionalProperties":false,"required":["key","type","labels"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["number"]},"labels":{"$ref":"#/$defs/FieldLabels"},"requiredToPublish":{"type":"boolean","description":"Every item of an entry in review or published needs a value. Absent: false."},"integer":{"type":"boolean","description":"Whole numbers only. Absent: false."},"minimum":{"type":"number"},"maximum":{"type":"number"}}},{"title":"BooleanSubField","type":"object","additionalProperties":false,"required":["key","type","labels"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["boolean"]},"labels":{"$ref":"#/$defs/FieldLabels"}}},{"title":"DateSubField","type":"object","additionalProperties":false,"required":["key","type","labels"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["date"]},"labels":{"$ref":"#/$defs/FieldLabels"},"requiredToPublish":{"type":"boolean","description":"Every item of an entry in review or published needs a value. Absent: false."}}},{"title":"UrlSubField","type":"object","additionalProperties":false,"required":["key","type","labels"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["url"]},"labels":{"$ref":"#/$defs/FieldLabels"},"requiredToPublish":{"type":"boolean","description":"Every item of an entry in review or published needs a value. Absent: false."}}},{"title":"ImageSubField","type":"object","additionalProperties":false,"required":["key","type","labels"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["image"]},"labels":{"$ref":"#/$defs/FieldLabels"},"requiredToPublish":{"type":"boolean","description":"Every item of an entry in review or published needs a value. Absent: false."}}},{"title":"SelectSubField","type":"object","additionalProperties":false,"required":["key","type","labels","options"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["select"]},"labels":{"$ref":"#/$defs/FieldLabels"},"requiredToPublish":{"type":"boolean","description":"Every item of an entry in review or published needs a value. Absent: false."},"options":{"type":"array","minItems":1,"maxItems":50,"items":{"$ref":"#/$defs/FieldOption"}}}},{"title":"ReferenceSubField","type":"object","additionalProperties":false,"required":["key","type","labels","collections"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["reference"]},"labels":{"$ref":"#/$defs/FieldLabels"},"requiredToPublish":{"type":"boolean","description":"Every item of an entry in review or published needs a value. Absent: false."},"collections":{"type":"array","description":"The collections a target may belong to (checked in code).","minItems":1,"maxItems":5,"uniqueItems":true,"items":{"$ref":"#/$defs/Slug"}}}}]},"SurfaceComponent":{"type":"object","description":"One element of a generative UI surface (RFC-0020 §1), in a flat list: `component` names a catalog entry (a PascalCase built-in primitive such as `Stack`, or a registry block type such as `callout`), `children` refer to other ids of the same surface, and `props` follow the entry's schema, which the store checks. Field names follow A2UI v0.9.","additionalProperties":false,"required":["id","component"],"properties":{"id":{"$ref":"#/$defs/SurfaceComponentId"},"component":{"type":"string","pattern":"^(?:[A-Z][A-Za-z0-9]{1,40}|[a-z][a-z0-9-]{1,40})$"},"children":{"type":"array","maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponentId"}},"props":{"type":"object","description":"Literal values, `{path}` bindings into the surface's data, or `{call}` named functions; validated by the store against the catalog, not by this contract.","additionalProperties":true},"visible":{"type":"object","description":"A visibility condition over the surface's data (`eq`, `ne`, `gt`, `lt`, `and`, `or`, `not`); validated by the store.","additionalProperties":true},"motion":{"type":"object","description":"Motion presets for this component (RFC-0024 §2): `reveal`, `hover`, `speed`, `delay`, `stagger`, `replay` and later `scroll`, `loop`; enums and clamped numbers only, validated by the store against the catalog. The renderer turns them into CSS; agents never write CSS.","additionalProperties":true}}},"SurfaceComponentId":{"type":"string","description":"A component's id, unique within its surface; `root` is the tree's root (RFC-0020 §1).","pattern":"^[a-z][a-z0-9-]{0,40}$"},"ThemeRef":{"type":"object","description":"Theme (a.k.a. Experience) applied by the renderer. Config is theme-defined and validated by the theme's own schema, not here.","additionalProperties":false,"required":["name"],"properties":{"name":{"$ref":"#/$defs/Slug"},"version":{"type":"string","maxLength":64},"config":{"type":"object","additionalProperties":true}}},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"},"TimeZone":{"type":"string","description":"IANA time zone name from the tz database (RFC-0008), e.g. Europe/Istanbul, America/Argentina/Buenos_Aires, Etc/GMT+3 or UTC. Wall-clock times a person enters are kept with their zone, so the instant is recomputed when governments change the rules. The pattern bounds the shape only; writers also check the name against the runtime's Intl zone list.","pattern":"^(?:UTC|[A-Z][A-Za-z_-]+(?:/[A-Za-z0-9_+-]+){1,2})$","maxLength":64}}}; const schema42 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; const schema43 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; const schema45 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; const schema46 = {"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"}; const schema48 = {"type":"object","additionalProperties":false,"required":["hostname","primary"],"properties":{"hostname":{"type":"string","pattern":"^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$","maxLength":253},"primary":{"type":"boolean"}}}; -const schema86 = {"type":"object","description":"The site's design rules for people and agents (RFC-0021): Markdown that agents read with the theme's DESIGN.md before they build. Guidance only: it changes neither rendering nor permissions.","additionalProperties":false,"required":["markdown"],"properties":{"markdown":{"type":"string","minLength":1,"maxLength":24000}}}; -const schema124 = {"type":"string","description":"IANA time zone name from the tz database (RFC-0008), e.g. Europe/Istanbul, America/Argentina/Buenos_Aires, Etc/GMT+3 or UTC. Wall-clock times a person enters are kept with their zone, so the instant is recomputed when governments change the rules. The pattern bounds the shape only; writers also check the name against the runtime's Intl zone list.","pattern":"^(?:UTC|[A-Z][A-Za-z_-]+(?:/[A-Za-z0-9_+-]+){1,2})$","maxLength":64}; -const schema126 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; -const schema110 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; -const schema129 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const schema103 = {"type":"object","description":"The site's design rules for people and agents (RFC-0021): Markdown that agents read with the theme's DESIGN.md before they build. Guidance only: it changes neither rendering nor permissions.","additionalProperties":false,"required":["markdown"],"properties":{"markdown":{"type":"string","minLength":1,"maxLength":24000}}}; +const schema141 = {"type":"string","description":"IANA time zone name from the tz database (RFC-0008), e.g. Europe/Istanbul, America/Argentina/Buenos_Aires, Etc/GMT+3 or UTC. Wall-clock times a person enters are kept with their zone, so the instant is recomputed when governments change the rules. The pattern bounds the shape only; writers also check the name against the runtime's Intl zone list.","pattern":"^(?:UTC|[A-Z][A-Za-z_-]+(?:/[A-Za-z0-9_+-]+){1,2})$","maxLength":64}; +const schema143 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema127 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema146 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; const pattern11 = new RegExp("^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$", "u"); const pattern13 = new RegExp("^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$", "u"); -const pattern66 = new RegExp("^(?:UTC|[A-Z][A-Za-z_-]+(?:/[A-Za-z0-9_+-]+){1,2})$", "u"); +const pattern82 = new RegExp("^(?:UTC|[A-Z][A-Za-z_-]+(?:/[A-Za-z0-9_+-]+){1,2})$", "u"); const schema51 = {"type":"object","description":"How the site serves its locales (RFC-0033 §2). Absent fields mean: prefix-except-default routing, no fallback, no required review, the machine-translation notice until reviewed.","additionalProperties":false,"properties":{"routing":{"type":"string","description":"prefix-except-default: the default locale at the root, the others under //. prefix-all: every locale prefixed, / is a language chooser (x-default).","enum":["prefix-except-default","prefix-all"]},"domains":{"type":"object","description":"Optional hostname per locale; that locale is then served at the host's root.","propertyNames":{"$ref":"#/$defs/Locale"},"additionalProperties":{"type":"string","pattern":"^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$","maxLength":253}},"fallback":{"type":"string","description":"none: a missing translation is not found. default-with-notice: the default locale's page is served with a notice and noindex.","enum":["none","default-with-notice"]},"review":{"type":"object","description":"Collections whose machine translations need a person's review before they publish ('pages' stands for pages).","additionalProperties":false,"properties":{"required":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"uniqueItems":true}}},"label":{"type":"string","description":"When readers see the machine-translation notice: unreviewed (until a person reviews) or always.","enum":["unreviewed","always"]}}}; function validate22(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ @@ -848,7 +848,8 @@ const schema58 = {"type":"string","description":"The field's key in ContentEntry const pattern19 = new RegExp("^(/[a-z0-9]+(-[a-z0-9]+)*)+/\\{slug\\}$", "u"); const pattern21 = new RegExp("^[a-z0-9]+(-[a-z0-9]+)*$", "u"); const pattern22 = new RegExp("^[a-z][A-Za-z0-9]{0,39}$", "u"); -const schema57 = {"description":"RFC-0041: one typed custom field of a collection, discriminated by `type`.","oneOf":[{"title":"TextField","type":"object","description":"Plain text: a string of at most maxLength characters.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["text"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"maxLength":{"type":"integer","description":"Absent: 5000.","minimum":1,"maximum":5000},"multiline":{"type":"boolean","description":"Line breaks allowed. Absent: false."},"searchable":{"type":"boolean","description":"Its words go into site search. Absent: false."}}},{"title":"RichTextField","type":"object","description":"Cloudhal Markdown v1 (ADR-0028), at most 50 000 characters.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["richText"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"searchable":{"type":"boolean","description":"Its words go into site search. Absent: false."}}},{"title":"NumberField","type":"object","description":"A finite number.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["number"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"integer":{"type":"boolean","description":"Whole numbers only. Absent: false."},"minimum":{"type":"number"},"maximum":{"type":"number"}}},{"title":"BooleanField","type":"object","description":"true or false; absent reads as false, so it cannot be required to publish.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["boolean"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"DateField","type":"object","description":"A calendar day without a zone (YYYY-MM-DD).","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["date"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"DateTimeField","type":"object","description":"An instant with the zone it was entered in: {at, timeZone} (RFC-0008 §9.1).","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["datetime"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"UrlField","type":"object","description":"A link: https, http, mailto, tel, a site-relative path or a #fragment; at most 2048 characters.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["url"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"ImageField","type":"object","description":"An image Asset of the same site, by id (asset_…).","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["image"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"SelectField","type":"object","description":"One of a closed list of options, or several when multiple.","additionalProperties":false,"required":["key","type","labels","localized","options"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["select"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"options":{"type":"array","minItems":1,"maxItems":50,"items":{"$ref":"#/$defs/FieldOption"}},"multiple":{"type":"boolean","description":"The value is a list of option values. Absent: false."}}}]}; +const schema57 = {"description":"RFC-0041: one typed custom field of a collection, discriminated by `type`.","oneOf":[{"title":"TextField","type":"object","description":"Plain text: a string of at most maxLength characters.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["text"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"maxLength":{"type":"integer","description":"Absent: 5000.","minimum":1,"maximum":5000},"multiline":{"type":"boolean","description":"Line breaks allowed. Absent: false."},"searchable":{"type":"boolean","description":"Its words go into site search. Absent: false."}}},{"title":"RichTextField","type":"object","description":"Cloudhal Markdown v1 (ADR-0028), at most 50 000 characters.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["richText"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"searchable":{"type":"boolean","description":"Its words go into site search. Absent: false."}}},{"title":"NumberField","type":"object","description":"A finite number.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["number"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"integer":{"type":"boolean","description":"Whole numbers only. Absent: false."},"minimum":{"type":"number"},"maximum":{"type":"number"}}},{"title":"BooleanField","type":"object","description":"true or false; absent reads as false, so it cannot be required to publish.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["boolean"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"DateField","type":"object","description":"A calendar day without a zone (YYYY-MM-DD).","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["date"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"DateTimeField","type":"object","description":"An instant with the zone it was entered in: {at, timeZone} (RFC-0008 §9.1).","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["datetime"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"UrlField","type":"object","description":"A link: https, http, mailto, tel, a site-relative path or a #fragment; at most 2048 characters.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["url"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"ImageField","type":"object","description":"An image Asset of the same site, by id (asset_…).","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["image"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"SelectField","type":"object","description":"One of a closed list of options, or several when multiple.","additionalProperties":false,"required":["key","type","labels","localized","options"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["select"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"options":{"type":"array","minItems":1,"maxItems":50,"items":{"$ref":"#/$defs/FieldOption"}},"multiple":{"type":"boolean","description":"The value is a list of option values. Absent: false."}}},{"title":"ReferenceField","type":"object","description":"RFC-0045: entries of other (or this) collections of the same site, by translation group id (entry_…): the id of the group's original, which every language version and edition carries in translation.group.","additionalProperties":false,"required":["key","type","labels","localized","collections"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["reference"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version picks its own targets; false: only the translation group's original holds them, and every version resolves them in its own language."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs at least one target. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"collections":{"type":"array","description":"The collections a target may belong to: declared collections of this site (checked in code).","minItems":1,"maxItems":5,"uniqueItems":true,"items":{"$ref":"#/$defs/Slug"}},"multiple":{"type":"boolean","description":"The value is an ordered list of targets. Absent: false."},"maxItems":{"type":"integer","description":"With multiple: at most this many targets. Absent: 20.","minimum":1,"maximum":50},"where":{"type":"array","description":"Only with one target collection: conditions on its list fields that a newly chosen target must meet (no `now`; checked in code). Narrows the picker; not re-checked when the target changes later.","minItems":1,"maxItems":4,"items":{"$ref":"#/$defs/FieldCondition"}}}},{"title":"RepeaterField","type":"object","description":"RFC-0045: an ordered list of items, each a flat object of sub-fields. A repeater cannot contain a repeater.","additionalProperties":false,"required":["key","type","labels","localized","fields"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["repeater"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own list (missing reads the original's); false: only the original holds the list."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs at least one item. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"fields":{"type":"array","description":"The sub-fields of every item, in form order. Unique keys (checked in code).","minItems":1,"maxItems":10,"items":{"$ref":"#/$defs/SubFieldDefinition"}},"maxItems":{"type":"integer","description":"At most this many items. Absent: 20.","minimum":1,"maximum":50},"itemLabel":{"$ref":"#/$defs/FieldKey","description":"The text, select or reference sub-field that names an item in Studio and approval prompts. Absent: the first such sub-field."}}}]}; +const schema79 = {"type":"object","description":"RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`.","additionalProperties":false,"required":["field","op"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","in","set","unset"]},"value":{"type":"string","minLength":1,"maxLength":100},"values":{"type":"array","minItems":1,"maxItems":20,"uniqueItems":true,"items":{"type":"string","minLength":1,"maxLength":100}}}}; const schema59 = {"type":"object","description":"The field's label and help per locale. Missing a locale: the default locale's, then the key.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"$ref":"#/$defs/FieldLabel"}}; const schema61 = {"type":"object","additionalProperties":false,"required":["label"],"properties":{"label":{"type":"string","minLength":1,"maxLength":100},"help":{"type":"string","maxLength":300}}}; @@ -1022,7 +1023,7 @@ return errors === 0; } validate26.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const schema62 = {"type":"object","description":"RFC-0041 §6: present only while a field operation rewrites entries.","additionalProperties":false,"required":["id"],"properties":{"id":{"$ref":"#/$defs/Id","description":"The operation's \"before\" snapshot."},"fromKey":{"$ref":"#/$defs/FieldKey"},"fromType":{"type":"string","enum":["text","richText","number","boolean","date","datetime","url","image","select"]},"fromLocalized":{"type":"boolean"}}}; +const schema62 = {"type":"object","description":"RFC-0041 §6: present only while a field operation rewrites entries.","additionalProperties":false,"required":["id"],"properties":{"id":{"$ref":"#/$defs/Id","description":"The operation's \"before\" snapshot."},"fromKey":{"$ref":"#/$defs/FieldKey"},"fromType":{"type":"string","enum":["text","richText","number","boolean","date","datetime","url","image","select","reference","repeater"]},"fromLocalized":{"type":"boolean"},"sub":{"$ref":"#/$defs/FieldKey","description":"RFC-0045: the repeater's sub-field the operation rewrites (its key once the operation is done); fromKey and fromType then describe the sub-field."}}}; function validate28(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; @@ -1046,7 +1047,7 @@ vErrors.push(err0); errors++; } for(const key0 in data){ -if(!((((key0 === "id") || (key0 === "fromKey")) || (key0 === "fromType")) || (key0 === "fromLocalized"))){ +if(!(((((key0 === "id") || (key0 === "fromKey")) || (key0 === "fromType")) || (key0 === "fromLocalized")) || (key0 === "sub"))){ const err1 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err1]; @@ -1139,7 +1140,7 @@ vErrors.push(err8); } errors++; } -if(!(((((((((data2 === "text") || (data2 === "richText")) || (data2 === "number")) || (data2 === "boolean")) || (data2 === "date")) || (data2 === "datetime")) || (data2 === "url")) || (data2 === "image")) || (data2 === "select"))){ +if(!(((((((((((data2 === "text") || (data2 === "richText")) || (data2 === "number")) || (data2 === "boolean")) || (data2 === "date")) || (data2 === "datetime")) || (data2 === "url")) || (data2 === "image")) || (data2 === "select")) || (data2 === "reference")) || (data2 === "repeater"))){ const err9 = {instancePath:instancePath+"/fromType",schemaPath:"#/properties/fromType/enum",keyword:"enum",params:{allowedValues: schema62.properties.fromType.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err9]; @@ -1162,9 +1163,11 @@ vErrors.push(err10); errors++; } } -} -else { -const err11 = {instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(data.sub !== undefined){ +let data4 = data.sub; +if(typeof data4 === "string"){ +if(!pattern22.test(data4)){ +const err11 = {instancePath:instancePath+"/sub",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; if(vErrors === null){ vErrors = [err11]; } @@ -1173,12 +1176,35 @@ vErrors.push(err11); } errors++; } +} +else { +const err12 = {instancePath:instancePath+"/sub",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err12]; +} +else { +vErrors.push(err12); +} +errors++; +} +} +} +else { +const err13 = {instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err13]; +} +else { +vErrors.push(err13); +} +errors++; +} validate28.errors = vErrors; return errors === 0; } validate28.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const schema73 = {"type":"object","description":"One option of a select field. Values are unique in the field (checked in code).","additionalProperties":false,"required":["value","labels"],"properties":{"value":{"$ref":"#/$defs/Slug","description":"Stored in entries, never shown."},"labels":{"type":"object","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"type":"string","minLength":1,"maxLength":100}}}}; +const schema74 = {"type":"object","description":"One option of a select field. Values are unique in the field (checked in code).","additionalProperties":false,"required":["value","labels"],"properties":{"value":{"$ref":"#/$defs/Slug","description":"Stored in entries, never shown."},"labels":{"type":"object","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"type":"string","minLength":1,"maxLength":100}}}}; function validate46(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; @@ -1380,11 +1406,12 @@ return errors === 0; } validate46.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +const schema81 = {"description":"RFC-0045: one sub-field of a repeater item, discriminated by `type`. Only types whose value is a scalar: no datetime, no multiple select or reference, no repeater. No localized and no migration: the repeater's apply.","oneOf":[{"title":"TextSubField","type":"object","additionalProperties":false,"required":["key","type","labels"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["text"]},"labels":{"$ref":"#/$defs/FieldLabels"},"requiredToPublish":{"type":"boolean","description":"Every item of an entry in review or published needs a value. Absent: false."},"maxLength":{"type":"integer","description":"Absent: 5000.","minimum":1,"maximum":5000},"multiline":{"type":"boolean","description":"Line breaks allowed. Absent: false."}}},{"title":"RichTextSubField","type":"object","additionalProperties":false,"required":["key","type","labels"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["richText"]},"labels":{"$ref":"#/$defs/FieldLabels"},"requiredToPublish":{"type":"boolean","description":"Every item of an entry in review or published needs a value. Absent: false."}}},{"title":"NumberSubField","type":"object","additionalProperties":false,"required":["key","type","labels"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["number"]},"labels":{"$ref":"#/$defs/FieldLabels"},"requiredToPublish":{"type":"boolean","description":"Every item of an entry in review or published needs a value. Absent: false."},"integer":{"type":"boolean","description":"Whole numbers only. Absent: false."},"minimum":{"type":"number"},"maximum":{"type":"number"}}},{"title":"BooleanSubField","type":"object","additionalProperties":false,"required":["key","type","labels"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["boolean"]},"labels":{"$ref":"#/$defs/FieldLabels"}}},{"title":"DateSubField","type":"object","additionalProperties":false,"required":["key","type","labels"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["date"]},"labels":{"$ref":"#/$defs/FieldLabels"},"requiredToPublish":{"type":"boolean","description":"Every item of an entry in review or published needs a value. Absent: false."}}},{"title":"UrlSubField","type":"object","additionalProperties":false,"required":["key","type","labels"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["url"]},"labels":{"$ref":"#/$defs/FieldLabels"},"requiredToPublish":{"type":"boolean","description":"Every item of an entry in review or published needs a value. Absent: false."}}},{"title":"ImageSubField","type":"object","additionalProperties":false,"required":["key","type","labels"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["image"]},"labels":{"$ref":"#/$defs/FieldLabels"},"requiredToPublish":{"type":"boolean","description":"Every item of an entry in review or published needs a value. Absent: false."}}},{"title":"SelectSubField","type":"object","additionalProperties":false,"required":["key","type","labels","options"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["select"]},"labels":{"$ref":"#/$defs/FieldLabels"},"requiredToPublish":{"type":"boolean","description":"Every item of an entry in review or published needs a value. Absent: false."},"options":{"type":"array","minItems":1,"maxItems":50,"items":{"$ref":"#/$defs/FieldOption"}}}},{"title":"ReferenceSubField","type":"object","additionalProperties":false,"required":["key","type","labels","collections"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["reference"]},"labels":{"$ref":"#/$defs/FieldLabels"},"requiredToPublish":{"type":"boolean","description":"Every item of an entry in review or published needs a value. Absent: false."},"collections":{"type":"array","description":"The collections a target may belong to (checked in code).","minItems":1,"maxItems":5,"uniqueItems":true,"items":{"$ref":"#/$defs/Slug"}}}}]}; -function validate25(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate52(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate25.evaluated; +const evaluated0 = validate52.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -1426,8 +1453,9 @@ vErrors.push(err2); } errors++; } -if(data.localized === undefined){ -const err3 = {instancePath,schemaPath:"#/oneOf/0/required",keyword:"required",params:{missingProperty: "localized"},message:"must have required property '"+"localized"+"'"}; +for(const key0 in data){ +if(!((((((key0 === "key") || (key0 === "type")) || (key0 === "labels")) || (key0 === "requiredToPublish")) || (key0 === "maxLength")) || (key0 === "multiline"))){ +const err3 = {instancePath,schemaPath:"#/oneOf/0/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err3]; } @@ -1436,39 +1464,28 @@ vErrors.push(err3); } errors++; } -for(const key0 in data){ -if(!(func1.call(schema57.oneOf[0].properties, key0))){ -const err4 = {instancePath,schemaPath:"#/oneOf/0/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; -if(vErrors === null){ -vErrors = [err4]; -} -else { -vErrors.push(err4); -} -errors++; -} } if(data.key !== undefined){ let data0 = data.key; if(typeof data0 === "string"){ if(!pattern22.test(data0)){ -const err5 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; +const err4 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; if(vErrors === null){ -vErrors = [err5]; +vErrors = [err4]; } else { -vErrors.push(err5); +vErrors.push(err4); } errors++; } } else { -const err6 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; +const err5 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ -vErrors = [err6]; +vErrors = [err5]; } else { -vErrors.push(err6); +vErrors.push(err5); } errors++; } @@ -1476,22 +1493,22 @@ errors++; if(data.type !== undefined){ let data1 = data.type; if(typeof data1 !== "string"){ -const err7 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/0/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +const err6 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/0/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ -vErrors = [err7]; +vErrors = [err6]; } else { -vErrors.push(err7); +vErrors.push(err6); } errors++; } if(!(data1 === "text")){ -const err8 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/0/properties/type/enum",keyword:"enum",params:{allowedValues: schema57.oneOf[0].properties.type.enum},message:"must be equal to one of the allowed values"}; +const err7 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/0/properties/type/enum",keyword:"enum",params:{allowedValues: schema81.oneOf[0].properties.type.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ -vErrors = [err8]; +vErrors = [err7]; } else { -vErrors.push(err8); +vErrors.push(err7); } errors++; } @@ -1502,40 +1519,43 @@ vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.error errors = vErrors.length; } } -if(data.localized !== undefined){ -if(typeof data.localized !== "boolean"){ -const err9 = {instancePath:instancePath+"/localized",schemaPath:"#/oneOf/0/properties/localized/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(data.requiredToPublish !== undefined){ +if(typeof data.requiredToPublish !== "boolean"){ +const err8 = {instancePath:instancePath+"/requiredToPublish",schemaPath:"#/oneOf/0/properties/requiredToPublish/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; if(vErrors === null){ -vErrors = [err9]; +vErrors = [err8]; } else { -vErrors.push(err9); +vErrors.push(err8); } errors++; } } -if(data.requiredToPublish !== undefined){ -if(typeof data.requiredToPublish !== "boolean"){ -const err10 = {instancePath:instancePath+"/requiredToPublish",schemaPath:"#/oneOf/0/properties/requiredToPublish/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(data.maxLength !== undefined){ +let data4 = data.maxLength; +if(!(((typeof data4 == "number") && (!(data4 % 1) && !isNaN(data4))) && (isFinite(data4)))){ +const err9 = {instancePath:instancePath+"/maxLength",schemaPath:"#/oneOf/0/properties/maxLength/type",keyword:"type",params:{type: "integer"},message:"must be integer"}; if(vErrors === null){ -vErrors = [err10]; +vErrors = [err9]; } else { -vErrors.push(err10); +vErrors.push(err9); } errors++; } +if((typeof data4 == "number") && (isFinite(data4))){ +if(data4 > 5000 || isNaN(data4)){ +const err10 = {instancePath:instancePath+"/maxLength",schemaPath:"#/oneOf/0/properties/maxLength/maximum",keyword:"maximum",params:{comparison: "<=", limit: 5000},message:"must be <= 5000"}; +if(vErrors === null){ +vErrors = [err10]; } -if(data.migration !== undefined){ -if(!(validate28(data.migration, {instancePath:instancePath+"/migration",parentData:data,parentDataProperty:"migration",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate28.errors : vErrors.concat(validate28.errors); -errors = vErrors.length; +else { +vErrors.push(err10); } +errors++; } -if(data.maxLength !== undefined){ -let data6 = data.maxLength; -if(!(((typeof data6 == "number") && (!(data6 % 1) && !isNaN(data6))) && (isFinite(data6)))){ -const err11 = {instancePath:instancePath+"/maxLength",schemaPath:"#/oneOf/0/properties/maxLength/type",keyword:"type",params:{type: "integer"},message:"must be integer"}; +if(data4 < 1 || isNaN(data4)){ +const err11 = {instancePath:instancePath+"/maxLength",schemaPath:"#/oneOf/0/properties/maxLength/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"}; if(vErrors === null){ vErrors = [err11]; } @@ -1544,9 +1564,11 @@ vErrors.push(err11); } errors++; } -if((typeof data6 == "number") && (isFinite(data6))){ -if(data6 > 5000 || isNaN(data6)){ -const err12 = {instancePath:instancePath+"/maxLength",schemaPath:"#/oneOf/0/properties/maxLength/maximum",keyword:"maximum",params:{comparison: "<=", limit: 5000},message:"must be <= 5000"}; +} +} +if(data.multiline !== undefined){ +if(typeof data.multiline !== "boolean"){ +const err12 = {instancePath:instancePath+"/multiline",schemaPath:"#/oneOf/0/properties/multiline/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; if(vErrors === null){ vErrors = [err12]; } @@ -1555,8 +1577,10 @@ vErrors.push(err12); } errors++; } -if(data6 < 1 || isNaN(data6)){ -const err13 = {instancePath:instancePath+"/maxLength",schemaPath:"#/oneOf/0/properties/maxLength/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"}; +} +} +else { +const err13 = {instancePath,schemaPath:"#/oneOf/0/type",keyword:"type",params:{type: "object"},message:"must be object"}; if(vErrors === null){ vErrors = [err13]; } @@ -1565,11 +1589,16 @@ vErrors.push(err13); } errors++; } +var _valid0 = _errs1 === errors; +if(_valid0){ +valid0 = true; +passing0 = 0; +var props0 = true; } -} -if(data.multiline !== undefined){ -if(typeof data.multiline !== "boolean"){ -const err14 = {instancePath:instancePath+"/multiline",schemaPath:"#/oneOf/0/properties/multiline/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +const _errs16 = errors; +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.key === undefined){ +const err14 = {instancePath,schemaPath:"#/oneOf/1/required",keyword:"required",params:{missingProperty: "key"},message:"must have required property '"+"key"+"'"}; if(vErrors === null){ vErrors = [err14]; } @@ -1578,10 +1607,8 @@ vErrors.push(err14); } errors++; } -} -if(data.searchable !== undefined){ -if(typeof data.searchable !== "boolean"){ -const err15 = {instancePath:instancePath+"/searchable",schemaPath:"#/oneOf/0/properties/searchable/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(data.type === undefined){ +const err15 = {instancePath,schemaPath:"#/oneOf/1/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; if(vErrors === null){ vErrors = [err15]; } @@ -1590,10 +1617,8 @@ vErrors.push(err15); } errors++; } -} -} -else { -const err16 = {instancePath,schemaPath:"#/oneOf/0/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(data.labels === undefined){ +const err16 = {instancePath,schemaPath:"#/oneOf/1/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; if(vErrors === null){ vErrors = [err16]; } @@ -1602,16 +1627,9 @@ vErrors.push(err16); } errors++; } -var _valid0 = _errs1 === errors; -if(_valid0){ -valid0 = true; -passing0 = 0; -var props0 = true; -} -const _errs21 = errors; -if(data && typeof data == "object" && !Array.isArray(data)){ -if(data.key === undefined){ -const err17 = {instancePath,schemaPath:"#/oneOf/1/required",keyword:"required",params:{missingProperty: "key"},message:"must have required property '"+"key"+"'"}; +for(const key1 in data){ +if(!((((key1 === "key") || (key1 === "type")) || (key1 === "labels")) || (key1 === "requiredToPublish"))){ +const err17 = {instancePath,schemaPath:"#/oneOf/1/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key1},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err17]; } @@ -1620,8 +1638,12 @@ vErrors.push(err17); } errors++; } -if(data.type === undefined){ -const err18 = {instancePath,schemaPath:"#/oneOf/1/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; +} +if(data.key !== undefined){ +let data6 = data.key; +if(typeof data6 === "string"){ +if(!pattern22.test(data6)){ +const err18 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; if(vErrors === null){ vErrors = [err18]; } @@ -1630,8 +1652,9 @@ vErrors.push(err18); } errors++; } -if(data.labels === undefined){ -const err19 = {instancePath,schemaPath:"#/oneOf/1/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; +} +else { +const err19 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ vErrors = [err19]; } @@ -1640,8 +1663,11 @@ vErrors.push(err19); } errors++; } -if(data.localized === undefined){ -const err20 = {instancePath,schemaPath:"#/oneOf/1/required",keyword:"required",params:{missingProperty: "localized"},message:"must have required property '"+"localized"+"'"}; +} +if(data.type !== undefined){ +let data7 = data.type; +if(typeof data7 !== "string"){ +const err20 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/1/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ vErrors = [err20]; } @@ -1650,9 +1676,8 @@ vErrors.push(err20); } errors++; } -for(const key1 in data){ -if(!(((((((key1 === "key") || (key1 === "type")) || (key1 === "labels")) || (key1 === "localized")) || (key1 === "requiredToPublish")) || (key1 === "migration")) || (key1 === "searchable"))){ -const err21 = {instancePath,schemaPath:"#/oneOf/1/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key1},message:"must NOT have additional properties"}; +if(!(data7 === "richText")){ +const err21 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/1/properties/type/enum",keyword:"enum",params:{allowedValues: schema81.oneOf[1].properties.type.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err21]; } @@ -1662,11 +1687,15 @@ vErrors.push(err21); errors++; } } -if(data.key !== undefined){ -let data9 = data.key; -if(typeof data9 === "string"){ -if(!pattern22.test(data9)){ -const err22 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; +if(data.labels !== undefined){ +if(!(validate26(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.errors); +errors = vErrors.length; +} +} +if(data.requiredToPublish !== undefined){ +if(typeof data.requiredToPublish !== "boolean"){ +const err22 = {instancePath:instancePath+"/requiredToPublish",schemaPath:"#/oneOf/1/properties/requiredToPublish/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; if(vErrors === null){ vErrors = [err22]; } @@ -1676,8 +1705,9 @@ vErrors.push(err22); errors++; } } +} else { -const err23 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; +const err23 = {instancePath,schemaPath:"#/oneOf/1/type",keyword:"type",params:{type: "object"},message:"must be object"}; if(vErrors === null){ vErrors = [err23]; } @@ -1686,11 +1716,23 @@ vErrors.push(err23); } errors++; } +var _valid0 = _errs16 === errors; +if(_valid0 && valid0){ +valid0 = false; +passing0 = [passing0, 1]; } -if(data.type !== undefined){ -let data10 = data.type; -if(typeof data10 !== "string"){ -const err24 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/1/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +else { +if(_valid0){ +valid0 = true; +passing0 = 1; +if(props0 !== true){ +props0 = true; +} +} +const _errs27 = errors; +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.key === undefined){ +const err24 = {instancePath,schemaPath:"#/oneOf/2/required",keyword:"required",params:{missingProperty: "key"},message:"must have required property '"+"key"+"'"}; if(vErrors === null){ vErrors = [err24]; } @@ -1699,8 +1741,8 @@ vErrors.push(err24); } errors++; } -if(!(data10 === "richText")){ -const err25 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/1/properties/type/enum",keyword:"enum",params:{allowedValues: schema57.oneOf[1].properties.type.enum},message:"must be equal to one of the allowed values"}; +if(data.type === undefined){ +const err25 = {instancePath,schemaPath:"#/oneOf/2/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; if(vErrors === null){ vErrors = [err25]; } @@ -1709,16 +1751,8 @@ vErrors.push(err25); } errors++; } -} -if(data.labels !== undefined){ -if(!(validate26(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.errors); -errors = vErrors.length; -} -} -if(data.localized !== undefined){ -if(typeof data.localized !== "boolean"){ -const err26 = {instancePath:instancePath+"/localized",schemaPath:"#/oneOf/1/properties/localized/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(data.labels === undefined){ +const err26 = {instancePath,schemaPath:"#/oneOf/2/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; if(vErrors === null){ vErrors = [err26]; } @@ -1727,10 +1761,9 @@ vErrors.push(err26); } errors++; } -} -if(data.requiredToPublish !== undefined){ -if(typeof data.requiredToPublish !== "boolean"){ -const err27 = {instancePath:instancePath+"/requiredToPublish",schemaPath:"#/oneOf/1/properties/requiredToPublish/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +for(const key2 in data){ +if(!(((((((key2 === "key") || (key2 === "type")) || (key2 === "labels")) || (key2 === "requiredToPublish")) || (key2 === "integer")) || (key2 === "minimum")) || (key2 === "maximum"))){ +const err27 = {instancePath,schemaPath:"#/oneOf/2/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key2},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err27]; } @@ -1740,15 +1773,11 @@ vErrors.push(err27); errors++; } } -if(data.migration !== undefined){ -if(!(validate28(data.migration, {instancePath:instancePath+"/migration",parentData:data,parentDataProperty:"migration",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate28.errors : vErrors.concat(validate28.errors); -errors = vErrors.length; -} -} -if(data.searchable !== undefined){ -if(typeof data.searchable !== "boolean"){ -const err28 = {instancePath:instancePath+"/searchable",schemaPath:"#/oneOf/1/properties/searchable/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(data.key !== undefined){ +let data10 = data.key; +if(typeof data10 === "string"){ +if(!pattern22.test(data10)){ +const err28 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; if(vErrors === null){ vErrors = [err28]; } @@ -1758,9 +1787,8 @@ vErrors.push(err28); errors++; } } -} else { -const err29 = {instancePath,schemaPath:"#/oneOf/1/type",keyword:"type",params:{type: "object"},message:"must be object"}; +const err29 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ vErrors = [err29]; } @@ -1769,23 +1797,11 @@ vErrors.push(err29); } errors++; } -var _valid0 = _errs21 === errors; -if(_valid0 && valid0){ -valid0 = false; -passing0 = [passing0, 1]; -} -else { -if(_valid0){ -valid0 = true; -passing0 = 1; -if(props0 !== true){ -props0 = true; -} } -const _errs37 = errors; -if(data && typeof data == "object" && !Array.isArray(data)){ -if(data.key === undefined){ -const err30 = {instancePath,schemaPath:"#/oneOf/2/required",keyword:"required",params:{missingProperty: "key"},message:"must have required property '"+"key"+"'"}; +if(data.type !== undefined){ +let data11 = data.type; +if(typeof data11 !== "string"){ +const err30 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/2/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ vErrors = [err30]; } @@ -1794,8 +1810,8 @@ vErrors.push(err30); } errors++; } -if(data.type === undefined){ -const err31 = {instancePath,schemaPath:"#/oneOf/2/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; +if(!(data11 === "number")){ +const err31 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/2/properties/type/enum",keyword:"enum",params:{allowedValues: schema81.oneOf[2].properties.type.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err31]; } @@ -1804,8 +1820,16 @@ vErrors.push(err31); } errors++; } -if(data.labels === undefined){ -const err32 = {instancePath,schemaPath:"#/oneOf/2/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; +} +if(data.labels !== undefined){ +if(!(validate26(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.errors); +errors = vErrors.length; +} +} +if(data.requiredToPublish !== undefined){ +if(typeof data.requiredToPublish !== "boolean"){ +const err32 = {instancePath:instancePath+"/requiredToPublish",schemaPath:"#/oneOf/2/properties/requiredToPublish/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; if(vErrors === null){ vErrors = [err32]; } @@ -1814,8 +1838,10 @@ vErrors.push(err32); } errors++; } -if(data.localized === undefined){ -const err33 = {instancePath,schemaPath:"#/oneOf/2/required",keyword:"required",params:{missingProperty: "localized"},message:"must have required property '"+"localized"+"'"}; +} +if(data.integer !== undefined){ +if(typeof data.integer !== "boolean"){ +const err33 = {instancePath:instancePath+"/integer",schemaPath:"#/oneOf/2/properties/integer/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; if(vErrors === null){ vErrors = [err33]; } @@ -1824,9 +1850,11 @@ vErrors.push(err33); } errors++; } -for(const key2 in data){ -if(!(func1.call(schema57.oneOf[2].properties, key2))){ -const err34 = {instancePath,schemaPath:"#/oneOf/2/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key2},message:"must NOT have additional properties"}; +} +if(data.minimum !== undefined){ +let data15 = data.minimum; +if(!((typeof data15 == "number") && (isFinite(data15)))){ +const err34 = {instancePath:instancePath+"/minimum",schemaPath:"#/oneOf/2/properties/minimum/type",keyword:"type",params:{type: "number"},message:"must be number"}; if(vErrors === null){ vErrors = [err34]; } @@ -1836,11 +1864,10 @@ vErrors.push(err34); errors++; } } -if(data.key !== undefined){ -let data16 = data.key; -if(typeof data16 === "string"){ -if(!pattern22.test(data16)){ -const err35 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; +if(data.maximum !== undefined){ +let data16 = data.maximum; +if(!((typeof data16 == "number") && (isFinite(data16)))){ +const err35 = {instancePath:instancePath+"/maximum",schemaPath:"#/oneOf/2/properties/maximum/type",keyword:"type",params:{type: "number"},message:"must be number"}; if(vErrors === null){ vErrors = [err35]; } @@ -1850,8 +1877,9 @@ vErrors.push(err35); errors++; } } +} else { -const err36 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; +const err36 = {instancePath,schemaPath:"#/oneOf/2/type",keyword:"type",params:{type: "object"},message:"must be object"}; if(vErrors === null){ vErrors = [err36]; } @@ -1860,11 +1888,23 @@ vErrors.push(err36); } errors++; } +var _valid0 = _errs27 === errors; +if(_valid0 && valid0){ +valid0 = false; +passing0 = [passing0, 2]; +} +else { +if(_valid0){ +valid0 = true; +passing0 = 2; +if(props0 !== true){ +props0 = true; } -if(data.type !== undefined){ -let data17 = data.type; -if(typeof data17 !== "string"){ -const err37 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/2/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +} +const _errs44 = errors; +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.key === undefined){ +const err37 = {instancePath,schemaPath:"#/oneOf/3/required",keyword:"required",params:{missingProperty: "key"},message:"must have required property '"+"key"+"'"}; if(vErrors === null){ vErrors = [err37]; } @@ -1873,8 +1913,8 @@ vErrors.push(err37); } errors++; } -if(!(data17 === "number")){ -const err38 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/2/properties/type/enum",keyword:"enum",params:{allowedValues: schema57.oneOf[2].properties.type.enum},message:"must be equal to one of the allowed values"}; +if(data.type === undefined){ +const err38 = {instancePath,schemaPath:"#/oneOf/3/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; if(vErrors === null){ vErrors = [err38]; } @@ -1883,16 +1923,8 @@ vErrors.push(err38); } errors++; } -} -if(data.labels !== undefined){ -if(!(validate26(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.errors); -errors = vErrors.length; -} -} -if(data.localized !== undefined){ -if(typeof data.localized !== "boolean"){ -const err39 = {instancePath:instancePath+"/localized",schemaPath:"#/oneOf/2/properties/localized/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(data.labels === undefined){ +const err39 = {instancePath,schemaPath:"#/oneOf/3/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; if(vErrors === null){ vErrors = [err39]; } @@ -1901,10 +1933,9 @@ vErrors.push(err39); } errors++; } -} -if(data.requiredToPublish !== undefined){ -if(typeof data.requiredToPublish !== "boolean"){ -const err40 = {instancePath:instancePath+"/requiredToPublish",schemaPath:"#/oneOf/2/properties/requiredToPublish/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +for(const key3 in data){ +if(!(((key3 === "key") || (key3 === "type")) || (key3 === "labels"))){ +const err40 = {instancePath,schemaPath:"#/oneOf/3/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key3},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err40]; } @@ -1914,15 +1945,11 @@ vErrors.push(err40); errors++; } } -if(data.migration !== undefined){ -if(!(validate28(data.migration, {instancePath:instancePath+"/migration",parentData:data,parentDataProperty:"migration",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate28.errors : vErrors.concat(validate28.errors); -errors = vErrors.length; -} -} -if(data.integer !== undefined){ -if(typeof data.integer !== "boolean"){ -const err41 = {instancePath:instancePath+"/integer",schemaPath:"#/oneOf/2/properties/integer/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(data.key !== undefined){ +let data17 = data.key; +if(typeof data17 === "string"){ +if(!pattern22.test(data17)){ +const err41 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; if(vErrors === null){ vErrors = [err41]; } @@ -1932,10 +1959,8 @@ vErrors.push(err41); errors++; } } -if(data.minimum !== undefined){ -let data23 = data.minimum; -if(!((typeof data23 == "number") && (isFinite(data23)))){ -const err42 = {instancePath:instancePath+"/minimum",schemaPath:"#/oneOf/2/properties/minimum/type",keyword:"type",params:{type: "number"},message:"must be number"}; +else { +const err42 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ vErrors = [err42]; } @@ -1945,10 +1970,10 @@ vErrors.push(err42); errors++; } } -if(data.maximum !== undefined){ -let data24 = data.maximum; -if(!((typeof data24 == "number") && (isFinite(data24)))){ -const err43 = {instancePath:instancePath+"/maximum",schemaPath:"#/oneOf/2/properties/maximum/type",keyword:"type",params:{type: "number"},message:"must be number"}; +if(data.type !== undefined){ +let data18 = data.type; +if(typeof data18 !== "string"){ +const err43 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/3/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ vErrors = [err43]; } @@ -1957,45 +1982,51 @@ vErrors.push(err43); } errors++; } +if(!(data18 === "boolean")){ +const err44 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/3/properties/type/enum",keyword:"enum",params:{allowedValues: schema81.oneOf[3].properties.type.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err44]; +} +else { +vErrors.push(err44); +} +errors++; +} +} +if(data.labels !== undefined){ +if(!(validate26(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.errors); +errors = vErrors.length; +} } } else { -const err44 = {instancePath,schemaPath:"#/oneOf/2/type",keyword:"type",params:{type: "object"},message:"must be object"}; +const err45 = {instancePath,schemaPath:"#/oneOf/3/type",keyword:"type",params:{type: "object"},message:"must be object"}; if(vErrors === null){ -vErrors = [err44]; +vErrors = [err45]; } else { -vErrors.push(err44); +vErrors.push(err45); } errors++; } -var _valid0 = _errs37 === errors; +var _valid0 = _errs44 === errors; if(_valid0 && valid0){ valid0 = false; -passing0 = [passing0, 2]; +passing0 = [passing0, 3]; } else { if(_valid0){ valid0 = true; -passing0 = 2; +passing0 = 3; if(props0 !== true){ props0 = true; } } -const _errs57 = errors; +const _errs53 = errors; if(data && typeof data == "object" && !Array.isArray(data)){ if(data.key === undefined){ -const err45 = {instancePath,schemaPath:"#/oneOf/3/required",keyword:"required",params:{missingProperty: "key"},message:"must have required property '"+"key"+"'"}; -if(vErrors === null){ -vErrors = [err45]; -} -else { -vErrors.push(err45); -} -errors++; -} -if(data.type === undefined){ -const err46 = {instancePath,schemaPath:"#/oneOf/3/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; +const err46 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "key"},message:"must have required property '"+"key"+"'"}; if(vErrors === null){ vErrors = [err46]; } @@ -2004,8 +2035,8 @@ vErrors.push(err46); } errors++; } -if(data.labels === undefined){ -const err47 = {instancePath,schemaPath:"#/oneOf/3/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; +if(data.type === undefined){ +const err47 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; if(vErrors === null){ vErrors = [err47]; } @@ -2014,8 +2045,8 @@ vErrors.push(err47); } errors++; } -if(data.localized === undefined){ -const err48 = {instancePath,schemaPath:"#/oneOf/3/required",keyword:"required",params:{missingProperty: "localized"},message:"must have required property '"+"localized"+"'"}; +if(data.labels === undefined){ +const err48 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; if(vErrors === null){ vErrors = [err48]; } @@ -2024,9 +2055,9 @@ vErrors.push(err48); } errors++; } -for(const key3 in data){ -if(!(((((key3 === "key") || (key3 === "type")) || (key3 === "labels")) || (key3 === "localized")) || (key3 === "migration"))){ -const err49 = {instancePath,schemaPath:"#/oneOf/3/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key3},message:"must NOT have additional properties"}; +for(const key4 in data){ +if(!((((key4 === "key") || (key4 === "type")) || (key4 === "labels")) || (key4 === "requiredToPublish"))){ +const err49 = {instancePath,schemaPath:"#/oneOf/4/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key4},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err49]; } @@ -2037,9 +2068,9 @@ errors++; } } if(data.key !== undefined){ -let data25 = data.key; -if(typeof data25 === "string"){ -if(!pattern22.test(data25)){ +let data20 = data.key; +if(typeof data20 === "string"){ +if(!pattern22.test(data20)){ const err50 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; if(vErrors === null){ vErrors = [err50]; @@ -2062,9 +2093,9 @@ errors++; } } if(data.type !== undefined){ -let data26 = data.type; -if(typeof data26 !== "string"){ -const err52 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/3/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +let data21 = data.type; +if(typeof data21 !== "string"){ +const err52 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/4/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ vErrors = [err52]; } @@ -2073,8 +2104,8 @@ vErrors.push(err52); } errors++; } -if(!(data26 === "boolean")){ -const err53 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/3/properties/type/enum",keyword:"enum",params:{allowedValues: schema57.oneOf[3].properties.type.enum},message:"must be equal to one of the allowed values"}; +if(!(data21 === "date")){ +const err53 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/4/properties/type/enum",keyword:"enum",params:{allowedValues: schema81.oneOf[4].properties.type.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err53]; } @@ -2090,9 +2121,9 @@ vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.error errors = vErrors.length; } } -if(data.localized !== undefined){ -if(typeof data.localized !== "boolean"){ -const err54 = {instancePath:instancePath+"/localized",schemaPath:"#/oneOf/3/properties/localized/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(data.requiredToPublish !== undefined){ +if(typeof data.requiredToPublish !== "boolean"){ +const err54 = {instancePath:instancePath+"/requiredToPublish",schemaPath:"#/oneOf/4/properties/requiredToPublish/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; if(vErrors === null){ vErrors = [err54]; } @@ -2102,15 +2133,9 @@ vErrors.push(err54); errors++; } } -if(data.migration !== undefined){ -if(!(validate28(data.migration, {instancePath:instancePath+"/migration",parentData:data,parentDataProperty:"migration",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate28.errors : vErrors.concat(validate28.errors); -errors = vErrors.length; -} -} } else { -const err55 = {instancePath,schemaPath:"#/oneOf/3/type",keyword:"type",params:{type: "object"},message:"must be object"}; +const err55 = {instancePath,schemaPath:"#/oneOf/4/type",keyword:"type",params:{type: "object"},message:"must be object"}; if(vErrors === null){ vErrors = [err55]; } @@ -2119,23 +2144,23 @@ vErrors.push(err55); } errors++; } -var _valid0 = _errs57 === errors; +var _valid0 = _errs53 === errors; if(_valid0 && valid0){ valid0 = false; -passing0 = [passing0, 3]; +passing0 = [passing0, 4]; } else { if(_valid0){ valid0 = true; -passing0 = 3; +passing0 = 4; if(props0 !== true){ props0 = true; } } -const _errs69 = errors; +const _errs64 = errors; if(data && typeof data == "object" && !Array.isArray(data)){ if(data.key === undefined){ -const err56 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "key"},message:"must have required property '"+"key"+"'"}; +const err56 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "key"},message:"must have required property '"+"key"+"'"}; if(vErrors === null){ vErrors = [err56]; } @@ -2145,7 +2170,7 @@ vErrors.push(err56); errors++; } if(data.type === undefined){ -const err57 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; +const err57 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; if(vErrors === null){ vErrors = [err57]; } @@ -2155,7 +2180,7 @@ vErrors.push(err57); errors++; } if(data.labels === undefined){ -const err58 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; +const err58 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; if(vErrors === null){ vErrors = [err58]; } @@ -2164,8 +2189,9 @@ vErrors.push(err58); } errors++; } -if(data.localized === undefined){ -const err59 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "localized"},message:"must have required property '"+"localized"+"'"}; +for(const key5 in data){ +if(!((((key5 === "key") || (key5 === "type")) || (key5 === "labels")) || (key5 === "requiredToPublish"))){ +const err59 = {instancePath,schemaPath:"#/oneOf/5/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key5},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err59]; } @@ -2174,9 +2200,12 @@ vErrors.push(err59); } errors++; } -for(const key4 in data){ -if(!((((((key4 === "key") || (key4 === "type")) || (key4 === "labels")) || (key4 === "localized")) || (key4 === "requiredToPublish")) || (key4 === "migration"))){ -const err60 = {instancePath,schemaPath:"#/oneOf/4/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key4},message:"must NOT have additional properties"}; +} +if(data.key !== undefined){ +let data24 = data.key; +if(typeof data24 === "string"){ +if(!pattern22.test(data24)){ +const err60 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; if(vErrors === null){ vErrors = [err60]; } @@ -2186,11 +2215,8 @@ vErrors.push(err60); errors++; } } -if(data.key !== undefined){ -let data30 = data.key; -if(typeof data30 === "string"){ -if(!pattern22.test(data30)){ -const err61 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; +else { +const err61 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ vErrors = [err61]; } @@ -2200,8 +2226,10 @@ vErrors.push(err61); errors++; } } -else { -const err62 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(data.type !== undefined){ +let data25 = data.type; +if(typeof data25 !== "string"){ +const err62 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/5/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ vErrors = [err62]; } @@ -2210,11 +2238,8 @@ vErrors.push(err62); } errors++; } -} -if(data.type !== undefined){ -let data31 = data.type; -if(typeof data31 !== "string"){ -const err63 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/4/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(!(data25 === "url")){ +const err63 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/5/properties/type/enum",keyword:"enum",params:{allowedValues: schema81.oneOf[5].properties.type.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err63]; } @@ -2223,8 +2248,16 @@ vErrors.push(err63); } errors++; } -if(!(data31 === "date")){ -const err64 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/4/properties/type/enum",keyword:"enum",params:{allowedValues: schema57.oneOf[4].properties.type.enum},message:"must be equal to one of the allowed values"}; +} +if(data.labels !== undefined){ +if(!(validate26(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.errors); +errors = vErrors.length; +} +} +if(data.requiredToPublish !== undefined){ +if(typeof data.requiredToPublish !== "boolean"){ +const err64 = {instancePath:instancePath+"/requiredToPublish",schemaPath:"#/oneOf/5/properties/requiredToPublish/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; if(vErrors === null){ vErrors = [err64]; } @@ -2234,164 +2267,2364 @@ vErrors.push(err64); errors++; } } +} +else { +const err65 = {instancePath,schemaPath:"#/oneOf/5/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err65]; +} +else { +vErrors.push(err65); +} +errors++; +} +var _valid0 = _errs64 === errors; +if(_valid0 && valid0){ +valid0 = false; +passing0 = [passing0, 5]; +} +else { +if(_valid0){ +valid0 = true; +passing0 = 5; +if(props0 !== true){ +props0 = true; +} +} +const _errs75 = errors; +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.key === undefined){ +const err66 = {instancePath,schemaPath:"#/oneOf/6/required",keyword:"required",params:{missingProperty: "key"},message:"must have required property '"+"key"+"'"}; +if(vErrors === null){ +vErrors = [err66]; +} +else { +vErrors.push(err66); +} +errors++; +} +if(data.type === undefined){ +const err67 = {instancePath,schemaPath:"#/oneOf/6/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; +if(vErrors === null){ +vErrors = [err67]; +} +else { +vErrors.push(err67); +} +errors++; +} +if(data.labels === undefined){ +const err68 = {instancePath,schemaPath:"#/oneOf/6/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; +if(vErrors === null){ +vErrors = [err68]; +} +else { +vErrors.push(err68); +} +errors++; +} +for(const key6 in data){ +if(!((((key6 === "key") || (key6 === "type")) || (key6 === "labels")) || (key6 === "requiredToPublish"))){ +const err69 = {instancePath,schemaPath:"#/oneOf/6/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key6},message:"must NOT have additional properties"}; +if(vErrors === null){ +vErrors = [err69]; +} +else { +vErrors.push(err69); +} +errors++; +} +} +if(data.key !== undefined){ +let data28 = data.key; +if(typeof data28 === "string"){ +if(!pattern22.test(data28)){ +const err70 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; +if(vErrors === null){ +vErrors = [err70]; +} +else { +vErrors.push(err70); +} +errors++; +} +} +else { +const err71 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err71]; +} +else { +vErrors.push(err71); +} +errors++; +} +} +if(data.type !== undefined){ +let data29 = data.type; +if(typeof data29 !== "string"){ +const err72 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/6/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err72]; +} +else { +vErrors.push(err72); +} +errors++; +} +if(!(data29 === "image")){ +const err73 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/6/properties/type/enum",keyword:"enum",params:{allowedValues: schema81.oneOf[6].properties.type.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err73]; +} +else { +vErrors.push(err73); +} +errors++; +} +} if(data.labels !== undefined){ if(!(validate26(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.errors); errors = vErrors.length; } } -if(data.localized !== undefined){ -if(typeof data.localized !== "boolean"){ -const err65 = {instancePath:instancePath+"/localized",schemaPath:"#/oneOf/4/properties/localized/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(data.requiredToPublish !== undefined){ +if(typeof data.requiredToPublish !== "boolean"){ +const err74 = {instancePath:instancePath+"/requiredToPublish",schemaPath:"#/oneOf/6/properties/requiredToPublish/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; if(vErrors === null){ -vErrors = [err65]; +vErrors = [err74]; } else { -vErrors.push(err65); +vErrors.push(err74); +} +errors++; +} +} +} +else { +const err75 = {instancePath,schemaPath:"#/oneOf/6/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err75]; +} +else { +vErrors.push(err75); +} +errors++; +} +var _valid0 = _errs75 === errors; +if(_valid0 && valid0){ +valid0 = false; +passing0 = [passing0, 6]; +} +else { +if(_valid0){ +valid0 = true; +passing0 = 6; +if(props0 !== true){ +props0 = true; +} +} +const _errs86 = errors; +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.key === undefined){ +const err76 = {instancePath,schemaPath:"#/oneOf/7/required",keyword:"required",params:{missingProperty: "key"},message:"must have required property '"+"key"+"'"}; +if(vErrors === null){ +vErrors = [err76]; +} +else { +vErrors.push(err76); +} +errors++; +} +if(data.type === undefined){ +const err77 = {instancePath,schemaPath:"#/oneOf/7/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; +if(vErrors === null){ +vErrors = [err77]; +} +else { +vErrors.push(err77); +} +errors++; +} +if(data.labels === undefined){ +const err78 = {instancePath,schemaPath:"#/oneOf/7/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; +if(vErrors === null){ +vErrors = [err78]; +} +else { +vErrors.push(err78); +} +errors++; +} +if(data.options === undefined){ +const err79 = {instancePath,schemaPath:"#/oneOf/7/required",keyword:"required",params:{missingProperty: "options"},message:"must have required property '"+"options"+"'"}; +if(vErrors === null){ +vErrors = [err79]; +} +else { +vErrors.push(err79); +} +errors++; +} +for(const key7 in data){ +if(!(((((key7 === "key") || (key7 === "type")) || (key7 === "labels")) || (key7 === "requiredToPublish")) || (key7 === "options"))){ +const err80 = {instancePath,schemaPath:"#/oneOf/7/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key7},message:"must NOT have additional properties"}; +if(vErrors === null){ +vErrors = [err80]; +} +else { +vErrors.push(err80); +} +errors++; +} +} +if(data.key !== undefined){ +let data32 = data.key; +if(typeof data32 === "string"){ +if(!pattern22.test(data32)){ +const err81 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; +if(vErrors === null){ +vErrors = [err81]; +} +else { +vErrors.push(err81); +} +errors++; +} +} +else { +const err82 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err82]; +} +else { +vErrors.push(err82); +} +errors++; +} +} +if(data.type !== undefined){ +let data33 = data.type; +if(typeof data33 !== "string"){ +const err83 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/7/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err83]; +} +else { +vErrors.push(err83); +} +errors++; +} +if(!(data33 === "select")){ +const err84 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/7/properties/type/enum",keyword:"enum",params:{allowedValues: schema81.oneOf[7].properties.type.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err84]; +} +else { +vErrors.push(err84); } errors++; } } +if(data.labels !== undefined){ +if(!(validate26(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.errors); +errors = vErrors.length; +} +} if(data.requiredToPublish !== undefined){ if(typeof data.requiredToPublish !== "boolean"){ -const err66 = {instancePath:instancePath+"/requiredToPublish",schemaPath:"#/oneOf/4/properties/requiredToPublish/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +const err85 = {instancePath:instancePath+"/requiredToPublish",schemaPath:"#/oneOf/7/properties/requiredToPublish/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; if(vErrors === null){ -vErrors = [err66]; +vErrors = [err85]; } else { -vErrors.push(err66); +vErrors.push(err85); } errors++; } } -if(data.migration !== undefined){ -if(!(validate28(data.migration, {instancePath:instancePath+"/migration",parentData:data,parentDataProperty:"migration",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate28.errors : vErrors.concat(validate28.errors); +if(data.options !== undefined){ +let data36 = data.options; +if(Array.isArray(data36)){ +if(data36.length > 50){ +const err86 = {instancePath:instancePath+"/options",schemaPath:"#/oneOf/7/properties/options/maxItems",keyword:"maxItems",params:{limit: 50},message:"must NOT have more than 50 items"}; +if(vErrors === null){ +vErrors = [err86]; +} +else { +vErrors.push(err86); +} +errors++; +} +if(data36.length < 1){ +const err87 = {instancePath:instancePath+"/options",schemaPath:"#/oneOf/7/properties/options/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}; +if(vErrors === null){ +vErrors = [err87]; +} +else { +vErrors.push(err87); +} +errors++; +} +const len0 = data36.length; +for(let i0=0; i0 5){ +const err100 = {instancePath:instancePath+"/collections",schemaPath:"#/oneOf/8/properties/collections/maxItems",keyword:"maxItems",params:{limit: 5},message:"must NOT have more than 5 items"}; +if(vErrors === null){ +vErrors = [err100]; +} +else { +vErrors.push(err100); +} +errors++; +} +if(data42.length < 1){ +const err101 = {instancePath:instancePath+"/collections",schemaPath:"#/oneOf/8/properties/collections/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}; +if(vErrors === null){ +vErrors = [err101]; +} +else { +vErrors.push(err101); +} +errors++; +} +const len1 = data42.length; +for(let i1=0; i1 128){ +const err102 = {instancePath:instancePath+"/collections/" + i1,schemaPath:"#/$defs/Slug/maxLength",keyword:"maxLength",params:{limit: 128},message:"must NOT have more than 128 characters"}; +if(vErrors === null){ +vErrors = [err102]; +} +else { +vErrors.push(err102); +} +errors++; +} +if(func2(data43) < 1){ +const err103 = {instancePath:instancePath+"/collections/" + i1,schemaPath:"#/$defs/Slug/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}; +if(vErrors === null){ +vErrors = [err103]; +} +else { +vErrors.push(err103); +} +errors++; +} +if(!pattern5.test(data43)){ +const err104 = {instancePath:instancePath+"/collections/" + i1,schemaPath:"#/$defs/Slug/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}; +if(vErrors === null){ +vErrors = [err104]; +} +else { +vErrors.push(err104); +} +errors++; +} +} +else { +const err105 = {instancePath:instancePath+"/collections/" + i1,schemaPath:"#/$defs/Slug/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err105]; +} +else { +vErrors.push(err105); +} +errors++; +} +} +let i2 = data42.length; +let j0; +if(i2 > 1){ +outer0: +for(;i2--;){ +for(j0 = i2; j0--;){ +if(func0(data42[i2], data42[j0])){ +const err106 = {instancePath:instancePath+"/collections",schemaPath:"#/oneOf/8/properties/collections/uniqueItems",keyword:"uniqueItems",params:{i: i2, j: j0},message:"must NOT have duplicate items (items ## "+j0+" and "+i2+" are identical)"}; +if(vErrors === null){ +vErrors = [err106]; +} +else { +vErrors.push(err106); +} +errors++; +break outer0; +} +} +} +} +} +else { +const err107 = {instancePath:instancePath+"/collections",schemaPath:"#/oneOf/8/properties/collections/type",keyword:"type",params:{type: "array"},message:"must be array"}; +if(vErrors === null){ +vErrors = [err107]; +} +else { +vErrors.push(err107); +} +errors++; +} +} +} +else { +const err108 = {instancePath,schemaPath:"#/oneOf/8/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err108]; +} +else { +vErrors.push(err108); +} +errors++; +} +var _valid0 = _errs100 === errors; +if(_valid0 && valid0){ +valid0 = false; +passing0 = [passing0, 8]; +} +else { +if(_valid0){ +valid0 = true; +passing0 = 8; +if(props0 !== true){ +props0 = true; +} +} +} +} +} +} +} +} +} +} +if(!valid0){ +const err109 = {instancePath,schemaPath:"#/oneOf",keyword:"oneOf",params:{passingSchemas: passing0},message:"must match exactly one schema in oneOf"}; +if(vErrors === null){ +vErrors = [err109]; +} +else { +vErrors.push(err109); +} +errors++; +} +else { +errors = _errs0; +if(vErrors !== null){ +if(_errs0){ +vErrors.length = _errs0; +} +else { +vErrors = null; +} +} +} +validate52.errors = vErrors; +evaluated0.props = props0; +return errors === 0; +} +validate52.evaluated = {"dynamicProps":true,"dynamicItems":false}; + + +function validate25(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +let vErrors = null; +let errors = 0; +const evaluated0 = validate25.evaluated; +if(evaluated0.dynamicProps){ +evaluated0.props = undefined; +} +if(evaluated0.dynamicItems){ +evaluated0.items = undefined; +} +const _errs0 = errors; +let valid0 = false; +let passing0 = null; +const _errs1 = errors; +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.key === undefined){ +const err0 = {instancePath,schemaPath:"#/oneOf/0/required",keyword:"required",params:{missingProperty: "key"},message:"must have required property '"+"key"+"'"}; +if(vErrors === null){ +vErrors = [err0]; +} +else { +vErrors.push(err0); +} +errors++; +} +if(data.type === undefined){ +const err1 = {instancePath,schemaPath:"#/oneOf/0/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; +if(vErrors === null){ +vErrors = [err1]; +} +else { +vErrors.push(err1); +} +errors++; +} +if(data.labels === undefined){ +const err2 = {instancePath,schemaPath:"#/oneOf/0/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; +if(vErrors === null){ +vErrors = [err2]; +} +else { +vErrors.push(err2); +} +errors++; +} +if(data.localized === undefined){ +const err3 = {instancePath,schemaPath:"#/oneOf/0/required",keyword:"required",params:{missingProperty: "localized"},message:"must have required property '"+"localized"+"'"}; +if(vErrors === null){ +vErrors = [err3]; +} +else { +vErrors.push(err3); +} +errors++; +} +for(const key0 in data){ +if(!(func1.call(schema57.oneOf[0].properties, key0))){ +const err4 = {instancePath,schemaPath:"#/oneOf/0/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; +if(vErrors === null){ +vErrors = [err4]; +} +else { +vErrors.push(err4); +} +errors++; +} +} +if(data.key !== undefined){ +let data0 = data.key; +if(typeof data0 === "string"){ +if(!pattern22.test(data0)){ +const err5 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; +if(vErrors === null){ +vErrors = [err5]; +} +else { +vErrors.push(err5); +} +errors++; +} +} +else { +const err6 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err6]; +} +else { +vErrors.push(err6); +} +errors++; +} +} +if(data.type !== undefined){ +let data1 = data.type; +if(typeof data1 !== "string"){ +const err7 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/0/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err7]; +} +else { +vErrors.push(err7); +} +errors++; +} +if(!(data1 === "text")){ +const err8 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/0/properties/type/enum",keyword:"enum",params:{allowedValues: schema57.oneOf[0].properties.type.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err8]; +} +else { +vErrors.push(err8); +} +errors++; +} +} +if(data.labels !== undefined){ +if(!(validate26(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.errors); +errors = vErrors.length; +} +} +if(data.localized !== undefined){ +if(typeof data.localized !== "boolean"){ +const err9 = {instancePath:instancePath+"/localized",schemaPath:"#/oneOf/0/properties/localized/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(vErrors === null){ +vErrors = [err9]; +} +else { +vErrors.push(err9); +} +errors++; +} +} +if(data.requiredToPublish !== undefined){ +if(typeof data.requiredToPublish !== "boolean"){ +const err10 = {instancePath:instancePath+"/requiredToPublish",schemaPath:"#/oneOf/0/properties/requiredToPublish/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(vErrors === null){ +vErrors = [err10]; +} +else { +vErrors.push(err10); +} +errors++; +} +} +if(data.migration !== undefined){ +if(!(validate28(data.migration, {instancePath:instancePath+"/migration",parentData:data,parentDataProperty:"migration",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate28.errors : vErrors.concat(validate28.errors); +errors = vErrors.length; +} +} +if(data.maxLength !== undefined){ +let data6 = data.maxLength; +if(!(((typeof data6 == "number") && (!(data6 % 1) && !isNaN(data6))) && (isFinite(data6)))){ +const err11 = {instancePath:instancePath+"/maxLength",schemaPath:"#/oneOf/0/properties/maxLength/type",keyword:"type",params:{type: "integer"},message:"must be integer"}; +if(vErrors === null){ +vErrors = [err11]; +} +else { +vErrors.push(err11); +} +errors++; +} +if((typeof data6 == "number") && (isFinite(data6))){ +if(data6 > 5000 || isNaN(data6)){ +const err12 = {instancePath:instancePath+"/maxLength",schemaPath:"#/oneOf/0/properties/maxLength/maximum",keyword:"maximum",params:{comparison: "<=", limit: 5000},message:"must be <= 5000"}; +if(vErrors === null){ +vErrors = [err12]; +} +else { +vErrors.push(err12); +} +errors++; +} +if(data6 < 1 || isNaN(data6)){ +const err13 = {instancePath:instancePath+"/maxLength",schemaPath:"#/oneOf/0/properties/maxLength/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"}; +if(vErrors === null){ +vErrors = [err13]; +} +else { +vErrors.push(err13); +} +errors++; +} +} +} +if(data.multiline !== undefined){ +if(typeof data.multiline !== "boolean"){ +const err14 = {instancePath:instancePath+"/multiline",schemaPath:"#/oneOf/0/properties/multiline/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(vErrors === null){ +vErrors = [err14]; +} +else { +vErrors.push(err14); +} +errors++; +} +} +if(data.searchable !== undefined){ +if(typeof data.searchable !== "boolean"){ +const err15 = {instancePath:instancePath+"/searchable",schemaPath:"#/oneOf/0/properties/searchable/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(vErrors === null){ +vErrors = [err15]; +} +else { +vErrors.push(err15); +} +errors++; +} +} +} +else { +const err16 = {instancePath,schemaPath:"#/oneOf/0/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err16]; +} +else { +vErrors.push(err16); +} +errors++; +} +var _valid0 = _errs1 === errors; +if(_valid0){ +valid0 = true; +passing0 = 0; +var props0 = true; +} +const _errs21 = errors; +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.key === undefined){ +const err17 = {instancePath,schemaPath:"#/oneOf/1/required",keyword:"required",params:{missingProperty: "key"},message:"must have required property '"+"key"+"'"}; +if(vErrors === null){ +vErrors = [err17]; +} +else { +vErrors.push(err17); +} +errors++; +} +if(data.type === undefined){ +const err18 = {instancePath,schemaPath:"#/oneOf/1/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; +if(vErrors === null){ +vErrors = [err18]; +} +else { +vErrors.push(err18); +} +errors++; +} +if(data.labels === undefined){ +const err19 = {instancePath,schemaPath:"#/oneOf/1/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; +if(vErrors === null){ +vErrors = [err19]; +} +else { +vErrors.push(err19); +} +errors++; +} +if(data.localized === undefined){ +const err20 = {instancePath,schemaPath:"#/oneOf/1/required",keyword:"required",params:{missingProperty: "localized"},message:"must have required property '"+"localized"+"'"}; +if(vErrors === null){ +vErrors = [err20]; +} +else { +vErrors.push(err20); +} +errors++; +} +for(const key1 in data){ +if(!(((((((key1 === "key") || (key1 === "type")) || (key1 === "labels")) || (key1 === "localized")) || (key1 === "requiredToPublish")) || (key1 === "migration")) || (key1 === "searchable"))){ +const err21 = {instancePath,schemaPath:"#/oneOf/1/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key1},message:"must NOT have additional properties"}; +if(vErrors === null){ +vErrors = [err21]; +} +else { +vErrors.push(err21); +} +errors++; +} +} +if(data.key !== undefined){ +let data9 = data.key; +if(typeof data9 === "string"){ +if(!pattern22.test(data9)){ +const err22 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; +if(vErrors === null){ +vErrors = [err22]; +} +else { +vErrors.push(err22); +} +errors++; +} +} +else { +const err23 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err23]; +} +else { +vErrors.push(err23); +} +errors++; +} +} +if(data.type !== undefined){ +let data10 = data.type; +if(typeof data10 !== "string"){ +const err24 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/1/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err24]; +} +else { +vErrors.push(err24); +} +errors++; +} +if(!(data10 === "richText")){ +const err25 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/1/properties/type/enum",keyword:"enum",params:{allowedValues: schema57.oneOf[1].properties.type.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err25]; +} +else { +vErrors.push(err25); +} +errors++; +} +} +if(data.labels !== undefined){ +if(!(validate26(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.errors); +errors = vErrors.length; +} +} +if(data.localized !== undefined){ +if(typeof data.localized !== "boolean"){ +const err26 = {instancePath:instancePath+"/localized",schemaPath:"#/oneOf/1/properties/localized/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(vErrors === null){ +vErrors = [err26]; +} +else { +vErrors.push(err26); +} +errors++; +} +} +if(data.requiredToPublish !== undefined){ +if(typeof data.requiredToPublish !== "boolean"){ +const err27 = {instancePath:instancePath+"/requiredToPublish",schemaPath:"#/oneOf/1/properties/requiredToPublish/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(vErrors === null){ +vErrors = [err27]; +} +else { +vErrors.push(err27); +} +errors++; +} +} +if(data.migration !== undefined){ +if(!(validate28(data.migration, {instancePath:instancePath+"/migration",parentData:data,parentDataProperty:"migration",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate28.errors : vErrors.concat(validate28.errors); +errors = vErrors.length; +} +} +if(data.searchable !== undefined){ +if(typeof data.searchable !== "boolean"){ +const err28 = {instancePath:instancePath+"/searchable",schemaPath:"#/oneOf/1/properties/searchable/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(vErrors === null){ +vErrors = [err28]; +} +else { +vErrors.push(err28); +} +errors++; +} +} +} +else { +const err29 = {instancePath,schemaPath:"#/oneOf/1/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err29]; +} +else { +vErrors.push(err29); +} +errors++; +} +var _valid0 = _errs21 === errors; +if(_valid0 && valid0){ +valid0 = false; +passing0 = [passing0, 1]; +} +else { +if(_valid0){ +valid0 = true; +passing0 = 1; +if(props0 !== true){ +props0 = true; +} +} +const _errs37 = errors; +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.key === undefined){ +const err30 = {instancePath,schemaPath:"#/oneOf/2/required",keyword:"required",params:{missingProperty: "key"},message:"must have required property '"+"key"+"'"}; +if(vErrors === null){ +vErrors = [err30]; +} +else { +vErrors.push(err30); +} +errors++; +} +if(data.type === undefined){ +const err31 = {instancePath,schemaPath:"#/oneOf/2/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; +if(vErrors === null){ +vErrors = [err31]; +} +else { +vErrors.push(err31); +} +errors++; +} +if(data.labels === undefined){ +const err32 = {instancePath,schemaPath:"#/oneOf/2/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; +if(vErrors === null){ +vErrors = [err32]; +} +else { +vErrors.push(err32); +} +errors++; +} +if(data.localized === undefined){ +const err33 = {instancePath,schemaPath:"#/oneOf/2/required",keyword:"required",params:{missingProperty: "localized"},message:"must have required property '"+"localized"+"'"}; +if(vErrors === null){ +vErrors = [err33]; +} +else { +vErrors.push(err33); +} +errors++; +} +for(const key2 in data){ +if(!(func1.call(schema57.oneOf[2].properties, key2))){ +const err34 = {instancePath,schemaPath:"#/oneOf/2/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key2},message:"must NOT have additional properties"}; +if(vErrors === null){ +vErrors = [err34]; +} +else { +vErrors.push(err34); +} +errors++; +} +} +if(data.key !== undefined){ +let data16 = data.key; +if(typeof data16 === "string"){ +if(!pattern22.test(data16)){ +const err35 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; +if(vErrors === null){ +vErrors = [err35]; +} +else { +vErrors.push(err35); +} +errors++; +} +} +else { +const err36 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err36]; +} +else { +vErrors.push(err36); +} +errors++; +} +} +if(data.type !== undefined){ +let data17 = data.type; +if(typeof data17 !== "string"){ +const err37 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/2/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err37]; +} +else { +vErrors.push(err37); +} +errors++; +} +if(!(data17 === "number")){ +const err38 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/2/properties/type/enum",keyword:"enum",params:{allowedValues: schema57.oneOf[2].properties.type.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err38]; +} +else { +vErrors.push(err38); +} +errors++; +} +} +if(data.labels !== undefined){ +if(!(validate26(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.errors); +errors = vErrors.length; +} +} +if(data.localized !== undefined){ +if(typeof data.localized !== "boolean"){ +const err39 = {instancePath:instancePath+"/localized",schemaPath:"#/oneOf/2/properties/localized/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(vErrors === null){ +vErrors = [err39]; +} +else { +vErrors.push(err39); +} +errors++; +} +} +if(data.requiredToPublish !== undefined){ +if(typeof data.requiredToPublish !== "boolean"){ +const err40 = {instancePath:instancePath+"/requiredToPublish",schemaPath:"#/oneOf/2/properties/requiredToPublish/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(vErrors === null){ +vErrors = [err40]; +} +else { +vErrors.push(err40); +} +errors++; +} +} +if(data.migration !== undefined){ +if(!(validate28(data.migration, {instancePath:instancePath+"/migration",parentData:data,parentDataProperty:"migration",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate28.errors : vErrors.concat(validate28.errors); +errors = vErrors.length; +} +} +if(data.integer !== undefined){ +if(typeof data.integer !== "boolean"){ +const err41 = {instancePath:instancePath+"/integer",schemaPath:"#/oneOf/2/properties/integer/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(vErrors === null){ +vErrors = [err41]; +} +else { +vErrors.push(err41); +} +errors++; +} +} +if(data.minimum !== undefined){ +let data23 = data.minimum; +if(!((typeof data23 == "number") && (isFinite(data23)))){ +const err42 = {instancePath:instancePath+"/minimum",schemaPath:"#/oneOf/2/properties/minimum/type",keyword:"type",params:{type: "number"},message:"must be number"}; +if(vErrors === null){ +vErrors = [err42]; +} +else { +vErrors.push(err42); +} +errors++; +} +} +if(data.maximum !== undefined){ +let data24 = data.maximum; +if(!((typeof data24 == "number") && (isFinite(data24)))){ +const err43 = {instancePath:instancePath+"/maximum",schemaPath:"#/oneOf/2/properties/maximum/type",keyword:"type",params:{type: "number"},message:"must be number"}; +if(vErrors === null){ +vErrors = [err43]; +} +else { +vErrors.push(err43); +} +errors++; +} +} +} +else { +const err44 = {instancePath,schemaPath:"#/oneOf/2/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err44]; +} +else { +vErrors.push(err44); +} +errors++; +} +var _valid0 = _errs37 === errors; +if(_valid0 && valid0){ +valid0 = false; +passing0 = [passing0, 2]; +} +else { +if(_valid0){ +valid0 = true; +passing0 = 2; +if(props0 !== true){ +props0 = true; +} +} +const _errs57 = errors; +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.key === undefined){ +const err45 = {instancePath,schemaPath:"#/oneOf/3/required",keyword:"required",params:{missingProperty: "key"},message:"must have required property '"+"key"+"'"}; +if(vErrors === null){ +vErrors = [err45]; +} +else { +vErrors.push(err45); +} +errors++; +} +if(data.type === undefined){ +const err46 = {instancePath,schemaPath:"#/oneOf/3/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; +if(vErrors === null){ +vErrors = [err46]; +} +else { +vErrors.push(err46); +} +errors++; +} +if(data.labels === undefined){ +const err47 = {instancePath,schemaPath:"#/oneOf/3/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; +if(vErrors === null){ +vErrors = [err47]; +} +else { +vErrors.push(err47); +} +errors++; +} +if(data.localized === undefined){ +const err48 = {instancePath,schemaPath:"#/oneOf/3/required",keyword:"required",params:{missingProperty: "localized"},message:"must have required property '"+"localized"+"'"}; +if(vErrors === null){ +vErrors = [err48]; +} +else { +vErrors.push(err48); +} +errors++; +} +for(const key3 in data){ +if(!(((((key3 === "key") || (key3 === "type")) || (key3 === "labels")) || (key3 === "localized")) || (key3 === "migration"))){ +const err49 = {instancePath,schemaPath:"#/oneOf/3/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key3},message:"must NOT have additional properties"}; +if(vErrors === null){ +vErrors = [err49]; +} +else { +vErrors.push(err49); +} +errors++; +} +} +if(data.key !== undefined){ +let data25 = data.key; +if(typeof data25 === "string"){ +if(!pattern22.test(data25)){ +const err50 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; +if(vErrors === null){ +vErrors = [err50]; +} +else { +vErrors.push(err50); +} +errors++; +} +} +else { +const err51 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err51]; +} +else { +vErrors.push(err51); +} +errors++; +} +} +if(data.type !== undefined){ +let data26 = data.type; +if(typeof data26 !== "string"){ +const err52 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/3/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err52]; +} +else { +vErrors.push(err52); +} +errors++; +} +if(!(data26 === "boolean")){ +const err53 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/3/properties/type/enum",keyword:"enum",params:{allowedValues: schema57.oneOf[3].properties.type.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err53]; +} +else { +vErrors.push(err53); +} +errors++; +} +} +if(data.labels !== undefined){ +if(!(validate26(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.errors); +errors = vErrors.length; +} +} +if(data.localized !== undefined){ +if(typeof data.localized !== "boolean"){ +const err54 = {instancePath:instancePath+"/localized",schemaPath:"#/oneOf/3/properties/localized/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(vErrors === null){ +vErrors = [err54]; +} +else { +vErrors.push(err54); +} +errors++; +} +} +if(data.migration !== undefined){ +if(!(validate28(data.migration, {instancePath:instancePath+"/migration",parentData:data,parentDataProperty:"migration",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate28.errors : vErrors.concat(validate28.errors); +errors = vErrors.length; +} +} +} +else { +const err55 = {instancePath,schemaPath:"#/oneOf/3/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err55]; +} +else { +vErrors.push(err55); +} +errors++; +} +var _valid0 = _errs57 === errors; +if(_valid0 && valid0){ +valid0 = false; +passing0 = [passing0, 3]; +} +else { +if(_valid0){ +valid0 = true; +passing0 = 3; +if(props0 !== true){ +props0 = true; +} +} +const _errs69 = errors; +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.key === undefined){ +const err56 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "key"},message:"must have required property '"+"key"+"'"}; +if(vErrors === null){ +vErrors = [err56]; +} +else { +vErrors.push(err56); +} +errors++; +} +if(data.type === undefined){ +const err57 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; +if(vErrors === null){ +vErrors = [err57]; +} +else { +vErrors.push(err57); +} +errors++; +} +if(data.labels === undefined){ +const err58 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; +if(vErrors === null){ +vErrors = [err58]; +} +else { +vErrors.push(err58); +} +errors++; +} +if(data.localized === undefined){ +const err59 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "localized"},message:"must have required property '"+"localized"+"'"}; +if(vErrors === null){ +vErrors = [err59]; +} +else { +vErrors.push(err59); +} +errors++; +} +for(const key4 in data){ +if(!((((((key4 === "key") || (key4 === "type")) || (key4 === "labels")) || (key4 === "localized")) || (key4 === "requiredToPublish")) || (key4 === "migration"))){ +const err60 = {instancePath,schemaPath:"#/oneOf/4/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key4},message:"must NOT have additional properties"}; +if(vErrors === null){ +vErrors = [err60]; +} +else { +vErrors.push(err60); +} +errors++; +} +} +if(data.key !== undefined){ +let data30 = data.key; +if(typeof data30 === "string"){ +if(!pattern22.test(data30)){ +const err61 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; +if(vErrors === null){ +vErrors = [err61]; +} +else { +vErrors.push(err61); +} +errors++; +} +} +else { +const err62 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err62]; +} +else { +vErrors.push(err62); +} +errors++; +} +} +if(data.type !== undefined){ +let data31 = data.type; +if(typeof data31 !== "string"){ +const err63 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/4/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err63]; +} +else { +vErrors.push(err63); +} +errors++; +} +if(!(data31 === "date")){ +const err64 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/4/properties/type/enum",keyword:"enum",params:{allowedValues: schema57.oneOf[4].properties.type.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err64]; +} +else { +vErrors.push(err64); +} +errors++; +} +} +if(data.labels !== undefined){ +if(!(validate26(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.errors); +errors = vErrors.length; +} +} +if(data.localized !== undefined){ +if(typeof data.localized !== "boolean"){ +const err65 = {instancePath:instancePath+"/localized",schemaPath:"#/oneOf/4/properties/localized/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(vErrors === null){ +vErrors = [err65]; +} +else { +vErrors.push(err65); +} +errors++; +} +} +if(data.requiredToPublish !== undefined){ +if(typeof data.requiredToPublish !== "boolean"){ +const err66 = {instancePath:instancePath+"/requiredToPublish",schemaPath:"#/oneOf/4/properties/requiredToPublish/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(vErrors === null){ +vErrors = [err66]; +} +else { +vErrors.push(err66); +} +errors++; +} +} +if(data.migration !== undefined){ +if(!(validate28(data.migration, {instancePath:instancePath+"/migration",parentData:data,parentDataProperty:"migration",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate28.errors : vErrors.concat(validate28.errors); +errors = vErrors.length; +} +} +} +else { +const err67 = {instancePath,schemaPath:"#/oneOf/4/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err67]; +} +else { +vErrors.push(err67); +} +errors++; +} +var _valid0 = _errs69 === errors; +if(_valid0 && valid0){ +valid0 = false; +passing0 = [passing0, 4]; +} +else { +if(_valid0){ +valid0 = true; +passing0 = 4; +if(props0 !== true){ +props0 = true; +} +} +const _errs83 = errors; +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.key === undefined){ +const err68 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "key"},message:"must have required property '"+"key"+"'"}; +if(vErrors === null){ +vErrors = [err68]; +} +else { +vErrors.push(err68); +} +errors++; +} +if(data.type === undefined){ +const err69 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; +if(vErrors === null){ +vErrors = [err69]; +} +else { +vErrors.push(err69); +} +errors++; +} +if(data.labels === undefined){ +const err70 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; +if(vErrors === null){ +vErrors = [err70]; +} +else { +vErrors.push(err70); +} +errors++; +} +if(data.localized === undefined){ +const err71 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "localized"},message:"must have required property '"+"localized"+"'"}; +if(vErrors === null){ +vErrors = [err71]; +} +else { +vErrors.push(err71); +} +errors++; +} +for(const key5 in data){ +if(!((((((key5 === "key") || (key5 === "type")) || (key5 === "labels")) || (key5 === "localized")) || (key5 === "requiredToPublish")) || (key5 === "migration"))){ +const err72 = {instancePath,schemaPath:"#/oneOf/5/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key5},message:"must NOT have additional properties"}; +if(vErrors === null){ +vErrors = [err72]; +} +else { +vErrors.push(err72); +} +errors++; +} +} +if(data.key !== undefined){ +let data36 = data.key; +if(typeof data36 === "string"){ +if(!pattern22.test(data36)){ +const err73 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; +if(vErrors === null){ +vErrors = [err73]; +} +else { +vErrors.push(err73); +} +errors++; +} +} +else { +const err74 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err74]; +} +else { +vErrors.push(err74); +} +errors++; +} +} +if(data.type !== undefined){ +let data37 = data.type; +if(typeof data37 !== "string"){ +const err75 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/5/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err75]; +} +else { +vErrors.push(err75); +} +errors++; +} +if(!(data37 === "datetime")){ +const err76 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/5/properties/type/enum",keyword:"enum",params:{allowedValues: schema57.oneOf[5].properties.type.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err76]; +} +else { +vErrors.push(err76); +} +errors++; +} +} +if(data.labels !== undefined){ +if(!(validate26(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.errors); +errors = vErrors.length; +} +} +if(data.localized !== undefined){ +if(typeof data.localized !== "boolean"){ +const err77 = {instancePath:instancePath+"/localized",schemaPath:"#/oneOf/5/properties/localized/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(vErrors === null){ +vErrors = [err77]; +} +else { +vErrors.push(err77); +} +errors++; +} +} +if(data.requiredToPublish !== undefined){ +if(typeof data.requiredToPublish !== "boolean"){ +const err78 = {instancePath:instancePath+"/requiredToPublish",schemaPath:"#/oneOf/5/properties/requiredToPublish/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(vErrors === null){ +vErrors = [err78]; +} +else { +vErrors.push(err78); +} +errors++; +} +} +if(data.migration !== undefined){ +if(!(validate28(data.migration, {instancePath:instancePath+"/migration",parentData:data,parentDataProperty:"migration",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate28.errors : vErrors.concat(validate28.errors); +errors = vErrors.length; +} +} +} +else { +const err79 = {instancePath,schemaPath:"#/oneOf/5/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err79]; +} +else { +vErrors.push(err79); +} +errors++; +} +var _valid0 = _errs83 === errors; +if(_valid0 && valid0){ +valid0 = false; +passing0 = [passing0, 5]; +} +else { +if(_valid0){ +valid0 = true; +passing0 = 5; +if(props0 !== true){ +props0 = true; +} +} +const _errs97 = errors; +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.key === undefined){ +const err80 = {instancePath,schemaPath:"#/oneOf/6/required",keyword:"required",params:{missingProperty: "key"},message:"must have required property '"+"key"+"'"}; +if(vErrors === null){ +vErrors = [err80]; +} +else { +vErrors.push(err80); +} +errors++; +} +if(data.type === undefined){ +const err81 = {instancePath,schemaPath:"#/oneOf/6/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; +if(vErrors === null){ +vErrors = [err81]; +} +else { +vErrors.push(err81); +} +errors++; +} +if(data.labels === undefined){ +const err82 = {instancePath,schemaPath:"#/oneOf/6/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; +if(vErrors === null){ +vErrors = [err82]; +} +else { +vErrors.push(err82); +} +errors++; +} +if(data.localized === undefined){ +const err83 = {instancePath,schemaPath:"#/oneOf/6/required",keyword:"required",params:{missingProperty: "localized"},message:"must have required property '"+"localized"+"'"}; +if(vErrors === null){ +vErrors = [err83]; +} +else { +vErrors.push(err83); +} +errors++; +} +for(const key6 in data){ +if(!((((((key6 === "key") || (key6 === "type")) || (key6 === "labels")) || (key6 === "localized")) || (key6 === "requiredToPublish")) || (key6 === "migration"))){ +const err84 = {instancePath,schemaPath:"#/oneOf/6/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key6},message:"must NOT have additional properties"}; +if(vErrors === null){ +vErrors = [err84]; +} +else { +vErrors.push(err84); +} +errors++; +} +} +if(data.key !== undefined){ +let data42 = data.key; +if(typeof data42 === "string"){ +if(!pattern22.test(data42)){ +const err85 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; +if(vErrors === null){ +vErrors = [err85]; +} +else { +vErrors.push(err85); +} +errors++; +} +} +else { +const err86 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err86]; +} +else { +vErrors.push(err86); +} +errors++; +} +} +if(data.type !== undefined){ +let data43 = data.type; +if(typeof data43 !== "string"){ +const err87 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/6/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err87]; +} +else { +vErrors.push(err87); +} +errors++; +} +if(!(data43 === "url")){ +const err88 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/6/properties/type/enum",keyword:"enum",params:{allowedValues: schema57.oneOf[6].properties.type.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err88]; +} +else { +vErrors.push(err88); +} +errors++; +} +} +if(data.labels !== undefined){ +if(!(validate26(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.errors); +errors = vErrors.length; +} +} +if(data.localized !== undefined){ +if(typeof data.localized !== "boolean"){ +const err89 = {instancePath:instancePath+"/localized",schemaPath:"#/oneOf/6/properties/localized/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(vErrors === null){ +vErrors = [err89]; +} +else { +vErrors.push(err89); +} +errors++; +} +} +if(data.requiredToPublish !== undefined){ +if(typeof data.requiredToPublish !== "boolean"){ +const err90 = {instancePath:instancePath+"/requiredToPublish",schemaPath:"#/oneOf/6/properties/requiredToPublish/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(vErrors === null){ +vErrors = [err90]; +} +else { +vErrors.push(err90); +} +errors++; +} +} +if(data.migration !== undefined){ +if(!(validate28(data.migration, {instancePath:instancePath+"/migration",parentData:data,parentDataProperty:"migration",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate28.errors : vErrors.concat(validate28.errors); +errors = vErrors.length; +} +} +} +else { +const err91 = {instancePath,schemaPath:"#/oneOf/6/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err91]; +} +else { +vErrors.push(err91); +} +errors++; +} +var _valid0 = _errs97 === errors; +if(_valid0 && valid0){ +valid0 = false; +passing0 = [passing0, 6]; +} +else { +if(_valid0){ +valid0 = true; +passing0 = 6; +if(props0 !== true){ +props0 = true; +} +} +const _errs111 = errors; +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.key === undefined){ +const err92 = {instancePath,schemaPath:"#/oneOf/7/required",keyword:"required",params:{missingProperty: "key"},message:"must have required property '"+"key"+"'"}; +if(vErrors === null){ +vErrors = [err92]; +} +else { +vErrors.push(err92); +} +errors++; +} +if(data.type === undefined){ +const err93 = {instancePath,schemaPath:"#/oneOf/7/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; +if(vErrors === null){ +vErrors = [err93]; +} +else { +vErrors.push(err93); +} +errors++; +} +if(data.labels === undefined){ +const err94 = {instancePath,schemaPath:"#/oneOf/7/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; +if(vErrors === null){ +vErrors = [err94]; +} +else { +vErrors.push(err94); +} +errors++; +} +if(data.localized === undefined){ +const err95 = {instancePath,schemaPath:"#/oneOf/7/required",keyword:"required",params:{missingProperty: "localized"},message:"must have required property '"+"localized"+"'"}; +if(vErrors === null){ +vErrors = [err95]; +} +else { +vErrors.push(err95); +} +errors++; +} +for(const key7 in data){ +if(!((((((key7 === "key") || (key7 === "type")) || (key7 === "labels")) || (key7 === "localized")) || (key7 === "requiredToPublish")) || (key7 === "migration"))){ +const err96 = {instancePath,schemaPath:"#/oneOf/7/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key7},message:"must NOT have additional properties"}; +if(vErrors === null){ +vErrors = [err96]; +} +else { +vErrors.push(err96); +} +errors++; +} +} +if(data.key !== undefined){ +let data48 = data.key; +if(typeof data48 === "string"){ +if(!pattern22.test(data48)){ +const err97 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; +if(vErrors === null){ +vErrors = [err97]; +} +else { +vErrors.push(err97); +} +errors++; +} +} +else { +const err98 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err98]; +} +else { +vErrors.push(err98); +} +errors++; +} +} +if(data.type !== undefined){ +let data49 = data.type; +if(typeof data49 !== "string"){ +const err99 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/7/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err99]; +} +else { +vErrors.push(err99); +} +errors++; +} +if(!(data49 === "image")){ +const err100 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/7/properties/type/enum",keyword:"enum",params:{allowedValues: schema57.oneOf[7].properties.type.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err100]; +} +else { +vErrors.push(err100); +} +errors++; +} +} +if(data.labels !== undefined){ +if(!(validate26(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.errors); +errors = vErrors.length; +} +} +if(data.localized !== undefined){ +if(typeof data.localized !== "boolean"){ +const err101 = {instancePath:instancePath+"/localized",schemaPath:"#/oneOf/7/properties/localized/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(vErrors === null){ +vErrors = [err101]; +} +else { +vErrors.push(err101); +} +errors++; +} +} +if(data.requiredToPublish !== undefined){ +if(typeof data.requiredToPublish !== "boolean"){ +const err102 = {instancePath:instancePath+"/requiredToPublish",schemaPath:"#/oneOf/7/properties/requiredToPublish/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(vErrors === null){ +vErrors = [err102]; +} +else { +vErrors.push(err102); +} +errors++; +} +} +if(data.migration !== undefined){ +if(!(validate28(data.migration, {instancePath:instancePath+"/migration",parentData:data,parentDataProperty:"migration",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate28.errors : vErrors.concat(validate28.errors); +errors = vErrors.length; +} +} +} +else { +const err103 = {instancePath,schemaPath:"#/oneOf/7/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err103]; +} +else { +vErrors.push(err103); +} +errors++; +} +var _valid0 = _errs111 === errors; +if(_valid0 && valid0){ +valid0 = false; +passing0 = [passing0, 7]; +} +else { +if(_valid0){ +valid0 = true; +passing0 = 7; +if(props0 !== true){ +props0 = true; +} +} +const _errs125 = errors; +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.key === undefined){ +const err104 = {instancePath,schemaPath:"#/oneOf/8/required",keyword:"required",params:{missingProperty: "key"},message:"must have required property '"+"key"+"'"}; +if(vErrors === null){ +vErrors = [err104]; +} +else { +vErrors.push(err104); +} +errors++; +} +if(data.type === undefined){ +const err105 = {instancePath,schemaPath:"#/oneOf/8/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; +if(vErrors === null){ +vErrors = [err105]; +} +else { +vErrors.push(err105); +} +errors++; +} +if(data.labels === undefined){ +const err106 = {instancePath,schemaPath:"#/oneOf/8/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; +if(vErrors === null){ +vErrors = [err106]; +} +else { +vErrors.push(err106); +} +errors++; +} +if(data.localized === undefined){ +const err107 = {instancePath,schemaPath:"#/oneOf/8/required",keyword:"required",params:{missingProperty: "localized"},message:"must have required property '"+"localized"+"'"}; +if(vErrors === null){ +vErrors = [err107]; +} +else { +vErrors.push(err107); +} +errors++; +} +if(data.options === undefined){ +const err108 = {instancePath,schemaPath:"#/oneOf/8/required",keyword:"required",params:{missingProperty: "options"},message:"must have required property '"+"options"+"'"}; +if(vErrors === null){ +vErrors = [err108]; +} +else { +vErrors.push(err108); +} +errors++; +} +for(const key8 in data){ +if(!((((((((key8 === "key") || (key8 === "type")) || (key8 === "labels")) || (key8 === "localized")) || (key8 === "requiredToPublish")) || (key8 === "migration")) || (key8 === "options")) || (key8 === "multiple"))){ +const err109 = {instancePath,schemaPath:"#/oneOf/8/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key8},message:"must NOT have additional properties"}; +if(vErrors === null){ +vErrors = [err109]; +} +else { +vErrors.push(err109); +} +errors++; +} +} +if(data.key !== undefined){ +let data54 = data.key; +if(typeof data54 === "string"){ +if(!pattern22.test(data54)){ +const err110 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; +if(vErrors === null){ +vErrors = [err110]; +} +else { +vErrors.push(err110); +} +errors++; +} +} +else { +const err111 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err111]; +} +else { +vErrors.push(err111); +} +errors++; +} +} +if(data.type !== undefined){ +let data55 = data.type; +if(typeof data55 !== "string"){ +const err112 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/8/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err112]; +} +else { +vErrors.push(err112); +} +errors++; +} +if(!(data55 === "select")){ +const err113 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/8/properties/type/enum",keyword:"enum",params:{allowedValues: schema57.oneOf[8].properties.type.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err113]; +} +else { +vErrors.push(err113); +} +errors++; +} +} +if(data.labels !== undefined){ +if(!(validate26(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.errors); +errors = vErrors.length; +} +} +if(data.localized !== undefined){ +if(typeof data.localized !== "boolean"){ +const err114 = {instancePath:instancePath+"/localized",schemaPath:"#/oneOf/8/properties/localized/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(vErrors === null){ +vErrors = [err114]; +} +else { +vErrors.push(err114); +} +errors++; +} +} +if(data.requiredToPublish !== undefined){ +if(typeof data.requiredToPublish !== "boolean"){ +const err115 = {instancePath:instancePath+"/requiredToPublish",schemaPath:"#/oneOf/8/properties/requiredToPublish/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(vErrors === null){ +vErrors = [err115]; +} +else { +vErrors.push(err115); +} +errors++; +} +} +if(data.migration !== undefined){ +if(!(validate28(data.migration, {instancePath:instancePath+"/migration",parentData:data,parentDataProperty:"migration",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate28.errors : vErrors.concat(validate28.errors); +errors = vErrors.length; +} +} +if(data.options !== undefined){ +let data60 = data.options; +if(Array.isArray(data60)){ +if(data60.length > 50){ +const err116 = {instancePath:instancePath+"/options",schemaPath:"#/oneOf/8/properties/options/maxItems",keyword:"maxItems",params:{limit: 50},message:"must NOT have more than 50 items"}; +if(vErrors === null){ +vErrors = [err116]; +} +else { +vErrors.push(err116); +} +errors++; +} +if(data60.length < 1){ +const err117 = {instancePath:instancePath+"/options",schemaPath:"#/oneOf/8/properties/options/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}; +if(vErrors === null){ +vErrors = [err117]; +} +else { +vErrors.push(err117); +} +errors++; +} +const len0 = data60.length; +for(let i0=0; i0 5){ +const err133 = {instancePath:instancePath+"/collections",schemaPath:"#/oneOf/9/properties/collections/maxItems",keyword:"maxItems",params:{limit: 5},message:"must NOT have more than 5 items"}; +if(vErrors === null){ +vErrors = [err133]; } else { -const err79 = {instancePath,schemaPath:"#/oneOf/5/type",keyword:"type",params:{type: "object"},message:"must be object"}; +vErrors.push(err133); +} +errors++; +} +if(data69.length < 1){ +const err134 = {instancePath:instancePath+"/collections",schemaPath:"#/oneOf/9/properties/collections/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}; if(vErrors === null){ -vErrors = [err79]; +vErrors = [err134]; } else { -vErrors.push(err79); +vErrors.push(err134); } errors++; } -var _valid0 = _errs83 === errors; -if(_valid0 && valid0){ -valid0 = false; -passing0 = [passing0, 5]; +const len1 = data69.length; +for(let i1=0; i1 128){ +const err135 = {instancePath:instancePath+"/collections/" + i1,schemaPath:"#/$defs/Slug/maxLength",keyword:"maxLength",params:{limit: 128},message:"must NOT have more than 128 characters"}; +if(vErrors === null){ +vErrors = [err135]; } else { -if(_valid0){ -valid0 = true; -passing0 = 5; -if(props0 !== true){ -props0 = true; +vErrors.push(err135); } +errors++; } -const _errs97 = errors; -if(data && typeof data == "object" && !Array.isArray(data)){ -if(data.key === undefined){ -const err80 = {instancePath,schemaPath:"#/oneOf/6/required",keyword:"required",params:{missingProperty: "key"},message:"must have required property '"+"key"+"'"}; +if(func2(data70) < 1){ +const err136 = {instancePath:instancePath+"/collections/" + i1,schemaPath:"#/$defs/Slug/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}; if(vErrors === null){ -vErrors = [err80]; +vErrors = [err136]; } else { -vErrors.push(err80); +vErrors.push(err136); } errors++; } -if(data.type === undefined){ -const err81 = {instancePath,schemaPath:"#/oneOf/6/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; +if(!pattern5.test(data70)){ +const err137 = {instancePath:instancePath+"/collections/" + i1,schemaPath:"#/$defs/Slug/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}; if(vErrors === null){ -vErrors = [err81]; +vErrors = [err137]; } else { -vErrors.push(err81); +vErrors.push(err137); } errors++; } -if(data.labels === undefined){ -const err82 = {instancePath,schemaPath:"#/oneOf/6/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; +} +else { +const err138 = {instancePath:instancePath+"/collections/" + i1,schemaPath:"#/$defs/Slug/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ -vErrors = [err82]; +vErrors = [err138]; } else { -vErrors.push(err82); +vErrors.push(err138); } errors++; } -if(data.localized === undefined){ -const err83 = {instancePath,schemaPath:"#/oneOf/6/required",keyword:"required",params:{missingProperty: "localized"},message:"must have required property '"+"localized"+"'"}; +} +let i2 = data69.length; +let j0; +if(i2 > 1){ +outer0: +for(;i2--;){ +for(j0 = i2; j0--;){ +if(func0(data69[i2], data69[j0])){ +const err139 = {instancePath:instancePath+"/collections",schemaPath:"#/oneOf/9/properties/collections/uniqueItems",keyword:"uniqueItems",params:{i: i2, j: j0},message:"must NOT have duplicate items (items ## "+j0+" and "+i2+" are identical)"}; if(vErrors === null){ -vErrors = [err83]; +vErrors = [err139]; } else { -vErrors.push(err83); +vErrors.push(err139); } errors++; +break outer0; } -for(const key6 in data){ -if(!((((((key6 === "key") || (key6 === "type")) || (key6 === "labels")) || (key6 === "localized")) || (key6 === "requiredToPublish")) || (key6 === "migration"))){ -const err84 = {instancePath,schemaPath:"#/oneOf/6/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key6},message:"must NOT have additional properties"}; +} +} +} +} +else { +const err140 = {instancePath:instancePath+"/collections",schemaPath:"#/oneOf/9/properties/collections/type",keyword:"type",params:{type: "array"},message:"must be array"}; if(vErrors === null){ -vErrors = [err84]; +vErrors = [err140]; } else { -vErrors.push(err84); +vErrors.push(err140); } errors++; } } -if(data.key !== undefined){ -let data42 = data.key; -if(typeof data42 === "string"){ -if(!pattern22.test(data42)){ -const err85 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; +if(data.multiple !== undefined){ +if(typeof data.multiple !== "boolean"){ +const err141 = {instancePath:instancePath+"/multiple",schemaPath:"#/oneOf/9/properties/multiple/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; if(vErrors === null){ -vErrors = [err85]; +vErrors = [err141]; } else { -vErrors.push(err85); +vErrors.push(err141); } errors++; } } -else { -const err86 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(data.maxItems !== undefined){ +let data72 = data.maxItems; +if(!(((typeof data72 == "number") && (!(data72 % 1) && !isNaN(data72))) && (isFinite(data72)))){ +const err142 = {instancePath:instancePath+"/maxItems",schemaPath:"#/oneOf/9/properties/maxItems/type",keyword:"type",params:{type: "integer"},message:"must be integer"}; if(vErrors === null){ -vErrors = [err86]; +vErrors = [err142]; } else { -vErrors.push(err86); +vErrors.push(err142); } errors++; } +if((typeof data72 == "number") && (isFinite(data72))){ +if(data72 > 50 || isNaN(data72)){ +const err143 = {instancePath:instancePath+"/maxItems",schemaPath:"#/oneOf/9/properties/maxItems/maximum",keyword:"maximum",params:{comparison: "<=", limit: 50},message:"must be <= 50"}; +if(vErrors === null){ +vErrors = [err143]; +} +else { +vErrors.push(err143); } -if(data.type !== undefined){ -let data43 = data.type; -if(typeof data43 !== "string"){ -const err87 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/6/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +errors++; +} +if(data72 < 1 || isNaN(data72)){ +const err144 = {instancePath:instancePath+"/maxItems",schemaPath:"#/oneOf/9/properties/maxItems/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"}; if(vErrors === null){ -vErrors = [err87]; +vErrors = [err144]; } else { -vErrors.push(err87); +vErrors.push(err144); } errors++; } -if(!(data43 === "url")){ -const err88 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/6/properties/type/enum",keyword:"enum",params:{allowedValues: schema57.oneOf[6].properties.type.enum},message:"must be equal to one of the allowed values"}; +} +} +if(data.where !== undefined){ +let data73 = data.where; +if(Array.isArray(data73)){ +if(data73.length > 4){ +const err145 = {instancePath:instancePath+"/where",schemaPath:"#/oneOf/9/properties/where/maxItems",keyword:"maxItems",params:{limit: 4},message:"must NOT have more than 4 items"}; if(vErrors === null){ -vErrors = [err88]; +vErrors = [err145]; } else { -vErrors.push(err88); +vErrors.push(err145); } errors++; } +if(data73.length < 1){ +const err146 = {instancePath:instancePath+"/where",schemaPath:"#/oneOf/9/properties/where/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}; +if(vErrors === null){ +vErrors = [err146]; } -if(data.labels !== undefined){ -if(!(validate26(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.errors); -errors = vErrors.length; +else { +vErrors.push(err146); } +errors++; } -if(data.localized !== undefined){ -if(typeof data.localized !== "boolean"){ -const err89 = {instancePath:instancePath+"/localized",schemaPath:"#/oneOf/6/properties/localized/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +const len2 = data73.length; +for(let i3=0; i3 100){ +const err154 = {instancePath:instancePath+"/where/" + i3+"/value",schemaPath:"#/$defs/FieldCondition/properties/value/maxLength",keyword:"maxLength",params:{limit: 100},message:"must NOT have more than 100 characters"}; if(vErrors === null){ -vErrors = [err93]; +vErrors = [err154]; } else { -vErrors.push(err93); +vErrors.push(err154); } errors++; } -if(data.labels === undefined){ -const err94 = {instancePath,schemaPath:"#/oneOf/7/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; +if(func2(data77) < 1){ +const err155 = {instancePath:instancePath+"/where/" + i3+"/value",schemaPath:"#/$defs/FieldCondition/properties/value/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}; if(vErrors === null){ -vErrors = [err94]; +vErrors = [err155]; } else { -vErrors.push(err94); +vErrors.push(err155); } errors++; } -if(data.localized === undefined){ -const err95 = {instancePath,schemaPath:"#/oneOf/7/required",keyword:"required",params:{missingProperty: "localized"},message:"must have required property '"+"localized"+"'"}; +} +else { +const err156 = {instancePath:instancePath+"/where/" + i3+"/value",schemaPath:"#/$defs/FieldCondition/properties/value/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ -vErrors = [err95]; +vErrors = [err156]; } else { -vErrors.push(err95); +vErrors.push(err156); } errors++; } -for(const key7 in data){ -if(!((((((key7 === "key") || (key7 === "type")) || (key7 === "labels")) || (key7 === "localized")) || (key7 === "requiredToPublish")) || (key7 === "migration"))){ -const err96 = {instancePath,schemaPath:"#/oneOf/7/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key7},message:"must NOT have additional properties"}; +} +if(data74.values !== undefined){ +let data78 = data74.values; +if(Array.isArray(data78)){ +if(data78.length > 20){ +const err157 = {instancePath:instancePath+"/where/" + i3+"/values",schemaPath:"#/$defs/FieldCondition/properties/values/maxItems",keyword:"maxItems",params:{limit: 20},message:"must NOT have more than 20 items"}; if(vErrors === null){ -vErrors = [err96]; +vErrors = [err157]; } else { -vErrors.push(err96); +vErrors.push(err157); } errors++; } -} -if(data.key !== undefined){ -let data48 = data.key; -if(typeof data48 === "string"){ -if(!pattern22.test(data48)){ -const err97 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; +if(data78.length < 1){ +const err158 = {instancePath:instancePath+"/where/" + i3+"/values",schemaPath:"#/$defs/FieldCondition/properties/values/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}; if(vErrors === null){ -vErrors = [err97]; +vErrors = [err158]; } else { -vErrors.push(err97); +vErrors.push(err158); } errors++; } +const len3 = data78.length; +for(let i4=0; i4 100){ +const err159 = {instancePath:instancePath+"/where/" + i3+"/values/" + i4,schemaPath:"#/$defs/FieldCondition/properties/values/items/maxLength",keyword:"maxLength",params:{limit: 100},message:"must NOT have more than 100 characters"}; +if(vErrors === null){ +vErrors = [err159]; } else { -const err98 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; +vErrors.push(err159); +} +errors++; +} +if(func2(data79) < 1){ +const err160 = {instancePath:instancePath+"/where/" + i3+"/values/" + i4,schemaPath:"#/$defs/FieldCondition/properties/values/items/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}; if(vErrors === null){ -vErrors = [err98]; +vErrors = [err160]; } else { -vErrors.push(err98); +vErrors.push(err160); } errors++; } } -if(data.type !== undefined){ -let data49 = data.type; -if(typeof data49 !== "string"){ -const err99 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/7/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +else { +const err161 = {instancePath:instancePath+"/where/" + i3+"/values/" + i4,schemaPath:"#/$defs/FieldCondition/properties/values/items/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ -vErrors = [err99]; +vErrors = [err161]; } else { -vErrors.push(err99); +vErrors.push(err161); } errors++; } -if(!(data49 === "image")){ -const err100 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/7/properties/type/enum",keyword:"enum",params:{allowedValues: schema57.oneOf[7].properties.type.enum},message:"must be equal to one of the allowed values"}; +} +let i5 = data78.length; +let j1; +if(i5 > 1){ +const indices0 = {}; +for(;i5--;){ +let item0 = data78[i5]; +if(typeof item0 !== "string"){ +continue; +} +if(typeof indices0[item0] == "number"){ +j1 = indices0[item0]; +const err162 = {instancePath:instancePath+"/where/" + i3+"/values",schemaPath:"#/$defs/FieldCondition/properties/values/uniqueItems",keyword:"uniqueItems",params:{i: i5, j: j1},message:"must NOT have duplicate items (items ## "+j1+" and "+i5+" are identical)"}; if(vErrors === null){ -vErrors = [err100]; +vErrors = [err162]; } else { -vErrors.push(err100); +vErrors.push(err162); } errors++; +break; } +indices0[item0] = i5; } -if(data.labels !== undefined){ -if(!(validate26(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.errors); -errors = vErrors.length; } } -if(data.localized !== undefined){ -if(typeof data.localized !== "boolean"){ -const err101 = {instancePath:instancePath+"/localized",schemaPath:"#/oneOf/7/properties/localized/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +else { +const err163 = {instancePath:instancePath+"/where/" + i3+"/values",schemaPath:"#/$defs/FieldCondition/properties/values/type",keyword:"type",params:{type: "array"},message:"must be array"}; if(vErrors === null){ -vErrors = [err101]; +vErrors = [err163]; } else { -vErrors.push(err101); +vErrors.push(err163); } errors++; } } -if(data.requiredToPublish !== undefined){ -if(typeof data.requiredToPublish !== "boolean"){ -const err102 = {instancePath:instancePath+"/requiredToPublish",schemaPath:"#/oneOf/7/properties/requiredToPublish/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +} +else { +const err164 = {instancePath:instancePath+"/where/" + i3,schemaPath:"#/$defs/FieldCondition/type",keyword:"type",params:{type: "object"},message:"must be object"}; if(vErrors === null){ -vErrors = [err102]; +vErrors = [err164]; } else { -vErrors.push(err102); +vErrors.push(err164); } errors++; } } -if(data.migration !== undefined){ -if(!(validate28(data.migration, {instancePath:instancePath+"/migration",parentData:data,parentDataProperty:"migration",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate28.errors : vErrors.concat(validate28.errors); -errors = vErrors.length; +} +else { +const err165 = {instancePath:instancePath+"/where",schemaPath:"#/oneOf/9/properties/where/type",keyword:"type",params:{type: "array"},message:"must be array"}; +if(vErrors === null){ +vErrors = [err165]; +} +else { +vErrors.push(err165); +} +errors++; } } } else { -const err103 = {instancePath,schemaPath:"#/oneOf/7/type",keyword:"type",params:{type: "object"},message:"must be object"}; +const err166 = {instancePath,schemaPath:"#/oneOf/9/type",keyword:"type",params:{type: "object"},message:"must be object"}; if(vErrors === null){ -vErrors = [err103]; +vErrors = [err166]; } else { -vErrors.push(err103); +vErrors.push(err166); } errors++; } -var _valid0 = _errs111 === errors; +var _valid0 = _errs144 === errors; if(_valid0 && valid0){ valid0 = false; -passing0 = [passing0, 7]; +passing0 = [passing0, 9]; } else { if(_valid0){ valid0 = true; -passing0 = 7; +passing0 = 9; if(props0 !== true){ props0 = true; } } -const _errs125 = errors; +const _errs183 = errors; if(data && typeof data == "object" && !Array.isArray(data)){ if(data.key === undefined){ -const err104 = {instancePath,schemaPath:"#/oneOf/8/required",keyword:"required",params:{missingProperty: "key"},message:"must have required property '"+"key"+"'"}; +const err167 = {instancePath,schemaPath:"#/oneOf/10/required",keyword:"required",params:{missingProperty: "key"},message:"must have required property '"+"key"+"'"}; if(vErrors === null){ -vErrors = [err104]; +vErrors = [err167]; } else { -vErrors.push(err104); +vErrors.push(err167); } errors++; } if(data.type === undefined){ -const err105 = {instancePath,schemaPath:"#/oneOf/8/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; +const err168 = {instancePath,schemaPath:"#/oneOf/10/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; if(vErrors === null){ -vErrors = [err105]; +vErrors = [err168]; } else { -vErrors.push(err105); +vErrors.push(err168); } errors++; } if(data.labels === undefined){ -const err106 = {instancePath,schemaPath:"#/oneOf/8/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; +const err169 = {instancePath,schemaPath:"#/oneOf/10/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; if(vErrors === null){ -vErrors = [err106]; +vErrors = [err169]; } else { -vErrors.push(err106); +vErrors.push(err169); } errors++; } if(data.localized === undefined){ -const err107 = {instancePath,schemaPath:"#/oneOf/8/required",keyword:"required",params:{missingProperty: "localized"},message:"must have required property '"+"localized"+"'"}; +const err170 = {instancePath,schemaPath:"#/oneOf/10/required",keyword:"required",params:{missingProperty: "localized"},message:"must have required property '"+"localized"+"'"}; if(vErrors === null){ -vErrors = [err107]; +vErrors = [err170]; } else { -vErrors.push(err107); +vErrors.push(err170); } errors++; } -if(data.options === undefined){ -const err108 = {instancePath,schemaPath:"#/oneOf/8/required",keyword:"required",params:{missingProperty: "options"},message:"must have required property '"+"options"+"'"}; +if(data.fields === undefined){ +const err171 = {instancePath,schemaPath:"#/oneOf/10/required",keyword:"required",params:{missingProperty: "fields"},message:"must have required property '"+"fields"+"'"}; if(vErrors === null){ -vErrors = [err108]; +vErrors = [err171]; } else { -vErrors.push(err108); +vErrors.push(err171); } errors++; } -for(const key8 in data){ -if(!((((((((key8 === "key") || (key8 === "type")) || (key8 === "labels")) || (key8 === "localized")) || (key8 === "requiredToPublish")) || (key8 === "migration")) || (key8 === "options")) || (key8 === "multiple"))){ -const err109 = {instancePath,schemaPath:"#/oneOf/8/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key8},message:"must NOT have additional properties"}; +for(const key11 in data){ +if(!(func1.call(schema57.oneOf[10].properties, key11))){ +const err172 = {instancePath,schemaPath:"#/oneOf/10/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key11},message:"must NOT have additional properties"}; if(vErrors === null){ -vErrors = [err109]; +vErrors = [err172]; } else { -vErrors.push(err109); +vErrors.push(err172); } errors++; } } if(data.key !== undefined){ -let data54 = data.key; -if(typeof data54 === "string"){ -if(!pattern22.test(data54)){ -const err110 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; +let data80 = data.key; +if(typeof data80 === "string"){ +if(!pattern22.test(data80)){ +const err173 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; if(vErrors === null){ -vErrors = [err110]; +vErrors = [err173]; } else { -vErrors.push(err110); +vErrors.push(err173); } errors++; } } else { -const err111 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; +const err174 = {instancePath:instancePath+"/key",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ -vErrors = [err111]; +vErrors = [err174]; } else { -vErrors.push(err111); +vErrors.push(err174); } errors++; } } if(data.type !== undefined){ -let data55 = data.type; -if(typeof data55 !== "string"){ -const err112 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/8/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +let data81 = data.type; +if(typeof data81 !== "string"){ +const err175 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/10/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ -vErrors = [err112]; +vErrors = [err175]; } else { -vErrors.push(err112); +vErrors.push(err175); } errors++; } -if(!(data55 === "select")){ -const err113 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/8/properties/type/enum",keyword:"enum",params:{allowedValues: schema57.oneOf[8].properties.type.enum},message:"must be equal to one of the allowed values"}; +if(!(data81 === "repeater")){ +const err176 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/10/properties/type/enum",keyword:"enum",params:{allowedValues: schema57.oneOf[10].properties.type.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ -vErrors = [err113]; +vErrors = [err176]; } else { -vErrors.push(err113); +vErrors.push(err176); } errors++; } @@ -2900,24 +5218,24 @@ errors = vErrors.length; } if(data.localized !== undefined){ if(typeof data.localized !== "boolean"){ -const err114 = {instancePath:instancePath+"/localized",schemaPath:"#/oneOf/8/properties/localized/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +const err177 = {instancePath:instancePath+"/localized",schemaPath:"#/oneOf/10/properties/localized/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; if(vErrors === null){ -vErrors = [err114]; +vErrors = [err177]; } else { -vErrors.push(err114); +vErrors.push(err177); } errors++; } } if(data.requiredToPublish !== undefined){ if(typeof data.requiredToPublish !== "boolean"){ -const err115 = {instancePath:instancePath+"/requiredToPublish",schemaPath:"#/oneOf/8/properties/requiredToPublish/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +const err178 = {instancePath:instancePath+"/requiredToPublish",schemaPath:"#/oneOf/10/properties/requiredToPublish/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; if(vErrors === null){ -vErrors = [err115]; +vErrors = [err178]; } else { -vErrors.push(err115); +vErrors.push(err178); } errors++; } @@ -2928,80 +5246,128 @@ vErrors = vErrors === null ? validate28.errors : vErrors.concat(validate28.error errors = vErrors.length; } } -if(data.options !== undefined){ -let data60 = data.options; -if(Array.isArray(data60)){ -if(data60.length > 50){ -const err116 = {instancePath:instancePath+"/options",schemaPath:"#/oneOf/8/properties/options/maxItems",keyword:"maxItems",params:{limit: 50},message:"must NOT have more than 50 items"}; +if(data.fields !== undefined){ +let data86 = data.fields; +if(Array.isArray(data86)){ +if(data86.length > 10){ +const err179 = {instancePath:instancePath+"/fields",schemaPath:"#/oneOf/10/properties/fields/maxItems",keyword:"maxItems",params:{limit: 10},message:"must NOT have more than 10 items"}; if(vErrors === null){ -vErrors = [err116]; +vErrors = [err179]; } else { -vErrors.push(err116); +vErrors.push(err179); } errors++; } -if(data60.length < 1){ -const err117 = {instancePath:instancePath+"/options",schemaPath:"#/oneOf/8/properties/options/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}; +if(data86.length < 1){ +const err180 = {instancePath:instancePath+"/fields",schemaPath:"#/oneOf/10/properties/fields/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}; if(vErrors === null){ -vErrors = [err117]; +vErrors = [err180]; } else { -vErrors.push(err117); +vErrors.push(err180); } errors++; } -const len0 = data60.length; -for(let i0=0; i0 50 || isNaN(data88)){ +const err183 = {instancePath:instancePath+"/maxItems",schemaPath:"#/oneOf/10/properties/maxItems/maximum",keyword:"maximum",params:{comparison: "<=", limit: 50},message:"must be <= 50"}; +if(vErrors === null){ +vErrors = [err183]; +} +else { +vErrors.push(err183); +} +errors++; +} +if(data88 < 1 || isNaN(data88)){ +const err184 = {instancePath:instancePath+"/maxItems",schemaPath:"#/oneOf/10/properties/maxItems/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"}; +if(vErrors === null){ +vErrors = [err184]; +} +else { +vErrors.push(err184); } errors++; } } } +if(data.itemLabel !== undefined){ +let data89 = data.itemLabel; +if(typeof data89 === "string"){ +if(!pattern22.test(data89)){ +const err185 = {instancePath:instancePath+"/itemLabel",schemaPath:"#/$defs/FieldKey/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z0-9]{0,39}$"},message:"must match pattern \""+"^[a-z][A-Za-z0-9]{0,39}$"+"\""}; +if(vErrors === null){ +vErrors = [err185]; +} else { -const err120 = {instancePath,schemaPath:"#/oneOf/8/type",keyword:"type",params:{type: "object"},message:"must be object"}; +vErrors.push(err185); +} +errors++; +} +} +else { +const err186 = {instancePath:instancePath+"/itemLabel",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ -vErrors = [err120]; +vErrors = [err186]; } else { -vErrors.push(err120); +vErrors.push(err186); } errors++; } -var _valid0 = _errs125 === errors; +} +} +else { +const err187 = {instancePath,schemaPath:"#/oneOf/10/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err187]; +} +else { +vErrors.push(err187); +} +errors++; +} +var _valid0 = _errs183 === errors; if(_valid0 && valid0){ valid0 = false; -passing0 = [passing0, 8]; +passing0 = [passing0, 10]; } else { if(_valid0){ valid0 = true; -passing0 = 8; +passing0 = 10; if(props0 !== true){ props0 = true; } @@ -3014,13 +5380,15 @@ props0 = true; } } } +} +} if(!valid0){ -const err121 = {instancePath,schemaPath:"#/oneOf",keyword:"oneOf",params:{passingSchemas: passing0},message:"must match exactly one schema in oneOf"}; +const err188 = {instancePath,schemaPath:"#/oneOf",keyword:"oneOf",params:{passingSchemas: passing0},message:"must match exactly one schema in oneOf"}; if(vErrors === null){ -vErrors = [err121]; +vErrors = [err188]; } else { -vErrors.push(err121); +vErrors.push(err188); } errors++; } @@ -3041,12 +5409,12 @@ return errors === 0; } validate25.evaluated = {"dynamicProps":true,"dynamicItems":false}; -const schema77 = {"type":"object","description":"RFC-0042: a time gate every entry derives from one of its date or datetime fields, checked at render time like Availability.until. A date ends at the start of the next day in the site's time zone.","additionalProperties":false,"required":["field","action"],"properties":{"field":{"$ref":"#/$defs/FieldKey"},"action":{"type":"string","description":"unlist: the page stays, the entry leaves listings, feeds, the sitemap and search. hide: every public route answers 404.","enum":["unlist","hide"]},"afterMinutes":{"type":"integer","description":"Grace after the field's instant. Absent: 0.","minimum":0,"maximum":10080}}}; +const schema94 = {"type":"object","description":"RFC-0042: a time gate every entry derives from one of its date or datetime fields, checked at render time like Availability.until. A date ends at the start of the next day in the site's time zone.","additionalProperties":false,"required":["field","action"],"properties":{"field":{"$ref":"#/$defs/FieldKey"},"action":{"type":"string","description":"unlist: the page stays, the entry leaves listings, feeds, the sitemap and search. hide: every public route answers 404.","enum":["unlist","hide"]},"afterMinutes":{"type":"integer","description":"Grace after the field's instant. Absent: 0.","minimum":0,"maximum":10080}}}; -function validate49(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate65(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate49.evaluated; +const evaluated0 = validate65.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -3124,7 +5492,7 @@ vErrors.push(err5); errors++; } if(!((data1 === "unlist") || (data1 === "hide"))){ -const err6 = {instancePath:instancePath+"/action",schemaPath:"#/properties/action/enum",keyword:"enum",params:{allowedValues: schema77.properties.action.enum},message:"must be equal to one of the allowed values"}; +const err6 = {instancePath:instancePath+"/action",schemaPath:"#/properties/action/enum",keyword:"enum",params:{allowedValues: schema94.properties.action.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err6]; } @@ -3180,22 +5548,22 @@ vErrors.push(err10); } errors++; } -validate49.errors = vErrors; +validate65.errors = vErrors; return errors === 0; } -validate49.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate65.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const schema79 = {"type":"object","description":"RFC-0042: the JSON-LD type an entry page emits in place of BlogPosting, with its properties mapped to value sources. Paths, fitting field types and required properties are a closed table in code (@cloudhal/core-ts/structured-data).","additionalProperties":false,"required":["type","properties"],"properties":{"type":{"type":"string","enum":["Event","JobPosting","Product","Recipe"]},"properties":{"type":"object","description":"Keyed by property path, e.g. startDate or location.address.addressLocality.","propertyNames":{"type":"string","pattern":"^[a-z][A-Za-z]{0,39}(\\.[a-z][A-Za-z]{0,39}){0,2}$"},"minProperties":1,"maxProperties":40,"additionalProperties":{"$ref":"#/$defs/StructuredDataSource"}}}}; -const pattern38 = new RegExp("^[a-z][A-Za-z]{0,39}(\\.[a-z][A-Za-z]{0,39}){0,2}$", "u"); -const schema80 = {"description":"RFC-0042: where one JSON-LD property's value comes from, discriminated by `kind`.","oneOf":[{"title":"FieldValueSource","type":"object","description":"A typed field of this collection, converted by its type.","additionalProperties":false,"required":["kind","field"],"properties":{"kind":{"type":"string","enum":["field"]},"field":{"$ref":"#/$defs/FieldKey"}}},{"title":"EntryValueSource","type":"object","description":"A built-in property of the entry.","additionalProperties":false,"required":["kind","property"],"properties":{"kind":{"type":"string","enum":["entry"]},"property":{"$ref":"#/$defs/EntryValueProperty"}}},{"title":"SiteValueSource","type":"object","description":"A property of the site.","additionalProperties":false,"required":["kind","property"],"properties":{"kind":{"type":"string","enum":["site"]},"property":{"$ref":"#/$defs/SiteValueProperty"}}},{"title":"ConstantValueSource","type":"object","description":"A plain-text constant: no control characters, no markup meaning; vocabulary-checked on enum paths (in code).","additionalProperties":false,"required":["kind","value"],"properties":{"kind":{"type":"string","enum":["constant"]},"value":{"type":"string","minLength":1,"maxLength":200,"pattern":"^[^\\x00-\\x1F\\x7F]+$"}}}]}; -const schema82 = {"type":"string","description":"RFC-0042: entry properties a structured-data property may read.","enum":["title","summary","body","coverImage","url","publishedAt","updatedAt"]}; -const schema83 = {"type":"string","description":"RFC-0042: site properties a structured-data property may read.","enum":["name","url","logo"]}; -const pattern40 = new RegExp("^[^\\x00-\\x1F\\x7F]+$", "u"); +const schema96 = {"type":"object","description":"RFC-0042: the JSON-LD type an entry page emits in place of BlogPosting, with its properties mapped to value sources. Paths, fitting field types and required properties are a closed table in code (@cloudhal/core-ts/structured-data).","additionalProperties":false,"required":["type","properties"],"properties":{"type":{"type":"string","enum":["Event","JobPosting","Product","Recipe"]},"properties":{"type":"object","description":"Keyed by property path, e.g. startDate or location.address.addressLocality.","propertyNames":{"type":"string","pattern":"^[a-z][A-Za-z]{0,39}(\\.[a-z][A-Za-z]{0,39}){0,2}$"},"minProperties":1,"maxProperties":40,"additionalProperties":{"$ref":"#/$defs/StructuredDataSource"}}}}; +const pattern54 = new RegExp("^[a-z][A-Za-z]{0,39}(\\.[a-z][A-Za-z]{0,39}){0,2}$", "u"); +const schema97 = {"description":"RFC-0042: where one JSON-LD property's value comes from, discriminated by `kind`.","oneOf":[{"title":"FieldValueSource","type":"object","description":"A typed field of this collection, converted by its type.","additionalProperties":false,"required":["kind","field"],"properties":{"kind":{"type":"string","enum":["field"]},"field":{"$ref":"#/$defs/FieldKey"}}},{"title":"EntryValueSource","type":"object","description":"A built-in property of the entry.","additionalProperties":false,"required":["kind","property"],"properties":{"kind":{"type":"string","enum":["entry"]},"property":{"$ref":"#/$defs/EntryValueProperty"}}},{"title":"SiteValueSource","type":"object","description":"A property of the site.","additionalProperties":false,"required":["kind","property"],"properties":{"kind":{"type":"string","enum":["site"]},"property":{"$ref":"#/$defs/SiteValueProperty"}}},{"title":"ConstantValueSource","type":"object","description":"A plain-text constant: no control characters, no markup meaning; vocabulary-checked on enum paths (in code).","additionalProperties":false,"required":["kind","value"],"properties":{"kind":{"type":"string","enum":["constant"]},"value":{"type":"string","minLength":1,"maxLength":200,"pattern":"^[^\\x00-\\x1F\\x7F]+$"}}}]}; +const schema99 = {"type":"string","description":"RFC-0042: entry properties a structured-data property may read.","enum":["title","summary","body","coverImage","url","publishedAt","updatedAt"]}; +const schema100 = {"type":"string","description":"RFC-0042: site properties a structured-data property may read.","enum":["name","url","logo"]}; +const pattern56 = new RegExp("^[^\\x00-\\x1F\\x7F]+$", "u"); -function validate52(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate68(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate52.evaluated; +const evaluated0 = validate68.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -3252,7 +5620,7 @@ vErrors.push(err3); errors++; } if(!(data0 === "field")){ -const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema80.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema97.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -3351,7 +5719,7 @@ vErrors.push(err11); errors++; } if(!(data2 === "entry")){ -const err12 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema80.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err12 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema97.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err12]; } @@ -3374,7 +5742,7 @@ vErrors.push(err13); errors++; } if(!(((((((data3 === "title") || (data3 === "summary")) || (data3 === "body")) || (data3 === "coverImage")) || (data3 === "url")) || (data3 === "publishedAt")) || (data3 === "updatedAt"))){ -const err14 = {instancePath:instancePath+"/property",schemaPath:"#/$defs/EntryValueProperty/enum",keyword:"enum",params:{allowedValues: schema82.enum},message:"must be equal to one of the allowed values"}; +const err14 = {instancePath:instancePath+"/property",schemaPath:"#/$defs/EntryValueProperty/enum",keyword:"enum",params:{allowedValues: schema99.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err14]; } @@ -3455,7 +5823,7 @@ vErrors.push(err19); errors++; } if(!(data4 === "site")){ -const err20 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/2/properties/kind/enum",keyword:"enum",params:{allowedValues: schema80.oneOf[2].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err20 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/2/properties/kind/enum",keyword:"enum",params:{allowedValues: schema97.oneOf[2].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err20]; } @@ -3478,7 +5846,7 @@ vErrors.push(err21); errors++; } if(!(((data5 === "name") || (data5 === "url")) || (data5 === "logo"))){ -const err22 = {instancePath:instancePath+"/property",schemaPath:"#/$defs/SiteValueProperty/enum",keyword:"enum",params:{allowedValues: schema83.enum},message:"must be equal to one of the allowed values"}; +const err22 = {instancePath:instancePath+"/property",schemaPath:"#/$defs/SiteValueProperty/enum",keyword:"enum",params:{allowedValues: schema100.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err22]; } @@ -3559,7 +5927,7 @@ vErrors.push(err27); errors++; } if(!(data6 === "constant")){ -const err28 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/3/properties/kind/enum",keyword:"enum",params:{allowedValues: schema80.oneOf[3].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err28 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/3/properties/kind/enum",keyword:"enum",params:{allowedValues: schema97.oneOf[3].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err28]; } @@ -3592,7 +5960,7 @@ vErrors.push(err30); } errors++; } -if(!pattern40.test(data7)){ +if(!pattern56.test(data7)){ const err31 = {instancePath:instancePath+"/value",schemaPath:"#/oneOf/3/properties/value/pattern",keyword:"pattern",params:{pattern: "^[^\\x00-\\x1F\\x7F]+$"},message:"must match pattern \""+"^[^\\x00-\\x1F\\x7F]+$"+"\""}; if(vErrors === null){ vErrors = [err31]; @@ -3662,17 +6030,17 @@ vErrors = null; } } } -validate52.errors = vErrors; +validate68.errors = vErrors; evaluated0.props = props0; return errors === 0; } -validate52.evaluated = {"dynamicProps":true,"dynamicItems":false}; +validate68.evaluated = {"dynamicProps":true,"dynamicItems":false}; -function validate51(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate67(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate51.evaluated; +const evaluated0 = validate67.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -3725,7 +6093,7 @@ vErrors.push(err3); errors++; } if(!((((data0 === "Event") || (data0 === "JobPosting")) || (data0 === "Product")) || (data0 === "Recipe"))){ -const err4 = {instancePath:instancePath+"/type",schemaPath:"#/properties/type/enum",keyword:"enum",params:{allowedValues: schema79.properties.type.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/type",schemaPath:"#/properties/type/enum",keyword:"enum",params:{allowedValues: schema96.properties.type.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -3761,7 +6129,7 @@ errors++; for(const key1 in data1){ const _errs6 = errors; if(typeof key1 === "string"){ -if(!pattern38.test(key1)){ +if(!pattern54.test(key1)){ const err7 = {instancePath:instancePath+"/properties",schemaPath:"#/properties/properties/propertyNames/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z]{0,39}(\\.[a-z][A-Za-z]{0,39}){0,2}$"},message:"must match pattern \""+"^[a-z][A-Za-z]{0,39}(\\.[a-z][A-Za-z]{0,39}){0,2}$"+"\"",propertyName:key1}; if(vErrors === null){ vErrors = [err7]; @@ -3795,8 +6163,8 @@ errors++; } } for(const key2 in data1){ -if(!(validate52(data1[key2], {instancePath:instancePath+"/properties/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),parentData:data1,parentDataProperty:key2,rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate52.errors : vErrors.concat(validate52.errors); +if(!(validate68(data1[key2], {instancePath:instancePath+"/properties/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),parentData:data1,parentDataProperty:key2,rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate68.errors : vErrors.concat(validate68.errors); errors = vErrors.length; } } @@ -3823,10 +6191,10 @@ vErrors.push(err11); } errors++; } -validate51.errors = vErrors; +validate67.errors = vErrors; return errors === 0; } -validate51.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate67.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; function validate24(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ @@ -4286,14 +6654,14 @@ errors++; } } if(data.expiry !== undefined){ -if(!(validate49(data.expiry, {instancePath:instancePath+"/expiry",parentData:data,parentDataProperty:"expiry",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate49.errors : vErrors.concat(validate49.errors); +if(!(validate65(data.expiry, {instancePath:instancePath+"/expiry",parentData:data,parentDataProperty:"expiry",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate65.errors : vErrors.concat(validate65.errors); errors = vErrors.length; } } if(data.structuredData !== undefined){ -if(!(validate51(data.structuredData, {instancePath:instancePath+"/structuredData",parentData:data,parentDataProperty:"structuredData",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate51.errors : vErrors.concat(validate51.errors); +if(!(validate67(data.structuredData, {instancePath:instancePath+"/structuredData",parentData:data,parentDataProperty:"structuredData",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate67.errors : vErrors.concat(validate67.errors); errors = vErrors.length; } } @@ -4313,12 +6681,12 @@ return errors === 0; } validate24.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const schema84 = {"type":"object","description":"Theme (a.k.a. Experience) applied by the renderer. Config is theme-defined and validated by the theme's own schema, not here.","additionalProperties":false,"required":["name"],"properties":{"name":{"$ref":"#/$defs/Slug"},"version":{"type":"string","maxLength":64},"config":{"type":"object","additionalProperties":true}}}; +const schema101 = {"type":"object","description":"Theme (a.k.a. Experience) applied by the renderer. Config is theme-defined and validated by the theme's own schema, not here.","additionalProperties":false,"required":["name"],"properties":{"name":{"$ref":"#/$defs/Slug"},"version":{"type":"string","maxLength":64},"config":{"type":"object","additionalProperties":true}}}; -function validate56(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate72(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate56.evaluated; +const evaluated0 = validate72.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -4444,19 +6812,19 @@ vErrors.push(err9); } errors++; } -validate56.errors = vErrors; +validate72.errors = vErrors; return errors === 0; } -validate56.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate72.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const schema87 = {"type":"object","description":"The site's logo and social links (RFC-0025 §1), shared by the header, the footer and SEO.","additionalProperties":false,"properties":{"logo":{"$ref":"#/$defs/ImageRef"},"logoDark":{"$ref":"#/$defs/ImageRef"},"logoHeight":{"type":"string","enum":["s","m","l"]},"social":{"type":"array","maxItems":8,"items":{"$ref":"#/$defs/SocialLink"}}}}; -const schema90 = {"type":"object","additionalProperties":false,"required":["network","href"],"properties":{"network":{"type":"string","enum":["x","bluesky","mastodon","github","linkedin","instagram","facebook","youtube","tiktok","threads","rss","email"]},"href":{"type":"string","minLength":1,"maxLength":2048}}}; -const schema88 = {"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}}; +const schema104 = {"type":"object","description":"The site's logo and social links (RFC-0025 §1), shared by the header, the footer and SEO.","additionalProperties":false,"properties":{"logo":{"$ref":"#/$defs/ImageRef"},"logoDark":{"$ref":"#/$defs/ImageRef"},"logoHeight":{"type":"string","enum":["s","m","l"]},"social":{"type":"array","maxItems":8,"items":{"$ref":"#/$defs/SocialLink"}}}}; +const schema107 = {"type":"object","additionalProperties":false,"required":["network","href"],"properties":{"network":{"type":"string","enum":["x","bluesky","mastodon","github","linkedin","instagram","facebook","youtube","tiktok","threads","rss","email"]},"href":{"type":"string","minLength":1,"maxLength":2048}}}; +const schema105 = {"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}}; -function validate59(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate75(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate59.evaluated; +const evaluated0 = validate75.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -4662,16 +7030,16 @@ vErrors.push(err16); } errors++; } -validate59.errors = vErrors; +validate75.errors = vErrors; return errors === 0; } -validate59.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate75.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate58(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate74(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate58.evaluated; +const evaluated0 = validate74.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -4692,14 +7060,14 @@ errors++; } } if(data.logo !== undefined){ -if(!(validate59(data.logo, {instancePath:instancePath+"/logo",parentData:data,parentDataProperty:"logo",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate59.errors : vErrors.concat(validate59.errors); +if(!(validate75(data.logo, {instancePath:instancePath+"/logo",parentData:data,parentDataProperty:"logo",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate75.errors : vErrors.concat(validate75.errors); errors = vErrors.length; } } if(data.logoDark !== undefined){ -if(!(validate59(data.logoDark, {instancePath:instancePath+"/logoDark",parentData:data,parentDataProperty:"logoDark",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate59.errors : vErrors.concat(validate59.errors); +if(!(validate75(data.logoDark, {instancePath:instancePath+"/logoDark",parentData:data,parentDataProperty:"logoDark",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate75.errors : vErrors.concat(validate75.errors); errors = vErrors.length; } } @@ -4716,7 +7084,7 @@ vErrors.push(err1); errors++; } if(!(((data2 === "s") || (data2 === "m")) || (data2 === "l"))){ -const err2 = {instancePath:instancePath+"/logoHeight",schemaPath:"#/properties/logoHeight/enum",keyword:"enum",params:{allowedValues: schema87.properties.logoHeight.enum},message:"must be equal to one of the allowed values"}; +const err2 = {instancePath:instancePath+"/logoHeight",schemaPath:"#/properties/logoHeight/enum",keyword:"enum",params:{allowedValues: schema104.properties.logoHeight.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err2]; } @@ -4788,7 +7156,7 @@ vErrors.push(err7); errors++; } if(!((((((((((((data5 === "x") || (data5 === "bluesky")) || (data5 === "mastodon")) || (data5 === "github")) || (data5 === "linkedin")) || (data5 === "instagram")) || (data5 === "facebook")) || (data5 === "youtube")) || (data5 === "tiktok")) || (data5 === "threads")) || (data5 === "rss")) || (data5 === "email"))){ -const err8 = {instancePath:instancePath+"/social/" + i0+"/network",schemaPath:"#/$defs/SocialLink/properties/network/enum",keyword:"enum",params:{allowedValues: schema90.properties.network.enum},message:"must be equal to one of the allowed values"}; +const err8 = {instancePath:instancePath+"/social/" + i0+"/network",schemaPath:"#/$defs/SocialLink/properties/network/enum",keyword:"enum",params:{allowedValues: schema107.properties.network.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err8]; } @@ -4868,20 +7236,20 @@ vErrors.push(err14); } errors++; } -validate58.errors = vErrors; +validate74.errors = vErrors; return errors === 0; } -validate58.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate74.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const schema91 = {"type":"object","description":"The site's header, footer and bars as data (RFC-0025): enums, menu keys and short texts; the theme renders them with landmarks, focus and contrast guaranteed.","additionalProperties":false,"properties":{"header":{"$ref":"#/$defs/ChromeHeader"},"footer":{"$ref":"#/$defs/ChromeFooter"},"announcement":{"$ref":"#/$defs/Announcement"},"consent":{"$ref":"#/$defs/Consent"}}}; -const schema121 = {"type":"object","additionalProperties":false,"required":["id","text"],"properties":{"id":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,40}$","description":"Change it when the text changes, so readers who dismissed the old one see the new one."},"text":{"type":"string","minLength":1,"maxLength":200},"label":{"type":"string","minLength":1,"maxLength":40},"href":{"type":"string","minLength":1,"maxLength":2048},"tone":{"type":"string","enum":["info","brand","warning"]},"dismissible":{"type":"boolean"}}}; -const schema92 = {"type":"object","additionalProperties":false,"properties":{"layout":{"type":"string","enum":["logo-left","logo-left-nav-center","centered","split","minimal"]},"behavior":{"type":"string","enum":["static","sticky","hide-on-scroll","fixed"]},"shrink":{"type":"boolean"},"overHero":{"type":"string","enum":["solid","transparent"]},"width":{"type":"string","enum":["content","wide","full"]},"surface":{"type":"string","enum":["plain","muted","raised","inverse","brand"]},"mobileStyle":{"type":"string","enum":["dropdown","drawer-start","drawer-end","fullscreen"]},"collapseAt":{"type":"string","enum":["sm","md","lg","always"]},"nav":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"utility":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"actions":{"type":"array","maxItems":3,"items":{"$ref":"#/$defs/HeaderAction"}}}}; -const schema95 = {"type":"object","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["button","search","locale","theme-toggle","motion-toggle"]},"label":{"type":"string","minLength":1,"maxLength":40},"href":{"type":"string","minLength":1,"maxLength":2048},"variant":{"type":"string","enum":["primary","secondary"]}}}; +const schema108 = {"type":"object","description":"The site's header, footer and bars as data (RFC-0025): enums, menu keys and short texts; the theme renders them with landmarks, focus and contrast guaranteed.","additionalProperties":false,"properties":{"header":{"$ref":"#/$defs/ChromeHeader"},"footer":{"$ref":"#/$defs/ChromeFooter"},"announcement":{"$ref":"#/$defs/Announcement"},"consent":{"$ref":"#/$defs/Consent"}}}; +const schema138 = {"type":"object","additionalProperties":false,"required":["id","text"],"properties":{"id":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,40}$","description":"Change it when the text changes, so readers who dismissed the old one see the new one."},"text":{"type":"string","minLength":1,"maxLength":200},"label":{"type":"string","minLength":1,"maxLength":40},"href":{"type":"string","minLength":1,"maxLength":2048},"tone":{"type":"string","enum":["info","brand","warning"]},"dismissible":{"type":"boolean"}}}; +const schema109 = {"type":"object","additionalProperties":false,"properties":{"layout":{"type":"string","enum":["logo-left","logo-left-nav-center","centered","split","minimal"]},"behavior":{"type":"string","enum":["static","sticky","hide-on-scroll","fixed"]},"shrink":{"type":"boolean"},"overHero":{"type":"string","enum":["solid","transparent"]},"width":{"type":"string","enum":["content","wide","full"]},"surface":{"type":"string","enum":["plain","muted","raised","inverse","brand"]},"mobileStyle":{"type":"string","enum":["dropdown","drawer-start","drawer-end","fullscreen"]},"collapseAt":{"type":"string","enum":["sm","md","lg","always"]},"nav":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"utility":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"actions":{"type":"array","maxItems":3,"items":{"$ref":"#/$defs/HeaderAction"}}}}; +const schema112 = {"type":"object","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["button","search","locale","theme-toggle","motion-toggle"]},"label":{"type":"string","minLength":1,"maxLength":40},"href":{"type":"string","minLength":1,"maxLength":2048},"variant":{"type":"string","enum":["primary","secondary"]}}}; -function validate64(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate80(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate64.evaluated; +const evaluated0 = validate80.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -4890,7 +7258,7 @@ evaluated0.items = undefined; } if(data && typeof data == "object" && !Array.isArray(data)){ for(const key0 in data){ -if(!(func1.call(schema92.properties, key0))){ +if(!(func1.call(schema109.properties, key0))){ const err0 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err0]; @@ -4914,7 +7282,7 @@ vErrors.push(err1); errors++; } if(!(((((data0 === "logo-left") || (data0 === "logo-left-nav-center")) || (data0 === "centered")) || (data0 === "split")) || (data0 === "minimal"))){ -const err2 = {instancePath:instancePath+"/layout",schemaPath:"#/properties/layout/enum",keyword:"enum",params:{allowedValues: schema92.properties.layout.enum},message:"must be equal to one of the allowed values"}; +const err2 = {instancePath:instancePath+"/layout",schemaPath:"#/properties/layout/enum",keyword:"enum",params:{allowedValues: schema109.properties.layout.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err2]; } @@ -4937,7 +7305,7 @@ vErrors.push(err3); errors++; } if(!((((data1 === "static") || (data1 === "sticky")) || (data1 === "hide-on-scroll")) || (data1 === "fixed"))){ -const err4 = {instancePath:instancePath+"/behavior",schemaPath:"#/properties/behavior/enum",keyword:"enum",params:{allowedValues: schema92.properties.behavior.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/behavior",schemaPath:"#/properties/behavior/enum",keyword:"enum",params:{allowedValues: schema109.properties.behavior.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -4972,7 +7340,7 @@ vErrors.push(err6); errors++; } if(!((data3 === "solid") || (data3 === "transparent"))){ -const err7 = {instancePath:instancePath+"/overHero",schemaPath:"#/properties/overHero/enum",keyword:"enum",params:{allowedValues: schema92.properties.overHero.enum},message:"must be equal to one of the allowed values"}; +const err7 = {instancePath:instancePath+"/overHero",schemaPath:"#/properties/overHero/enum",keyword:"enum",params:{allowedValues: schema109.properties.overHero.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err7]; } @@ -4995,7 +7363,7 @@ vErrors.push(err8); errors++; } if(!(((data4 === "content") || (data4 === "wide")) || (data4 === "full"))){ -const err9 = {instancePath:instancePath+"/width",schemaPath:"#/properties/width/enum",keyword:"enum",params:{allowedValues: schema92.properties.width.enum},message:"must be equal to one of the allowed values"}; +const err9 = {instancePath:instancePath+"/width",schemaPath:"#/properties/width/enum",keyword:"enum",params:{allowedValues: schema109.properties.width.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err9]; } @@ -5018,7 +7386,7 @@ vErrors.push(err10); errors++; } if(!(((((data5 === "plain") || (data5 === "muted")) || (data5 === "raised")) || (data5 === "inverse")) || (data5 === "brand"))){ -const err11 = {instancePath:instancePath+"/surface",schemaPath:"#/properties/surface/enum",keyword:"enum",params:{allowedValues: schema92.properties.surface.enum},message:"must be equal to one of the allowed values"}; +const err11 = {instancePath:instancePath+"/surface",schemaPath:"#/properties/surface/enum",keyword:"enum",params:{allowedValues: schema109.properties.surface.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err11]; } @@ -5041,7 +7409,7 @@ vErrors.push(err12); errors++; } if(!((((data6 === "dropdown") || (data6 === "drawer-start")) || (data6 === "drawer-end")) || (data6 === "fullscreen"))){ -const err13 = {instancePath:instancePath+"/mobileStyle",schemaPath:"#/properties/mobileStyle/enum",keyword:"enum",params:{allowedValues: schema92.properties.mobileStyle.enum},message:"must be equal to one of the allowed values"}; +const err13 = {instancePath:instancePath+"/mobileStyle",schemaPath:"#/properties/mobileStyle/enum",keyword:"enum",params:{allowedValues: schema109.properties.mobileStyle.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err13]; } @@ -5064,7 +7432,7 @@ vErrors.push(err14); errors++; } if(!((((data7 === "sm") || (data7 === "md")) || (data7 === "lg")) || (data7 === "always"))){ -const err15 = {instancePath:instancePath+"/collapseAt",schemaPath:"#/properties/collapseAt/enum",keyword:"enum",params:{allowedValues: schema92.properties.collapseAt.enum},message:"must be equal to one of the allowed values"}; +const err15 = {instancePath:instancePath+"/collapseAt",schemaPath:"#/properties/collapseAt/enum",keyword:"enum",params:{allowedValues: schema109.properties.collapseAt.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err15]; } @@ -5216,7 +7584,7 @@ vErrors.push(err27); errors++; } if(!(((((data12 === "button") || (data12 === "search")) || (data12 === "locale")) || (data12 === "theme-toggle")) || (data12 === "motion-toggle"))){ -const err28 = {instancePath:instancePath+"/actions/" + i0+"/kind",schemaPath:"#/$defs/HeaderAction/properties/kind/enum",keyword:"enum",params:{allowedValues: schema95.properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err28 = {instancePath:instancePath+"/actions/" + i0+"/kind",schemaPath:"#/$defs/HeaderAction/properties/kind/enum",keyword:"enum",params:{allowedValues: schema112.properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err28]; } @@ -5309,7 +7677,7 @@ vErrors.push(err35); errors++; } if(!((data15 === "primary") || (data15 === "secondary"))){ -const err36 = {instancePath:instancePath+"/actions/" + i0+"/variant",schemaPath:"#/$defs/HeaderAction/properties/variant/enum",keyword:"enum",params:{allowedValues: schema95.properties.variant.enum},message:"must be equal to one of the allowed values"}; +const err36 = {instancePath:instancePath+"/actions/" + i0+"/variant",schemaPath:"#/$defs/HeaderAction/properties/variant/enum",keyword:"enum",params:{allowedValues: schema112.properties.variant.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err36]; } @@ -5354,18 +7722,18 @@ vErrors.push(err39); } errors++; } -validate64.errors = vErrors; +validate80.errors = vErrors; return errors === 0; } -validate64.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate80.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const schema96 = {"type":"object","additionalProperties":false,"properties":{"layout":{"type":"string","enum":["simple","columns","cta-band","minimal"]},"surface":{"type":"string","enum":["plain","muted","inverse"]},"tagline":{"type":"string","minLength":1,"maxLength":200},"columns":{"type":"array","maxItems":5,"items":{"$ref":"#/$defs/FooterColumn"}},"ctaHeading":{"type":"string","minLength":1,"maxLength":120},"ctaLabel":{"type":"string","minLength":1,"maxLength":40},"ctaHref":{"type":"string","minLength":1,"maxLength":2048},"social":{"type":"boolean"},"copyright":{"type":"string","minLength":1,"maxLength":200,"description":"The legal line; {year} and {site} are the only placeholders."},"legal":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"extras":{"type":"array","maxItems":3,"uniqueItems":true,"items":{"type":"string","enum":["locale","theme-toggle","motion-toggle"]}},"credit":{"type":"boolean"},"region":{"$ref":"#/$defs/Section","description":"RFC-0025 §2: one optional surface (kind `surface`) above the footer's columns, built and checked like any surface; the store refuses other section kinds."}}}; -const schema97 = {"type":"object","additionalProperties":false,"required":["heading","menu"],"properties":{"heading":{"type":"string","minLength":1,"maxLength":60},"menu":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."}}}; +const schema113 = {"type":"object","additionalProperties":false,"properties":{"layout":{"type":"string","enum":["simple","columns","cta-band","minimal"]},"surface":{"type":"string","enum":["plain","muted","inverse"]},"tagline":{"type":"string","minLength":1,"maxLength":200},"columns":{"type":"array","maxItems":5,"items":{"$ref":"#/$defs/FooterColumn"}},"ctaHeading":{"type":"string","minLength":1,"maxLength":120},"ctaLabel":{"type":"string","minLength":1,"maxLength":40},"ctaHref":{"type":"string","minLength":1,"maxLength":2048},"social":{"type":"boolean"},"copyright":{"type":"string","minLength":1,"maxLength":200,"description":"The legal line; {year} and {site} are the only placeholders."},"legal":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"extras":{"type":"array","maxItems":3,"uniqueItems":true,"items":{"type":"string","enum":["locale","theme-toggle","motion-toggle"]}},"credit":{"type":"boolean"},"region":{"$ref":"#/$defs/Section","description":"RFC-0025 §2: one optional surface (kind `surface`) above the footer's columns, built and checked like any surface; the store refuses other section kinds."}}}; +const schema114 = {"type":"object","additionalProperties":false,"required":["heading","menu"],"properties":{"heading":{"type":"string","minLength":1,"maxLength":60},"menu":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."}}}; -function validate67(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate83(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate67.evaluated; +const evaluated0 = validate83.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -5496,29 +7864,28 @@ vErrors.push(err10); } errors++; } -validate67.errors = vErrors; +validate83.errors = vErrors; return errors === 0; } -validate67.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate83.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const schema100 = {"title":"Section","description":"A block inside a page, discriminated by `kind`. Variants are written inline (not via $ref) and `kind` is a single-value string enum so that TypeScript gets a literal union and Rust gets a serde internally-tagged enum.","oneOf":[{"type":"object","title":"HeroSection","additionalProperties":false,"required":["id","kind","heading"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["hero"]},"heading":{"type":"string","minLength":1,"maxLength":300},"subheading":{"type":"string","maxLength":1000},"image":{"$ref":"#/$defs/ImageRef"},"cta":{"$ref":"#/$defs/Link"}}},{"type":"object","title":"RichTextSection","additionalProperties":false,"required":["id","kind","format","body"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["richText"]},"format":{"type":"string","enum":["markdown","html"]},"body":{"type":"string","maxLength":200000},"profile":{"$ref":"#/$defs/BodyProfile"},"blocks":{"$ref":"#/$defs/BodyBlocks"}}},{"type":"object","title":"ImageSection","additionalProperties":false,"required":["id","kind","image"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["image"]},"image":{"$ref":"#/$defs/ImageRef"},"caption":{"type":"string","maxLength":1000}}},{"type":"object","title":"ContentListSection","description":"Renders the newest entries of a content collection (e.g. blog posts).","additionalProperties":false,"required":["id","kind","collection","limit","sort"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["contentList"]},"collection":{"$ref":"#/$defs/Slug"},"limit":{"type":"integer","minimum":1,"maximum":100},"sort":{"type":"string","enum":["newest","oldest","title"]},"filter":{"type":"string","description":"Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'.","minLength":1,"maxLength":512},"where":{"type":"array","description":"RFC-0042: conditions on the collection's list fields, ANDed.","minItems":1,"maxItems":4,"items":{"$ref":"#/$defs/FieldCondition"}},"orderBy":{"$ref":"#/$defs/FieldOrder"}}},{"type":"object","title":"ComponentSection","description":"A typed block from the block registry (RFC-0015): `type` names a registered block type (one global namespace of English names), `props` follow that type's props schema, which the store validates.","additionalProperties":false,"required":["id","kind","type","props"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["component"]},"type":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,40}$"},"props":{"type":"object","description":"Validated against the type's props schema by the store, not by this contract.","additionalProperties":true}}},{"type":"object","title":"SurfaceSection","description":"A generative UI surface (RFC-0020): a flat list of catalog components with a `root`, plus a data model their props may bind to. Written and streamed by agents as data, never code; the store checks the tree, the catalog and the limits.","additionalProperties":false,"required":["id","kind","catalog","components"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["surface"]},"catalog":{"type":"string","description":"The catalog the surface was written for: `@` (RFC-0018 manifest).","pattern":"^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"},"components":{"type":"array","minItems":1,"maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponent"}},"data":{"type":"object","description":"The surface's data model: JSON its components bind to with `{path}` (at most 64 KB serialized; the store enforces the size).","additionalProperties":true}}}]}; -const schema102 = {"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}}; -const schema104 = {"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."}; -const schema114 = {"type":"object","description":"RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`.","additionalProperties":false,"required":["field","op"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","in","set","unset"]},"value":{"type":"string","minLength":1,"maxLength":100},"values":{"type":"array","minItems":1,"maxItems":20,"uniqueItems":true,"items":{"type":"string","minLength":1,"maxLength":100}}}}; -const schema115 = {"type":"object","description":"RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last.","additionalProperties":false,"required":["field","direction"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"direction":{"type":"string","enum":["asc","desc"]}}}; -const pattern58 = new RegExp("^[a-z][a-z0-9-]{1,40}$", "u"); -const pattern60 = new RegExp("^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$", "u"); -const schema105 = {"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}}; -const schema106 = {"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}}; -const pattern49 = new RegExp("^blk_[0-9A-Za-z]{8,24}$", "u"); -const pattern50 = new RegExp("^[0-9a-f]{64}$", "u"); -const schema107 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; -const schema108 = {"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]}; +const schema117 = {"title":"Section","description":"A block inside a page, discriminated by `kind`. Variants are written inline (not via $ref) and `kind` is a single-value string enum so that TypeScript gets a literal union and Rust gets a serde internally-tagged enum.","oneOf":[{"type":"object","title":"HeroSection","additionalProperties":false,"required":["id","kind","heading"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["hero"]},"heading":{"type":"string","minLength":1,"maxLength":300},"subheading":{"type":"string","maxLength":1000},"image":{"$ref":"#/$defs/ImageRef"},"cta":{"$ref":"#/$defs/Link"}}},{"type":"object","title":"RichTextSection","additionalProperties":false,"required":["id","kind","format","body"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["richText"]},"format":{"type":"string","enum":["markdown","html"]},"body":{"type":"string","maxLength":200000},"profile":{"$ref":"#/$defs/BodyProfile"},"blocks":{"$ref":"#/$defs/BodyBlocks"}}},{"type":"object","title":"ImageSection","additionalProperties":false,"required":["id","kind","image"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["image"]},"image":{"$ref":"#/$defs/ImageRef"},"caption":{"type":"string","maxLength":1000}}},{"type":"object","title":"ContentListSection","description":"Renders the newest entries of a content collection (e.g. blog posts).","additionalProperties":false,"required":["id","kind","collection","limit","sort"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["contentList"]},"collection":{"$ref":"#/$defs/Slug"},"limit":{"type":"integer","minimum":1,"maximum":100},"sort":{"type":"string","enum":["newest","oldest","title"]},"filter":{"type":"string","description":"Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'.","minLength":1,"maxLength":512},"where":{"type":"array","description":"RFC-0042: conditions on the collection's list fields, ANDed.","minItems":1,"maxItems":4,"items":{"$ref":"#/$defs/FieldCondition"}},"orderBy":{"$ref":"#/$defs/FieldOrder"}}},{"type":"object","title":"ComponentSection","description":"A typed block from the block registry (RFC-0015): `type` names a registered block type (one global namespace of English names), `props` follow that type's props schema, which the store validates.","additionalProperties":false,"required":["id","kind","type","props"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["component"]},"type":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,40}$"},"props":{"type":"object","description":"Validated against the type's props schema by the store, not by this contract.","additionalProperties":true}}},{"type":"object","title":"SurfaceSection","description":"A generative UI surface (RFC-0020): a flat list of catalog components with a `root`, plus a data model their props may bind to. Written and streamed by agents as data, never code; the store checks the tree, the catalog and the limits.","additionalProperties":false,"required":["id","kind","catalog","components"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["surface"]},"catalog":{"type":"string","description":"The catalog the surface was written for: `@` (RFC-0018 manifest).","pattern":"^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"},"components":{"type":"array","minItems":1,"maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponent"}},"data":{"type":"object","description":"The surface's data model: JSON its components bind to with `{path}` (at most 64 KB serialized; the store enforces the size).","additionalProperties":true}}}]}; +const schema119 = {"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}}; +const schema121 = {"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."}; +const schema132 = {"type":"object","description":"RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last.","additionalProperties":false,"required":["field","direction"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"direction":{"type":"string","enum":["asc","desc"]}}}; +const pattern74 = new RegExp("^[a-z][a-z0-9-]{1,40}$", "u"); +const pattern76 = new RegExp("^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$", "u"); +const schema122 = {"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}}; +const schema123 = {"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}}; +const pattern65 = new RegExp("^blk_[0-9A-Za-z]{8,24}$", "u"); +const pattern66 = new RegExp("^[0-9a-f]{64}$", "u"); +const schema124 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; +const schema125 = {"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]}; -function validate73(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate89(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate73.evaluated; +const evaluated0 = validate89.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -5571,7 +7938,7 @@ vErrors.push(err3); errors++; } if(!(((data0 === "human") || (data0 === "agent")) || (data0 === "system"))){ -const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema108.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema125.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -5662,16 +8029,16 @@ vErrors.push(err11); } errors++; } -validate73.errors = vErrors; +validate89.errors = vErrors; return errors === 0; } -validate73.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate89.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate72(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate88(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate72.evaluated; +const evaluated0 = validate88.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -5714,7 +8081,7 @@ errors++; if(data.id !== undefined){ let data0 = data.id; if(typeof data0 === "string"){ -if(!pattern49.test(data0)){ +if(!pattern65.test(data0)){ const err3 = {instancePath:instancePath+"/id",schemaPath:"#/properties/id/pattern",keyword:"pattern",params:{pattern: "^blk_[0-9A-Za-z]{8,24}$"},message:"must match pattern \""+"^blk_[0-9A-Za-z]{8,24}$"+"\""}; if(vErrors === null){ vErrors = [err3]; @@ -5739,7 +8106,7 @@ errors++; if(data.hash !== undefined){ let data1 = data.hash; if(typeof data1 === "string"){ -if(!pattern50.test(data1)){ +if(!pattern66.test(data1)){ const err5 = {instancePath:instancePath+"/hash",schemaPath:"#/properties/hash/pattern",keyword:"pattern",params:{pattern: "^[0-9a-f]{64}$"},message:"must match pattern \""+"^[0-9a-f]{64}$"+"\""}; if(vErrors === null){ vErrors = [err5]; @@ -5762,8 +8129,8 @@ errors++; } } if(data.by !== undefined){ -if(!(validate73(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate73.errors : vErrors.concat(validate73.errors); +if(!(validate89(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate89.errors : vErrors.concat(validate89.errors); errors = vErrors.length; } } @@ -5780,7 +8147,7 @@ vErrors.push(err7); errors++; } if(!((((((data3 === "studio") || (data3 === "mcp")) || (data3 === "api")) || (data3 === "import")) || (data3 === "ai-command")) || (data3 === "proposal"))){ -const err8 = {instancePath:instancePath+"/via",schemaPath:"#/properties/via/enum",keyword:"enum",params:{allowedValues: schema106.properties.via.enum},message:"must be equal to one of the allowed values"}; +const err8 = {instancePath:instancePath+"/via",schemaPath:"#/properties/via/enum",keyword:"enum",params:{allowedValues: schema123.properties.via.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err8]; } @@ -5826,16 +8193,16 @@ vErrors.push(err11); } errors++; } -validate72.errors = vErrors; +validate88.errors = vErrors; return errors === 0; } -validate72.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate88.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate71(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate87(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate71.evaluated; +const evaluated0 = validate87.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -5855,8 +8222,8 @@ errors++; } const len0 = data.length; for(let i0=0; i0*'.","minLength":1,"maxLength":512},"where":{"type":"array","description":"RFC-0042: conditions on the collection's list fields, ANDed.","minItems":1,"maxItems":4,"items":{"$ref":"#/$defs/FieldCondition"}},"orderBy":{"$ref":"#/$defs/FieldOrder"}}},{"type":"object","title":"ComponentSection","description":"A typed block from the block registry (RFC-0015): `type` names a registered block type (one global namespace of English names), `props` follow that type's props schema, which the store validates.","additionalProperties":false,"required":["id","kind","type","props"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["component"]},"type":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,40}$"},"props":{"type":"object","description":"Validated against the type's props schema by the store, not by this contract.","additionalProperties":true}}},{"type":"object","title":"SurfaceSection","description":"A generative UI surface (RFC-0020): a flat list of catalog components with a `root`, plus a data model their props may bind to. Written and streamed by agents as data, never code; the store checks the tree, the catalog and the limits.","additionalProperties":false,"required":["id","kind","catalog","components"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["surface"]},"catalog":{"type":"string","description":"The catalog the surface was written for: `@` (RFC-0018 manifest).","pattern":"^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"},"components":{"type":"array","minItems":1,"maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponent"}},"data":{"type":"object","description":"The surface's data model: JSON its components bind to with `{path}` (at most 64 KB serialized; the store enforces the size).","additionalProperties":true}}}]},"Seo":{"type":"object","description":"Search and social metadata of a routable node (page, term archive).","additionalProperties":false,"properties":{"title":{"type":"string","maxLength":300},"description":{"type":"string","maxLength":500},"noindex":{"type":"boolean"},"canonical":{"type":"string","maxLength":2048},"ogImage":{"$ref":"#/$defs/ImageRef"}}},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"SurfaceComponent":{"type":"object","description":"One element of a generative UI surface (RFC-0020 §1), in a flat list: `component` names a catalog entry (a PascalCase built-in primitive such as `Stack`, or a registry block type such as `callout`), `children` refer to other ids of the same surface, and `props` follow the entry's schema, which the store checks. Field names follow A2UI v0.9.","additionalProperties":false,"required":["id","component"],"properties":{"id":{"$ref":"#/$defs/SurfaceComponentId"},"component":{"type":"string","pattern":"^(?:[A-Z][A-Za-z0-9]{1,40}|[a-z][a-z0-9-]{1,40})$"},"children":{"type":"array","maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponentId"}},"props":{"type":"object","description":"Literal values, `{path}` bindings into the surface's data, or `{call}` named functions; validated by the store against the catalog, not by this contract.","additionalProperties":true},"visible":{"type":"object","description":"A visibility condition over the surface's data (`eq`, `ne`, `gt`, `lt`, `and`, `or`, `not`); validated by the store.","additionalProperties":true},"motion":{"type":"object","description":"Motion presets for this component (RFC-0024 §2): `reveal`, `hover`, `speed`, `delay`, `stagger`, `replay` and later `scroll`, `loop`; enums and clamped numbers only, validated by the store against the catalog. The renderer turns them into CSS; agents never write CSS.","additionalProperties":true}}},"SurfaceComponentId":{"type":"string","description":"A component's id, unique within its surface; `root` is the tree's root (RFC-0020 §1).","pattern":"^[a-z][a-z0-9-]{0,40}$"},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"},"Translation":{"type":"object","description":"Links a page or entry to the other language versions of the same work (RFC-0033 §2). Staleness is computed: the source's current hash differs from source.hash.","additionalProperties":false,"required":["group","method"],"properties":{"group":{"$ref":"#/$defs/Id","description":"The group's id: the original node's id, shared by every translation. May cross sites in one project."},"method":{"$ref":"#/$defs/TranslationMethod"},"layout":{"type":"string","description":"linked: the original's structural changes propagate to this translation as drafts (RFC-0033 §4a); independent: nothing propagates. Absent means linked.","enum":["linked","independent"]},"source":{"type":"object","description":"The node and revision this translation was made from. Absent on the original.","additionalProperties":false,"required":["id","locale","rev","hash"],"properties":{"id":{"$ref":"#/$defs/Id"},"siteId":{"$ref":"#/$defs/Id","description":"The source's site when it is another site of the same project (a language edition). Absent means this site."},"locale":{"$ref":"#/$defs/Locale"},"rev":{"$ref":"#/$defs/Revision"},"hash":{"type":"string","description":"Lower-case hex SHA-256 of the source node's canonical JSON at that revision.","pattern":"^[0-9a-f]{64}$"}}},"provenance":{"type":"object","description":"The machine-translation record: engine, glossary revision, checks run, quality score. Required when method is machine or machine-reviewed.","additionalProperties":false,"required":["engine","at"],"properties":{"engine":{"type":"string","minLength":1,"maxLength":200},"glossaryRev":{"$ref":"#/$defs/Revision"},"score":{"type":"number","minimum":0,"maximum":100},"checks":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":50},"at":{"$ref":"#/$defs/Timestamp"}}},"review":{"type":"object","description":"The person who reviewed this translation and when. Only humans set it.","additionalProperties":false,"required":["by","at"],"properties":{"by":{"$ref":"#/$defs/Actor"},"at":{"$ref":"#/$defs/Timestamp"}}}}},"TranslationMethod":{"type":"string","description":"How a page's or entry's text came to be (RFC-0033). 'original' marks the source of a translation group; 'machine-reviewed' is machine output a person has reviewed.","enum":["original","human","machine","machine-reviewed"]}}}; -const schema131 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; -const schema132 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; -const schema134 = {"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"}; -const schema135 = {"type":"string","description":"Editorial lifecycle of a page or content entry.","enum":["draft","review","published","archived"]}; -const schema163 = {"type":"string","description":"How much a page may move (RFC-0024 §1); the reader's reduced-motion preference and pause control can only lower it.","enum":["none","subtle","standard","expressive"]}; -const schema164 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; -const schema148 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; -const schema177 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; -const schema136 = {"title":"Section","description":"A block inside a page, discriminated by `kind`. Variants are written inline (not via $ref) and `kind` is a single-value string enum so that TypeScript gets a literal union and Rust gets a serde internally-tagged enum.","oneOf":[{"type":"object","title":"HeroSection","additionalProperties":false,"required":["id","kind","heading"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["hero"]},"heading":{"type":"string","minLength":1,"maxLength":300},"subheading":{"type":"string","maxLength":1000},"image":{"$ref":"#/$defs/ImageRef"},"cta":{"$ref":"#/$defs/Link"}}},{"type":"object","title":"RichTextSection","additionalProperties":false,"required":["id","kind","format","body"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["richText"]},"format":{"type":"string","enum":["markdown","html"]},"body":{"type":"string","maxLength":200000},"profile":{"$ref":"#/$defs/BodyProfile"},"blocks":{"$ref":"#/$defs/BodyBlocks"}}},{"type":"object","title":"ImageSection","additionalProperties":false,"required":["id","kind","image"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["image"]},"image":{"$ref":"#/$defs/ImageRef"},"caption":{"type":"string","maxLength":1000}}},{"type":"object","title":"ContentListSection","description":"Renders the newest entries of a content collection (e.g. blog posts).","additionalProperties":false,"required":["id","kind","collection","limit","sort"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["contentList"]},"collection":{"$ref":"#/$defs/Slug"},"limit":{"type":"integer","minimum":1,"maximum":100},"sort":{"type":"string","enum":["newest","oldest","title"]},"filter":{"type":"string","description":"Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'.","minLength":1,"maxLength":512},"where":{"type":"array","description":"RFC-0042: conditions on the collection's list fields, ANDed.","minItems":1,"maxItems":4,"items":{"$ref":"#/$defs/FieldCondition"}},"orderBy":{"$ref":"#/$defs/FieldOrder"}}},{"type":"object","title":"ComponentSection","description":"A typed block from the block registry (RFC-0015): `type` names a registered block type (one global namespace of English names), `props` follow that type's props schema, which the store validates.","additionalProperties":false,"required":["id","kind","type","props"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["component"]},"type":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,40}$"},"props":{"type":"object","description":"Validated against the type's props schema by the store, not by this contract.","additionalProperties":true}}},{"type":"object","title":"SurfaceSection","description":"A generative UI surface (RFC-0020): a flat list of catalog components with a `root`, plus a data model their props may bind to. Written and streamed by agents as data, never code; the store checks the tree, the catalog and the limits.","additionalProperties":false,"required":["id","kind","catalog","components"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["surface"]},"catalog":{"type":"string","description":"The catalog the surface was written for: `@` (RFC-0018 manifest).","pattern":"^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"},"components":{"type":"array","minItems":1,"maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponent"}},"data":{"type":"object","description":"The surface's data model: JSON its components bind to with `{path}` (at most 64 KB serialized; the store enforces the size).","additionalProperties":true}}}]}; -const schema140 = {"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}}; -const schema142 = {"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."}; -const schema151 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; -const schema152 = {"type":"object","description":"RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`.","additionalProperties":false,"required":["field","op"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","in","set","unset"]},"value":{"type":"string","minLength":1,"maxLength":100},"values":{"type":"array","minItems":1,"maxItems":20,"uniqueItems":true,"items":{"type":"string","minLength":1,"maxLength":100}}}}; -const schema153 = {"type":"object","description":"RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last.","additionalProperties":false,"required":["field","direction"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"direction":{"type":"string","enum":["asc","desc"]}}}; -const schema138 = {"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}}; +export const validatePage = validate101; +const schema147 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/page.schema.json","title":"Page","description":"A routable page of a site: a path, editorial status and an ordered list of sections. Sections are embedded (not separate nodes) so a page is the unit of locking, snapshotting and diffing.","type":"object","additionalProperties":false,"required":["schemaVersion","id","siteId","path","title","locale","status","sections","rev","createdAt","updatedAt"],"properties":{"schemaVersion":{"$ref":"#/$defs/SchemaVersion"},"id":{"$ref":"#/$defs/Id"},"siteId":{"$ref":"#/$defs/Id"},"path":{"type":"string","description":"Absolute URL path, starting with '/', no trailing slash except root.","pattern":"^/(?:[a-z0-9]+(?:-[a-z0-9]+)*(?:/[a-z0-9]+(?:-[a-z0-9]+)*)*)?$","maxLength":1024},"title":{"type":"string","minLength":1,"maxLength":300},"locale":{"$ref":"#/$defs/Locale"},"status":{"$ref":"#/$defs/PublishStatus"},"sections":{"type":"array","items":{"$ref":"#/$defs/Section"}},"seo":{"$ref":"#/$defs/Seo"},"availability":{"$ref":"#/$defs/Availability"},"chrome":{"type":"object","description":"This page's own header and footer (RFC-0025 §1): a landing page without the menu, a checkout without the footer.","additionalProperties":false,"properties":{"header":{"type":"string","enum":["default","transparent","hidden"]},"footer":{"type":"string","enum":["default","minimal","hidden"]}}},"motion":{"$ref":"#/$defs/MotionLevel","description":"Overrides the site's motion level for this page, up or down (RFC-0024 §1); absent follows the site."},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"translation":{"$ref":"#/$defs/Translation"},"metadata":{"$ref":"#/$defs/Metadata"}},"$defs":{"Actor":{"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}},"ActorKind":{"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]},"Availability":{"type":"object","description":"Time gates of a page or content entry (RFC-0008): the published site shows the node only from `from` (inclusive: an embargo) until `until` (exclusive: an expiry), checked at render time, so the gates hold even when no scheduler runs. Before `from` and from `until` on, the node is absent from every public route, list, feed, sitemap and search output; previews still show it. At least one gate; writers check that `from` is before `until`.","additionalProperties":false,"minProperties":1,"properties":{"from":{"$ref":"#/$defs/Timestamp"},"until":{"$ref":"#/$defs/Timestamp"}}},"BodyBlock":{"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}},"BodyBlocks":{"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}},"BodyProfile":{"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."},"FieldCondition":{"type":"object","description":"RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`.","additionalProperties":false,"required":["field","op"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","in","set","unset"]},"value":{"type":"string","minLength":1,"maxLength":100},"values":{"type":"array","minItems":1,"maxItems":20,"uniqueItems":true,"items":{"type":"string","minLength":1,"maxLength":100}}}},"FieldOrder":{"type":"object","description":"RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last.","additionalProperties":false,"required":["field","direction"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"direction":{"type":"string","enum":["asc","desc"]}}},"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"ImageRef":{"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}},"Link":{"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"MotionLevel":{"type":"string","description":"How much a page may move (RFC-0024 §1); the reader's reduced-motion preference and pause control can only lower it.","enum":["none","subtle","standard","expressive"]},"PublishStatus":{"type":"string","description":"Editorial lifecycle of a page or content entry.","enum":["draft","review","published","archived"]},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Section":{"title":"Section","description":"A block inside a page, discriminated by `kind`. Variants are written inline (not via $ref) and `kind` is a single-value string enum so that TypeScript gets a literal union and Rust gets a serde internally-tagged enum.","oneOf":[{"type":"object","title":"HeroSection","additionalProperties":false,"required":["id","kind","heading"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["hero"]},"heading":{"type":"string","minLength":1,"maxLength":300},"subheading":{"type":"string","maxLength":1000},"image":{"$ref":"#/$defs/ImageRef"},"cta":{"$ref":"#/$defs/Link"}}},{"type":"object","title":"RichTextSection","additionalProperties":false,"required":["id","kind","format","body"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["richText"]},"format":{"type":"string","enum":["markdown","html"]},"body":{"type":"string","maxLength":200000},"profile":{"$ref":"#/$defs/BodyProfile"},"blocks":{"$ref":"#/$defs/BodyBlocks"}}},{"type":"object","title":"ImageSection","additionalProperties":false,"required":["id","kind","image"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["image"]},"image":{"$ref":"#/$defs/ImageRef"},"caption":{"type":"string","maxLength":1000}}},{"type":"object","title":"ContentListSection","description":"Renders the newest entries of a content collection (e.g. blog posts).","additionalProperties":false,"required":["id","kind","collection","limit","sort"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["contentList"]},"collection":{"$ref":"#/$defs/Slug"},"limit":{"type":"integer","minimum":1,"maximum":100},"sort":{"type":"string","enum":["newest","oldest","title"]},"filter":{"type":"string","description":"Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'.","minLength":1,"maxLength":512},"where":{"type":"array","description":"RFC-0042: conditions on the collection's list fields, ANDed.","minItems":1,"maxItems":4,"items":{"$ref":"#/$defs/FieldCondition"}},"orderBy":{"$ref":"#/$defs/FieldOrder"}}},{"type":"object","title":"ComponentSection","description":"A typed block from the block registry (RFC-0015): `type` names a registered block type (one global namespace of English names), `props` follow that type's props schema, which the store validates.","additionalProperties":false,"required":["id","kind","type","props"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["component"]},"type":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,40}$"},"props":{"type":"object","description":"Validated against the type's props schema by the store, not by this contract.","additionalProperties":true}}},{"type":"object","title":"SurfaceSection","description":"A generative UI surface (RFC-0020): a flat list of catalog components with a `root`, plus a data model their props may bind to. Written and streamed by agents as data, never code; the store checks the tree, the catalog and the limits.","additionalProperties":false,"required":["id","kind","catalog","components"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["surface"]},"catalog":{"type":"string","description":"The catalog the surface was written for: `@` (RFC-0018 manifest).","pattern":"^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"},"components":{"type":"array","minItems":1,"maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponent"}},"data":{"type":"object","description":"The surface's data model: JSON its components bind to with `{path}` (at most 64 KB serialized; the store enforces the size).","additionalProperties":true}}}]},"Seo":{"type":"object","description":"Search and social metadata of a routable node (page, term archive).","additionalProperties":false,"properties":{"title":{"type":"string","maxLength":300},"description":{"type":"string","maxLength":500},"noindex":{"type":"boolean"},"canonical":{"type":"string","maxLength":2048},"ogImage":{"$ref":"#/$defs/ImageRef"}}},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"SurfaceComponent":{"type":"object","description":"One element of a generative UI surface (RFC-0020 §1), in a flat list: `component` names a catalog entry (a PascalCase built-in primitive such as `Stack`, or a registry block type such as `callout`), `children` refer to other ids of the same surface, and `props` follow the entry's schema, which the store checks. Field names follow A2UI v0.9.","additionalProperties":false,"required":["id","component"],"properties":{"id":{"$ref":"#/$defs/SurfaceComponentId"},"component":{"type":"string","pattern":"^(?:[A-Z][A-Za-z0-9]{1,40}|[a-z][a-z0-9-]{1,40})$"},"children":{"type":"array","maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponentId"}},"props":{"type":"object","description":"Literal values, `{path}` bindings into the surface's data, or `{call}` named functions; validated by the store against the catalog, not by this contract.","additionalProperties":true},"visible":{"type":"object","description":"A visibility condition over the surface's data (`eq`, `ne`, `gt`, `lt`, `and`, `or`, `not`); validated by the store.","additionalProperties":true},"motion":{"type":"object","description":"Motion presets for this component (RFC-0024 §2): `reveal`, `hover`, `speed`, `delay`, `stagger`, `replay` and later `scroll`, `loop`; enums and clamped numbers only, validated by the store against the catalog. The renderer turns them into CSS; agents never write CSS.","additionalProperties":true}}},"SurfaceComponentId":{"type":"string","description":"A component's id, unique within its surface; `root` is the tree's root (RFC-0020 §1).","pattern":"^[a-z][a-z0-9-]{0,40}$"},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"},"Translation":{"type":"object","description":"Links a page or entry to the other language versions of the same work (RFC-0033 §2). Staleness is computed: the source's current hash differs from source.hash.","additionalProperties":false,"required":["group","method"],"properties":{"group":{"$ref":"#/$defs/Id","description":"The group's id: the original node's id, shared by every translation. May cross sites in one project."},"method":{"$ref":"#/$defs/TranslationMethod"},"layout":{"type":"string","description":"linked: the original's structural changes propagate to this translation as drafts (RFC-0033 §4a); independent: nothing propagates. Absent means linked.","enum":["linked","independent"]},"source":{"type":"object","description":"The node and revision this translation was made from. Absent on the original.","additionalProperties":false,"required":["id","locale","rev","hash"],"properties":{"id":{"$ref":"#/$defs/Id"},"siteId":{"$ref":"#/$defs/Id","description":"The source's site when it is another site of the same project (a language edition). Absent means this site."},"locale":{"$ref":"#/$defs/Locale"},"rev":{"$ref":"#/$defs/Revision"},"hash":{"type":"string","description":"Lower-case hex SHA-256 of the source node's canonical JSON at that revision.","pattern":"^[0-9a-f]{64}$"}}},"provenance":{"type":"object","description":"The machine-translation record: engine, glossary revision, checks run, quality score. Required when method is machine or machine-reviewed.","additionalProperties":false,"required":["engine","at"],"properties":{"engine":{"type":"string","minLength":1,"maxLength":200},"glossaryRev":{"$ref":"#/$defs/Revision"},"score":{"type":"number","minimum":0,"maximum":100},"checks":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":50},"at":{"$ref":"#/$defs/Timestamp"}}},"review":{"type":"object","description":"The person who reviewed this translation and when. Only humans set it.","additionalProperties":false,"required":["by","at"],"properties":{"by":{"$ref":"#/$defs/Actor"},"at":{"$ref":"#/$defs/Timestamp"}}}}},"TranslationMethod":{"type":"string","description":"How a page's or entry's text came to be (RFC-0033). 'original' marks the source of a translation group; 'machine-reviewed' is machine output a person has reviewed.","enum":["original","human","machine","machine-reviewed"]}}}; +const schema148 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; +const schema149 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; +const schema151 = {"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"}; +const schema152 = {"type":"string","description":"Editorial lifecycle of a page or content entry.","enum":["draft","review","published","archived"]}; +const schema180 = {"type":"string","description":"How much a page may move (RFC-0024 §1); the reader's reduced-motion preference and pause control can only lower it.","enum":["none","subtle","standard","expressive"]}; +const schema181 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema165 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema194 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const schema153 = {"title":"Section","description":"A block inside a page, discriminated by `kind`. Variants are written inline (not via $ref) and `kind` is a single-value string enum so that TypeScript gets a literal union and Rust gets a serde internally-tagged enum.","oneOf":[{"type":"object","title":"HeroSection","additionalProperties":false,"required":["id","kind","heading"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["hero"]},"heading":{"type":"string","minLength":1,"maxLength":300},"subheading":{"type":"string","maxLength":1000},"image":{"$ref":"#/$defs/ImageRef"},"cta":{"$ref":"#/$defs/Link"}}},{"type":"object","title":"RichTextSection","additionalProperties":false,"required":["id","kind","format","body"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["richText"]},"format":{"type":"string","enum":["markdown","html"]},"body":{"type":"string","maxLength":200000},"profile":{"$ref":"#/$defs/BodyProfile"},"blocks":{"$ref":"#/$defs/BodyBlocks"}}},{"type":"object","title":"ImageSection","additionalProperties":false,"required":["id","kind","image"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["image"]},"image":{"$ref":"#/$defs/ImageRef"},"caption":{"type":"string","maxLength":1000}}},{"type":"object","title":"ContentListSection","description":"Renders the newest entries of a content collection (e.g. blog posts).","additionalProperties":false,"required":["id","kind","collection","limit","sort"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["contentList"]},"collection":{"$ref":"#/$defs/Slug"},"limit":{"type":"integer","minimum":1,"maximum":100},"sort":{"type":"string","enum":["newest","oldest","title"]},"filter":{"type":"string","description":"Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'.","minLength":1,"maxLength":512},"where":{"type":"array","description":"RFC-0042: conditions on the collection's list fields, ANDed.","minItems":1,"maxItems":4,"items":{"$ref":"#/$defs/FieldCondition"}},"orderBy":{"$ref":"#/$defs/FieldOrder"}}},{"type":"object","title":"ComponentSection","description":"A typed block from the block registry (RFC-0015): `type` names a registered block type (one global namespace of English names), `props` follow that type's props schema, which the store validates.","additionalProperties":false,"required":["id","kind","type","props"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["component"]},"type":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,40}$"},"props":{"type":"object","description":"Validated against the type's props schema by the store, not by this contract.","additionalProperties":true}}},{"type":"object","title":"SurfaceSection","description":"A generative UI surface (RFC-0020): a flat list of catalog components with a `root`, plus a data model their props may bind to. Written and streamed by agents as data, never code; the store checks the tree, the catalog and the limits.","additionalProperties":false,"required":["id","kind","catalog","components"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["surface"]},"catalog":{"type":"string","description":"The catalog the surface was written for: `@` (RFC-0018 manifest).","pattern":"^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"},"components":{"type":"array","minItems":1,"maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponent"}},"data":{"type":"object","description":"The surface's data model: JSON its components bind to with `{path}` (at most 64 KB serialized; the store enforces the size).","additionalProperties":true}}}]}; +const schema157 = {"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}}; +const schema159 = {"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."}; +const schema168 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; +const schema169 = {"type":"object","description":"RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`.","additionalProperties":false,"required":["field","op"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","in","set","unset"]},"value":{"type":"string","minLength":1,"maxLength":100},"values":{"type":"array","minItems":1,"maxItems":20,"uniqueItems":true,"items":{"type":"string","minLength":1,"maxLength":100}}}}; +const schema170 = {"type":"object","description":"RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last.","additionalProperties":false,"required":["field","direction"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"direction":{"type":"string","enum":["asc","desc"]}}}; +const schema155 = {"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}}; -function validate87(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate103(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate87.evaluated; +const evaluated0 = validate103.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -10021,20 +12388,20 @@ vErrors.push(err16); } errors++; } -validate87.errors = vErrors; +validate103.errors = vErrors; return errors === 0; } -validate87.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate103.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const schema143 = {"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}}; -const schema144 = {"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}}; -const schema145 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; -const schema146 = {"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]}; +const schema160 = {"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}}; +const schema161 = {"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}}; +const schema162 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; +const schema163 = {"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]}; -function validate91(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate107(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate91.evaluated; +const evaluated0 = validate107.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -10087,7 +12454,7 @@ vErrors.push(err3); errors++; } if(!(((data0 === "human") || (data0 === "agent")) || (data0 === "system"))){ -const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema146.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema163.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -10178,16 +12545,16 @@ vErrors.push(err11); } errors++; } -validate91.errors = vErrors; +validate107.errors = vErrors; return errors === 0; } -validate91.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate107.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate90(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate106(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate90.evaluated; +const evaluated0 = validate106.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -10230,7 +12597,7 @@ errors++; if(data.id !== undefined){ let data0 = data.id; if(typeof data0 === "string"){ -if(!pattern49.test(data0)){ +if(!pattern65.test(data0)){ const err3 = {instancePath:instancePath+"/id",schemaPath:"#/properties/id/pattern",keyword:"pattern",params:{pattern: "^blk_[0-9A-Za-z]{8,24}$"},message:"must match pattern \""+"^blk_[0-9A-Za-z]{8,24}$"+"\""}; if(vErrors === null){ vErrors = [err3]; @@ -10255,7 +12622,7 @@ errors++; if(data.hash !== undefined){ let data1 = data.hash; if(typeof data1 === "string"){ -if(!pattern50.test(data1)){ +if(!pattern66.test(data1)){ const err5 = {instancePath:instancePath+"/hash",schemaPath:"#/properties/hash/pattern",keyword:"pattern",params:{pattern: "^[0-9a-f]{64}$"},message:"must match pattern \""+"^[0-9a-f]{64}$"+"\""}; if(vErrors === null){ vErrors = [err5]; @@ -10278,8 +12645,8 @@ errors++; } } if(data.by !== undefined){ -if(!(validate91(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate91.errors : vErrors.concat(validate91.errors); +if(!(validate107(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate107.errors : vErrors.concat(validate107.errors); errors = vErrors.length; } } @@ -10296,7 +12663,7 @@ vErrors.push(err7); errors++; } if(!((((((data3 === "studio") || (data3 === "mcp")) || (data3 === "api")) || (data3 === "import")) || (data3 === "ai-command")) || (data3 === "proposal"))){ -const err8 = {instancePath:instancePath+"/via",schemaPath:"#/properties/via/enum",keyword:"enum",params:{allowedValues: schema144.properties.via.enum},message:"must be equal to one of the allowed values"}; +const err8 = {instancePath:instancePath+"/via",schemaPath:"#/properties/via/enum",keyword:"enum",params:{allowedValues: schema161.properties.via.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err8]; } @@ -10342,16 +12709,16 @@ vErrors.push(err11); } errors++; } -validate90.errors = vErrors; +validate106.errors = vErrors; return errors === 0; } -validate90.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate106.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate89(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate105(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate89.evaluated; +const evaluated0 = validate105.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -10371,8 +12738,8 @@ errors++; } const len0 = data.length; for(let i0=0; i0*'.","minLength":1,"maxLength":512},"where":{"type":"array","description":"RFC-0042: conditions on the collection's list fields, ANDed.","minItems":1,"maxItems":4,"items":{"$ref":"#/$defs/FieldCondition"}},"orderBy":{"$ref":"#/$defs/FieldOrder"}}},{"type":"object","title":"ComponentSection","description":"A typed block from the block registry (RFC-0015): `type` names a registered block type (one global namespace of English names), `props` follow that type's props schema, which the store validates.","additionalProperties":false,"required":["id","kind","type","props"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["component"]},"type":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,40}$"},"props":{"type":"object","description":"Validated against the type's props schema by the store, not by this contract.","additionalProperties":true}}},{"type":"object","title":"SurfaceSection","description":"A generative UI surface (RFC-0020): a flat list of catalog components with a `root`, plus a data model their props may bind to. Written and streamed by agents as data, never code; the store checks the tree, the catalog and the limits.","additionalProperties":false,"required":["id","kind","catalog","components"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["surface"]},"catalog":{"type":"string","description":"The catalog the surface was written for: `@` (RFC-0018 manifest).","pattern":"^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"},"components":{"type":"array","minItems":1,"maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponent"}},"data":{"type":"object","description":"The surface's data model: JSON its components bind to with `{path}` (at most 64 KB serialized; the store enforces the size).","additionalProperties":true}}}],"$defs":{"Actor":{"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}},"ActorKind":{"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]},"BodyBlock":{"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}},"BodyBlocks":{"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}},"BodyProfile":{"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."},"FieldCondition":{"type":"object","description":"RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`.","additionalProperties":false,"required":["field","op"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","in","set","unset"]},"value":{"type":"string","minLength":1,"maxLength":100},"values":{"type":"array","minItems":1,"maxItems":20,"uniqueItems":true,"items":{"type":"string","minLength":1,"maxLength":100}}}},"FieldOrder":{"type":"object","description":"RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last.","additionalProperties":false,"required":["field","direction"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"direction":{"type":"string","enum":["asc","desc"]}}},"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"ImageRef":{"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}},"Link":{"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"SurfaceComponent":{"type":"object","description":"One element of a generative UI surface (RFC-0020 §1), in a flat list: `component` names a catalog entry (a PascalCase built-in primitive such as `Stack`, or a registry block type such as `callout`), `children` refer to other ids of the same surface, and `props` follow the entry's schema, which the store checks. Field names follow A2UI v0.9.","additionalProperties":false,"required":["id","component"],"properties":{"id":{"$ref":"#/$defs/SurfaceComponentId"},"component":{"type":"string","pattern":"^(?:[A-Z][A-Za-z0-9]{1,40}|[a-z][a-z0-9-]{1,40})$"},"children":{"type":"array","maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponentId"}},"props":{"type":"object","description":"Literal values, `{path}` bindings into the surface's data, or `{call}` named functions; validated by the store against the catalog, not by this contract.","additionalProperties":true},"visible":{"type":"object","description":"A visibility condition over the surface's data (`eq`, `ne`, `gt`, `lt`, `and`, `or`, `not`); validated by the store.","additionalProperties":true},"motion":{"type":"object","description":"Motion presets for this component (RFC-0024 §2): `reveal`, `hover`, `speed`, `delay`, `stagger`, `replay` and later `scroll`, `loop`; enums and clamped numbers only, validated by the store against the catalog. The renderer turns them into CSS; agents never write CSS.","additionalProperties":true}}},"SurfaceComponentId":{"type":"string","description":"A component's id, unique within its surface; `root` is the tree's root (RFC-0020 §1).","pattern":"^[a-z][a-z0-9-]{0,40}$"},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}}}; -const schema179 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; -const schema182 = {"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}}; -const schema184 = {"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."}; -const schema193 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; -const schema194 = {"type":"object","description":"RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`.","additionalProperties":false,"required":["field","op"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","in","set","unset"]},"value":{"type":"string","minLength":1,"maxLength":100},"values":{"type":"array","minItems":1,"maxItems":20,"uniqueItems":true,"items":{"type":"string","minLength":1,"maxLength":100}}}}; -const schema195 = {"type":"object","description":"RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last.","additionalProperties":false,"required":["field","direction"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"direction":{"type":"string","enum":["asc","desc"]}}}; -const schema180 = {"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}}; +export const validateSection = validate123; +const schema195 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/section.schema.json","title":"Section","description":"A block inside a page, discriminated by `kind`. Variants are written inline (not via $ref) and `kind` is a single-value string enum so that TypeScript gets a literal union and Rust gets a serde internally-tagged enum.","oneOf":[{"type":"object","title":"HeroSection","additionalProperties":false,"required":["id","kind","heading"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["hero"]},"heading":{"type":"string","minLength":1,"maxLength":300},"subheading":{"type":"string","maxLength":1000},"image":{"$ref":"#/$defs/ImageRef"},"cta":{"$ref":"#/$defs/Link"}}},{"type":"object","title":"RichTextSection","additionalProperties":false,"required":["id","kind","format","body"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["richText"]},"format":{"type":"string","enum":["markdown","html"]},"body":{"type":"string","maxLength":200000},"profile":{"$ref":"#/$defs/BodyProfile"},"blocks":{"$ref":"#/$defs/BodyBlocks"}}},{"type":"object","title":"ImageSection","additionalProperties":false,"required":["id","kind","image"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["image"]},"image":{"$ref":"#/$defs/ImageRef"},"caption":{"type":"string","maxLength":1000}}},{"type":"object","title":"ContentListSection","description":"Renders the newest entries of a content collection (e.g. blog posts).","additionalProperties":false,"required":["id","kind","collection","limit","sort"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["contentList"]},"collection":{"$ref":"#/$defs/Slug"},"limit":{"type":"integer","minimum":1,"maximum":100},"sort":{"type":"string","enum":["newest","oldest","title"]},"filter":{"type":"string","description":"Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'.","minLength":1,"maxLength":512},"where":{"type":"array","description":"RFC-0042: conditions on the collection's list fields, ANDed.","minItems":1,"maxItems":4,"items":{"$ref":"#/$defs/FieldCondition"}},"orderBy":{"$ref":"#/$defs/FieldOrder"}}},{"type":"object","title":"ComponentSection","description":"A typed block from the block registry (RFC-0015): `type` names a registered block type (one global namespace of English names), `props` follow that type's props schema, which the store validates.","additionalProperties":false,"required":["id","kind","type","props"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["component"]},"type":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,40}$"},"props":{"type":"object","description":"Validated against the type's props schema by the store, not by this contract.","additionalProperties":true}}},{"type":"object","title":"SurfaceSection","description":"A generative UI surface (RFC-0020): a flat list of catalog components with a `root`, plus a data model their props may bind to. Written and streamed by agents as data, never code; the store checks the tree, the catalog and the limits.","additionalProperties":false,"required":["id","kind","catalog","components"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["surface"]},"catalog":{"type":"string","description":"The catalog the surface was written for: `@` (RFC-0018 manifest).","pattern":"^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"},"components":{"type":"array","minItems":1,"maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponent"}},"data":{"type":"object","description":"The surface's data model: JSON its components bind to with `{path}` (at most 64 KB serialized; the store enforces the size).","additionalProperties":true}}}],"$defs":{"Actor":{"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}},"ActorKind":{"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]},"BodyBlock":{"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}},"BodyBlocks":{"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}},"BodyProfile":{"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."},"FieldCondition":{"type":"object","description":"RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`.","additionalProperties":false,"required":["field","op"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","in","set","unset"]},"value":{"type":"string","minLength":1,"maxLength":100},"values":{"type":"array","minItems":1,"maxItems":20,"uniqueItems":true,"items":{"type":"string","minLength":1,"maxLength":100}}}},"FieldOrder":{"type":"object","description":"RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last.","additionalProperties":false,"required":["field","direction"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"direction":{"type":"string","enum":["asc","desc"]}}},"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"ImageRef":{"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}},"Link":{"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"SurfaceComponent":{"type":"object","description":"One element of a generative UI surface (RFC-0020 §1), in a flat list: `component` names a catalog entry (a PascalCase built-in primitive such as `Stack`, or a registry block type such as `callout`), `children` refer to other ids of the same surface, and `props` follow the entry's schema, which the store checks. Field names follow A2UI v0.9.","additionalProperties":false,"required":["id","component"],"properties":{"id":{"$ref":"#/$defs/SurfaceComponentId"},"component":{"type":"string","pattern":"^(?:[A-Z][A-Za-z0-9]{1,40}|[a-z][a-z0-9-]{1,40})$"},"children":{"type":"array","maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponentId"}},"props":{"type":"object","description":"Literal values, `{path}` bindings into the surface's data, or `{call}` named functions; validated by the store against the catalog, not by this contract.","additionalProperties":true},"visible":{"type":"object","description":"A visibility condition over the surface's data (`eq`, `ne`, `gt`, `lt`, `and`, `or`, `not`); validated by the store.","additionalProperties":true},"motion":{"type":"object","description":"Motion presets for this component (RFC-0024 §2): `reveal`, `hover`, `speed`, `delay`, `stagger`, `replay` and later `scroll`, `loop`; enums and clamped numbers only, validated by the store against the catalog. The renderer turns them into CSS; agents never write CSS.","additionalProperties":true}}},"SurfaceComponentId":{"type":"string","description":"A component's id, unique within its surface; `root` is the tree's root (RFC-0020 §1).","pattern":"^[a-z][a-z0-9-]{0,40}$"},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}}}; +const schema196 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; +const schema199 = {"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}}; +const schema201 = {"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."}; +const schema210 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; +const schema211 = {"type":"object","description":"RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`.","additionalProperties":false,"required":["field","op"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","in","set","unset"]},"value":{"type":"string","minLength":1,"maxLength":100},"values":{"type":"array","minItems":1,"maxItems":20,"uniqueItems":true,"items":{"type":"string","minLength":1,"maxLength":100}}}}; +const schema212 = {"type":"object","description":"RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last.","additionalProperties":false,"required":["field","direction"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"direction":{"type":"string","enum":["asc","desc"]}}}; +const schema197 = {"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}}; -function validate108(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate124(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate108.evaluated; +const evaluated0 = validate124.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -14189,21 +16556,21 @@ vErrors.push(err16); } errors++; } -validate108.errors = vErrors; +validate124.errors = vErrors; return errors === 0; } -validate108.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate124.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const schema185 = {"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}}; -const schema186 = {"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}}; -const schema190 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; -const schema187 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; -const schema188 = {"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]}; +const schema202 = {"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}}; +const schema203 = {"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}}; +const schema207 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema204 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; +const schema205 = {"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]}; -function validate112(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate128(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate112.evaluated; +const evaluated0 = validate128.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -14256,7 +16623,7 @@ vErrors.push(err3); errors++; } if(!(((data0 === "human") || (data0 === "agent")) || (data0 === "system"))){ -const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema188.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema205.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -14347,16 +16714,16 @@ vErrors.push(err11); } errors++; } -validate112.errors = vErrors; +validate128.errors = vErrors; return errors === 0; } -validate112.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate128.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate111(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate127(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate111.evaluated; +const evaluated0 = validate127.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -14399,7 +16766,7 @@ errors++; if(data.id !== undefined){ let data0 = data.id; if(typeof data0 === "string"){ -if(!pattern49.test(data0)){ +if(!pattern65.test(data0)){ const err3 = {instancePath:instancePath+"/id",schemaPath:"#/properties/id/pattern",keyword:"pattern",params:{pattern: "^blk_[0-9A-Za-z]{8,24}$"},message:"must match pattern \""+"^blk_[0-9A-Za-z]{8,24}$"+"\""}; if(vErrors === null){ vErrors = [err3]; @@ -14424,7 +16791,7 @@ errors++; if(data.hash !== undefined){ let data1 = data.hash; if(typeof data1 === "string"){ -if(!pattern50.test(data1)){ +if(!pattern66.test(data1)){ const err5 = {instancePath:instancePath+"/hash",schemaPath:"#/properties/hash/pattern",keyword:"pattern",params:{pattern: "^[0-9a-f]{64}$"},message:"must match pattern \""+"^[0-9a-f]{64}$"+"\""}; if(vErrors === null){ vErrors = [err5]; @@ -14447,8 +16814,8 @@ errors++; } } if(data.by !== undefined){ -if(!(validate112(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate112.errors : vErrors.concat(validate112.errors); +if(!(validate128(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate128.errors : vErrors.concat(validate128.errors); errors = vErrors.length; } } @@ -14465,7 +16832,7 @@ vErrors.push(err7); errors++; } if(!((((((data3 === "studio") || (data3 === "mcp")) || (data3 === "api")) || (data3 === "import")) || (data3 === "ai-command")) || (data3 === "proposal"))){ -const err8 = {instancePath:instancePath+"/via",schemaPath:"#/properties/via/enum",keyword:"enum",params:{allowedValues: schema186.properties.via.enum},message:"must be equal to one of the allowed values"}; +const err8 = {instancePath:instancePath+"/via",schemaPath:"#/properties/via/enum",keyword:"enum",params:{allowedValues: schema203.properties.via.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err8]; } @@ -14511,16 +16878,16 @@ vErrors.push(err11); } errors++; } -validate111.errors = vErrors; +validate127.errors = vErrors; return errors === 0; } -validate111.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate127.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate110(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate126(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate110.evaluated; +const evaluated0 = validate126.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -14540,8 +16907,8 @@ errors++; } const len0 = data.length; for(let i0=0; i0.fields) checks each value against its definition on write; otherwise any JSON. An absent key means no value.","additionalProperties":true},"tags":{"type":"array","description":"Deprecated by RFC-0002: use `terms.tag`. Still accepted on input for one release and translated into terms; the store then writes []. Removed in schemaVersion 2.","deprecated":true,"items":{"$ref":"#/$defs/Slug"},"uniqueItems":true,"maxItems":50},"terms":{"type":"object","description":"Term assignments by taxonomy key (e.g. 'category', 'tag'). Terms are referenced by id, independent of locale. A taxonomy without terms is omitted, never an empty object.","propertyNames":{"$ref":"#/$defs/Slug"},"minProperties":1,"maxProperties":10,"additionalProperties":{"$ref":"#/$defs/TermAssignment"}},"author":{"$ref":"#/$defs/Actor"},"coverImage":{"$ref":"#/$defs/ImageRef"},"publishedAt":{"$ref":"#/$defs/Timestamp"},"availability":{"$ref":"#/$defs/Availability"},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"translation":{"$ref":"#/$defs/Translation"},"metadata":{"$ref":"#/$defs/Metadata"},"corrections":{"type":"array","description":"Corrections, clarifications and updates appended to the entry, oldest first (RFC-0019 §2). The store stamps `at`; an item that has been live can no longer be removed, and only a person may edit its note.","items":{"$ref":"#/$defs/Correction"},"maxItems":100}},"$defs":{"Actor":{"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}},"ActorKind":{"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]},"Availability":{"type":"object","description":"Time gates of a page or content entry (RFC-0008): the published site shows the node only from `from` (inclusive: an embargo) until `until` (exclusive: an expiry), checked at render time, so the gates hold even when no scheduler runs. Before `from` and from `until` on, the node is absent from every public route, list, feed, sitemap and search output; previews still show it. At least one gate; writers check that `from` is before `until`.","additionalProperties":false,"minProperties":1,"properties":{"from":{"$ref":"#/$defs/Timestamp"},"until":{"$ref":"#/$defs/Timestamp"}}},"Body":{"type":"object","additionalProperties":false,"required":["format","value"],"properties":{"format":{"type":"string","enum":["markdown","html"]},"value":{"type":"string","maxLength":1000000},"profile":{"$ref":"#/$defs/BodyProfile"},"blocks":{"$ref":"#/$defs/BodyBlocks"}}},"BodyBlock":{"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}},"BodyBlocks":{"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}},"BodyProfile":{"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."},"Correction":{"type":"object","description":"One correction notice (RFC-0019 §2).","additionalProperties":false,"required":["id","kind","at","note"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["correction","clarification","update"]},"at":{"$ref":"#/$defs/Timestamp","description":"When it was written; set by the store, never by the caller."},"note":{"type":"string","description":"What changed, in markdown (inline).","minLength":1,"maxLength":2000},"snapshotId":{"$ref":"#/$defs/Id","description":"The snapshot it was first published in."}}},"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"ImageRef":{"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"PublishStatus":{"type":"string","description":"Editorial lifecycle of a page or content entry.","enum":["draft","review","published","archived"]},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"TermAssignment":{"type":"object","description":"Terms of one taxonomy on one entry, in display order. The primary term is explicit and never derived from the order.","additionalProperties":false,"required":["ids"],"properties":{"ids":{"type":"array","items":{"$ref":"#/$defs/Id"},"minItems":1,"maxItems":50,"uniqueItems":true},"primary":{"$ref":"#/$defs/Id","description":"The entry's main term in this taxonomy (breadcrumbs, articleSection, later permalinks). Must be one of `ids`."}}},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"},"Translation":{"type":"object","description":"Links a page or entry to the other language versions of the same work (RFC-0033 §2). Staleness is computed: the source's current hash differs from source.hash.","additionalProperties":false,"required":["group","method"],"properties":{"group":{"$ref":"#/$defs/Id","description":"The group's id: the original node's id, shared by every translation. May cross sites in one project."},"method":{"$ref":"#/$defs/TranslationMethod"},"layout":{"type":"string","description":"linked: the original's structural changes propagate to this translation as drafts (RFC-0033 §4a); independent: nothing propagates. Absent means linked.","enum":["linked","independent"]},"source":{"type":"object","description":"The node and revision this translation was made from. Absent on the original.","additionalProperties":false,"required":["id","locale","rev","hash"],"properties":{"id":{"$ref":"#/$defs/Id"},"siteId":{"$ref":"#/$defs/Id","description":"The source's site when it is another site of the same project (a language edition). Absent means this site."},"locale":{"$ref":"#/$defs/Locale"},"rev":{"$ref":"#/$defs/Revision"},"hash":{"type":"string","description":"Lower-case hex SHA-256 of the source node's canonical JSON at that revision.","pattern":"^[0-9a-f]{64}$"}}},"provenance":{"type":"object","description":"The machine-translation record: engine, glossary revision, checks run, quality score. Required when method is machine or machine-reviewed.","additionalProperties":false,"required":["engine","at"],"properties":{"engine":{"type":"string","minLength":1,"maxLength":200},"glossaryRev":{"$ref":"#/$defs/Revision"},"score":{"type":"number","minimum":0,"maximum":100},"checks":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":50},"at":{"$ref":"#/$defs/Timestamp"}}},"review":{"type":"object","description":"The person who reviewed this translation and when. Only humans set it.","additionalProperties":false,"required":["by","at"],"properties":{"by":{"$ref":"#/$defs/Actor"},"at":{"$ref":"#/$defs/Timestamp"}}}}},"TranslationMethod":{"type":"string","description":"How a page's or entry's text came to be (RFC-0033). 'original' marks the source of a translation group; 'machine-reviewed' is machine output a person has reviewed.","enum":["original","human","machine","machine-reviewed"]}}}; -const schema202 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; -const schema203 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; -const schema205 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; -const schema207 = {"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"}; -const schema208 = {"type":"string","description":"Editorial lifecycle of a page or content entry.","enum":["draft","review","published","archived"]}; -const schema216 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; -const schema228 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; -const schema241 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; -const schema209 = {"type":"object","additionalProperties":false,"required":["format","value"],"properties":{"format":{"type":"string","enum":["markdown","html"]},"value":{"type":"string","maxLength":1000000},"profile":{"$ref":"#/$defs/BodyProfile"},"blocks":{"$ref":"#/$defs/BodyBlocks"}}}; -const schema210 = {"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."}; -const schema211 = {"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}}; -const schema212 = {"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}}; -const schema213 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; -const schema214 = {"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]}; +export const validateContentEntry = validate135; +const schema218 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/content-entry.schema.json","title":"ContentEntry","description":"A structured content item in a named collection (e.g. collection 'posts'). Core fields are typed; collection-specific fields live in `fields` and are validated by the collection's own schema in a later phase.","type":"object","additionalProperties":false,"required":["schemaVersion","id","siteId","collection","slug","locale","status","title","body","fields","tags","author","rev","createdAt","updatedAt"],"properties":{"schemaVersion":{"$ref":"#/$defs/SchemaVersion"},"id":{"$ref":"#/$defs/Id"},"siteId":{"$ref":"#/$defs/Id"},"collection":{"$ref":"#/$defs/Slug"},"slug":{"$ref":"#/$defs/Slug"},"locale":{"$ref":"#/$defs/Locale"},"status":{"$ref":"#/$defs/PublishStatus"},"title":{"type":"string","minLength":1,"maxLength":300},"summary":{"type":"string","maxLength":2000},"body":{"$ref":"#/$defs/Body"},"fields":{"type":"object","description":"Collection-specific values, keyed by field. A collection with typed fields (RFC-0041, Site.collections..fields) checks each value against its definition on write; otherwise any JSON. An absent key means no value. A reference value is a translation group id or a list of them, a repeater value a list of flat objects (RFC-0045).","additionalProperties":true},"tags":{"type":"array","description":"Deprecated by RFC-0002: use `terms.tag`. Still accepted on input for one release and translated into terms; the store then writes []. Removed in schemaVersion 2.","deprecated":true,"items":{"$ref":"#/$defs/Slug"},"uniqueItems":true,"maxItems":50},"terms":{"type":"object","description":"Term assignments by taxonomy key (e.g. 'category', 'tag'). Terms are referenced by id, independent of locale. A taxonomy without terms is omitted, never an empty object.","propertyNames":{"$ref":"#/$defs/Slug"},"minProperties":1,"maxProperties":10,"additionalProperties":{"$ref":"#/$defs/TermAssignment"}},"author":{"$ref":"#/$defs/Actor"},"coverImage":{"$ref":"#/$defs/ImageRef"},"publishedAt":{"$ref":"#/$defs/Timestamp"},"availability":{"$ref":"#/$defs/Availability"},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"translation":{"$ref":"#/$defs/Translation"},"metadata":{"$ref":"#/$defs/Metadata"},"corrections":{"type":"array","description":"Corrections, clarifications and updates appended to the entry, oldest first (RFC-0019 §2). The store stamps `at`; an item that has been live can no longer be removed, and only a person may edit its note.","items":{"$ref":"#/$defs/Correction"},"maxItems":100}},"$defs":{"Actor":{"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}},"ActorKind":{"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]},"Availability":{"type":"object","description":"Time gates of a page or content entry (RFC-0008): the published site shows the node only from `from` (inclusive: an embargo) until `until` (exclusive: an expiry), checked at render time, so the gates hold even when no scheduler runs. Before `from` and from `until` on, the node is absent from every public route, list, feed, sitemap and search output; previews still show it. At least one gate; writers check that `from` is before `until`.","additionalProperties":false,"minProperties":1,"properties":{"from":{"$ref":"#/$defs/Timestamp"},"until":{"$ref":"#/$defs/Timestamp"}}},"Body":{"type":"object","additionalProperties":false,"required":["format","value"],"properties":{"format":{"type":"string","enum":["markdown","html"]},"value":{"type":"string","maxLength":1000000},"profile":{"$ref":"#/$defs/BodyProfile"},"blocks":{"$ref":"#/$defs/BodyBlocks"}}},"BodyBlock":{"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}},"BodyBlocks":{"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}},"BodyProfile":{"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."},"Correction":{"type":"object","description":"One correction notice (RFC-0019 §2).","additionalProperties":false,"required":["id","kind","at","note"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["correction","clarification","update"]},"at":{"$ref":"#/$defs/Timestamp","description":"When it was written; set by the store, never by the caller."},"note":{"type":"string","description":"What changed, in markdown (inline).","minLength":1,"maxLength":2000},"snapshotId":{"$ref":"#/$defs/Id","description":"The snapshot it was first published in."}}},"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"ImageRef":{"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"PublishStatus":{"type":"string","description":"Editorial lifecycle of a page or content entry.","enum":["draft","review","published","archived"]},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"TermAssignment":{"type":"object","description":"Terms of one taxonomy on one entry, in display order. The primary term is explicit and never derived from the order.","additionalProperties":false,"required":["ids"],"properties":{"ids":{"type":"array","items":{"$ref":"#/$defs/Id"},"minItems":1,"maxItems":50,"uniqueItems":true},"primary":{"$ref":"#/$defs/Id","description":"The entry's main term in this taxonomy (breadcrumbs, articleSection, later permalinks). Must be one of `ids`."}}},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"},"Translation":{"type":"object","description":"Links a page or entry to the other language versions of the same work (RFC-0033 §2). Staleness is computed: the source's current hash differs from source.hash.","additionalProperties":false,"required":["group","method"],"properties":{"group":{"$ref":"#/$defs/Id","description":"The group's id: the original node's id, shared by every translation. May cross sites in one project."},"method":{"$ref":"#/$defs/TranslationMethod"},"layout":{"type":"string","description":"linked: the original's structural changes propagate to this translation as drafts (RFC-0033 §4a); independent: nothing propagates. Absent means linked.","enum":["linked","independent"]},"source":{"type":"object","description":"The node and revision this translation was made from. Absent on the original.","additionalProperties":false,"required":["id","locale","rev","hash"],"properties":{"id":{"$ref":"#/$defs/Id"},"siteId":{"$ref":"#/$defs/Id","description":"The source's site when it is another site of the same project (a language edition). Absent means this site."},"locale":{"$ref":"#/$defs/Locale"},"rev":{"$ref":"#/$defs/Revision"},"hash":{"type":"string","description":"Lower-case hex SHA-256 of the source node's canonical JSON at that revision.","pattern":"^[0-9a-f]{64}$"}}},"provenance":{"type":"object","description":"The machine-translation record: engine, glossary revision, checks run, quality score. Required when method is machine or machine-reviewed.","additionalProperties":false,"required":["engine","at"],"properties":{"engine":{"type":"string","minLength":1,"maxLength":200},"glossaryRev":{"$ref":"#/$defs/Revision"},"score":{"type":"number","minimum":0,"maximum":100},"checks":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":50},"at":{"$ref":"#/$defs/Timestamp"}}},"review":{"type":"object","description":"The person who reviewed this translation and when. Only humans set it.","additionalProperties":false,"required":["by","at"],"properties":{"by":{"$ref":"#/$defs/Actor"},"at":{"$ref":"#/$defs/Timestamp"}}}}},"TranslationMethod":{"type":"string","description":"How a page's or entry's text came to be (RFC-0033). 'original' marks the source of a translation group; 'machine-reviewed' is machine output a person has reviewed.","enum":["original","human","machine","machine-reviewed"]}}}; +const schema219 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; +const schema220 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; +const schema222 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; +const schema224 = {"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"}; +const schema225 = {"type":"string","description":"Editorial lifecycle of a page or content entry.","enum":["draft","review","published","archived"]}; +const schema233 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema245 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema258 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const schema226 = {"type":"object","additionalProperties":false,"required":["format","value"],"properties":{"format":{"type":"string","enum":["markdown","html"]},"value":{"type":"string","maxLength":1000000},"profile":{"$ref":"#/$defs/BodyProfile"},"blocks":{"$ref":"#/$defs/BodyBlocks"}}}; +const schema227 = {"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."}; +const schema228 = {"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}}; +const schema229 = {"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}}; +const schema230 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; +const schema231 = {"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]}; -function validate123(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate139(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate123.evaluated; +const evaluated0 = validate139.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -16667,7 +19034,7 @@ vErrors.push(err3); errors++; } if(!(((data0 === "human") || (data0 === "agent")) || (data0 === "system"))){ -const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema214.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema231.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -16758,16 +19125,16 @@ vErrors.push(err11); } errors++; } -validate123.errors = vErrors; +validate139.errors = vErrors; return errors === 0; } -validate123.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate139.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate122(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate138(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate122.evaluated; +const evaluated0 = validate138.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -16810,7 +19177,7 @@ errors++; if(data.id !== undefined){ let data0 = data.id; if(typeof data0 === "string"){ -if(!pattern49.test(data0)){ +if(!pattern65.test(data0)){ const err3 = {instancePath:instancePath+"/id",schemaPath:"#/properties/id/pattern",keyword:"pattern",params:{pattern: "^blk_[0-9A-Za-z]{8,24}$"},message:"must match pattern \""+"^blk_[0-9A-Za-z]{8,24}$"+"\""}; if(vErrors === null){ vErrors = [err3]; @@ -16835,7 +19202,7 @@ errors++; if(data.hash !== undefined){ let data1 = data.hash; if(typeof data1 === "string"){ -if(!pattern50.test(data1)){ +if(!pattern66.test(data1)){ const err5 = {instancePath:instancePath+"/hash",schemaPath:"#/properties/hash/pattern",keyword:"pattern",params:{pattern: "^[0-9a-f]{64}$"},message:"must match pattern \""+"^[0-9a-f]{64}$"+"\""}; if(vErrors === null){ vErrors = [err5]; @@ -16858,8 +19225,8 @@ errors++; } } if(data.by !== undefined){ -if(!(validate123(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate123.errors : vErrors.concat(validate123.errors); +if(!(validate139(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate139.errors : vErrors.concat(validate139.errors); errors = vErrors.length; } } @@ -16876,7 +19243,7 @@ vErrors.push(err7); errors++; } if(!((((((data3 === "studio") || (data3 === "mcp")) || (data3 === "api")) || (data3 === "import")) || (data3 === "ai-command")) || (data3 === "proposal"))){ -const err8 = {instancePath:instancePath+"/via",schemaPath:"#/properties/via/enum",keyword:"enum",params:{allowedValues: schema212.properties.via.enum},message:"must be equal to one of the allowed values"}; +const err8 = {instancePath:instancePath+"/via",schemaPath:"#/properties/via/enum",keyword:"enum",params:{allowedValues: schema229.properties.via.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err8]; } @@ -16922,16 +19289,16 @@ vErrors.push(err11); } errors++; } -validate122.errors = vErrors; +validate138.errors = vErrors; return errors === 0; } -validate122.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate138.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate121(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate137(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate121.evaluated; +const evaluated0 = validate137.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -16951,8 +19318,8 @@ errors++; } const len0 = data.length; for(let i0=0; i0//rss.xml. Requires archive.enabled."},"minEntriesToIndex":{"type":"integer","description":"Archives listing fewer published entries than this are served with robots 'noindex, follow' and left out of the sitemap.","minimum":0,"maximum":1000},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"metadata":{"$ref":"#/$defs/Metadata"}},"$defs":{"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"Path":{"type":"string","description":"Absolute URL path of a route the site owns: starts with '/', lower-case slug segments, no trailing slash except root.","pattern":"^/(?:[a-z0-9]+(?:-[a-z0-9]+)*(?:/[a-z0-9]+(?:-[a-z0-9]+)*)*)?$","maxLength":1024},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"TaxonomyArchive":{"type":"object","description":"Public archive pages: / and //page/.","additionalProperties":false,"required":["enabled","basePath"],"properties":{"enabled":{"type":"boolean"},"basePath":{"$ref":"#/$defs/Path"}}},"TaxonomyLabel":{"type":"object","additionalProperties":false,"required":["singular","plural"],"properties":{"singular":{"type":"string","minLength":1,"maxLength":100},"plural":{"type":"string","minLength":1,"maxLength":100},"description":{"type":"string","maxLength":1000}}},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}}}; -const schema264 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; -const schema265 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; -const schema267 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; -const schema268 = {"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"}; -const schema269 = {"type":"object","additionalProperties":false,"required":["singular","plural"],"properties":{"singular":{"type":"string","minLength":1,"maxLength":100},"plural":{"type":"string","minLength":1,"maxLength":100},"description":{"type":"string","maxLength":1000}}}; -const schema273 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; -const schema274 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; -const schema276 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; -const schema271 = {"type":"object","description":"Public archive pages: / and //page/.","additionalProperties":false,"required":["enabled","basePath"],"properties":{"enabled":{"type":"boolean"},"basePath":{"$ref":"#/$defs/Path"}}}; -const schema272 = {"type":"string","description":"Absolute URL path of a route the site owns: starts with '/', lower-case slug segments, no trailing slash except root.","pattern":"^/(?:[a-z0-9]+(?:-[a-z0-9]+)*(?:/[a-z0-9]+(?:-[a-z0-9]+)*)*)?$","maxLength":1024}; +export const validateTaxonomy = validate162; +const schema280 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/taxonomy.schema.json","title":"Taxonomy","description":"A classification scheme of a site (e.g. 'category', 'tag', 'series') and its rules: hierarchy, who may create terms, how many terms an entry takes, and how its archives are published. Terms are separate nodes (term.schema.json) that point here by id (RFC-0002).","type":"object","additionalProperties":false,"required":["schemaVersion","id","siteId","key","labels","hierarchical","allowNewTerms","required","archive","feed","minEntriesToIndex","rev","createdAt","updatedAt"],"properties":{"schemaVersion":{"$ref":"#/$defs/SchemaVersion"},"id":{"$ref":"#/$defs/Id"},"siteId":{"$ref":"#/$defs/Id"},"key":{"$ref":"#/$defs/Slug","description":"Stable name of the taxonomy, unique per site and immutable once created. ContentEntry.terms and filter expressions use it (e.g. 'category:news')."},"labels":{"type":"object","description":"Display names per locale. Must contain the site's default locale.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"$ref":"#/$defs/TaxonomyLabel"}},"hierarchical":{"type":"boolean","description":"Terms may have a parent term (categories). Flat when false (tags)."},"allowNewTerms":{"type":"string","description":"Who may add terms. none: people only. propose: agents may create terms with status 'proposed' that wait for a person. free: agents may create active terms.","enum":["none","propose","free"]},"required":{"type":"boolean","description":"An entry in review or published status must have at least one term of this taxonomy and an explicit primary term."},"maxPerEntry":{"type":"integer","description":"Most terms of this taxonomy one entry may carry. Absent: the contract limit of 50.","minimum":1,"maximum":50},"collections":{"type":"array","description":"Collections this taxonomy applies to. Absent: every collection.","items":{"$ref":"#/$defs/Slug"},"minItems":1,"maxItems":50,"uniqueItems":true},"archive":{"$ref":"#/$defs/TaxonomyArchive"},"feed":{"type":"boolean","description":"Publish an RSS feed per term at //rss.xml. Requires archive.enabled."},"minEntriesToIndex":{"type":"integer","description":"Archives listing fewer published entries than this are served with robots 'noindex, follow' and left out of the sitemap.","minimum":0,"maximum":1000},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"metadata":{"$ref":"#/$defs/Metadata"}},"$defs":{"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"Path":{"type":"string","description":"Absolute URL path of a route the site owns: starts with '/', lower-case slug segments, no trailing slash except root.","pattern":"^/(?:[a-z0-9]+(?:-[a-z0-9]+)*(?:/[a-z0-9]+(?:-[a-z0-9]+)*)*)?$","maxLength":1024},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"TaxonomyArchive":{"type":"object","description":"Public archive pages: / and //page/.","additionalProperties":false,"required":["enabled","basePath"],"properties":{"enabled":{"type":"boolean"},"basePath":{"$ref":"#/$defs/Path"}}},"TaxonomyLabel":{"type":"object","additionalProperties":false,"required":["singular","plural"],"properties":{"singular":{"type":"string","minLength":1,"maxLength":100},"plural":{"type":"string","minLength":1,"maxLength":100},"description":{"type":"string","maxLength":1000}}},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}}}; +const schema281 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; +const schema282 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; +const schema284 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; +const schema285 = {"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"}; +const schema286 = {"type":"object","additionalProperties":false,"required":["singular","plural"],"properties":{"singular":{"type":"string","minLength":1,"maxLength":100},"plural":{"type":"string","minLength":1,"maxLength":100},"description":{"type":"string","maxLength":1000}}}; +const schema290 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema291 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema293 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const schema288 = {"type":"object","description":"Public archive pages: / and //page/.","additionalProperties":false,"required":["enabled","basePath"],"properties":{"enabled":{"type":"boolean"},"basePath":{"$ref":"#/$defs/Path"}}}; +const schema289 = {"type":"string","description":"Absolute URL path of a route the site owns: starts with '/', lower-case slug segments, no trailing slash except root.","pattern":"^/(?:[a-z0-9]+(?:-[a-z0-9]+)*(?:/[a-z0-9]+(?:-[a-z0-9]+)*)*)?$","maxLength":1024}; -function validate147(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate163(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate147.evaluated; +const evaluated0 = validate163.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -20725,7 +23092,7 @@ vErrors.push(err4); } errors++; } -if(!pattern65.test(data1)){ +if(!pattern81.test(data1)){ const err5 = {instancePath:instancePath+"/basePath",schemaPath:"#/$defs/Path/pattern",keyword:"pattern",params:{pattern: "^/(?:[a-z0-9]+(?:-[a-z0-9]+)*(?:/[a-z0-9]+(?:-[a-z0-9]+)*)*)?$"},message:"must match pattern \""+"^/(?:[a-z0-9]+(?:-[a-z0-9]+)*(?:/[a-z0-9]+(?:-[a-z0-9]+)*)*)?$"+"\""}; if(vErrors === null){ vErrors = [err5]; @@ -20758,17 +23125,17 @@ vErrors.push(err7); } errors++; } -validate147.errors = vErrors; +validate163.errors = vErrors; return errors === 0; } -validate147.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate163.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate146(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate162(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ /*# sourceURL="https://cloudhal.dev/schemas/v1/taxonomy.schema.json" */; let vErrors = null; let errors = 0; -const evaluated0 = validate146.evaluated; +const evaluated0 = validate162.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -20917,7 +23284,7 @@ vErrors.push(err13); errors++; } for(const key0 in data){ -if(!(func1.call(schema263.properties, key0))){ +if(!(func1.call(schema280.properties, key0))){ const err14 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err14]; @@ -20941,7 +23308,7 @@ vErrors.push(err15); errors++; } if(!(data0 === 1)){ -const err16 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema264.enum},message:"must be equal to one of the allowed values"}; +const err16 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema281.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err16]; } @@ -21314,7 +23681,7 @@ vErrors.push(err47); errors++; } if(!(((data10 === "none") || (data10 === "propose")) || (data10 === "free"))){ -const err48 = {instancePath:instancePath+"/allowNewTerms",schemaPath:"#/properties/allowNewTerms/enum",keyword:"enum",params:{allowedValues: schema263.properties.allowNewTerms.enum},message:"must be equal to one of the allowed values"}; +const err48 = {instancePath:instancePath+"/allowNewTerms",schemaPath:"#/properties/allowNewTerms/enum",keyword:"enum",params:{allowedValues: schema280.properties.allowNewTerms.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err48]; } @@ -21473,8 +23840,8 @@ errors++; } } if(data.archive !== undefined){ -if(!(validate147(data.archive, {instancePath:instancePath+"/archive",parentData:data,parentDataProperty:"archive",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate147.errors : vErrors.concat(validate147.errors); +if(!(validate163(data.archive, {instancePath:instancePath+"/archive",parentData:data,parentDataProperty:"archive",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate163.errors : vErrors.concat(validate163.errors); errors = vErrors.length; } } @@ -21638,29 +24005,29 @@ vErrors.push(err73); } errors++; } -validate146.errors = vErrors; +validate162.errors = vErrors; return errors === 0; } -validate146.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate162.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -export const validateTerm = validate149; -const schema277 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/term.schema.json","title":"Term","description":"One concept in a taxonomy (a category, a tag, a series). Content refers to terms by id, independent of locale; names, synonyms, archive intro and SEO are per locale. Renames, merges and retirements keep the id and leave redirects behind; `proposedBy` records the actor that proposed it (RFC-0002).","type":"object","additionalProperties":false,"required":["schemaVersion","id","siteId","taxonomyId","slug","status","visibility","labels","rev","createdAt","updatedAt"],"properties":{"schemaVersion":{"$ref":"#/$defs/SchemaVersion"},"id":{"$ref":"#/$defs/Id"},"siteId":{"$ref":"#/$defs/Id"},"taxonomyId":{"$ref":"#/$defs/Id"},"slug":{"$ref":"#/$defs/Slug","description":"URL segment of the term's archive; unique within its taxonomy, together with every slug in any term's slugHistory."},"status":{"type":"string","description":"active: usable and public (subject to visibility). proposed: created by an agent, waiting for a person; assignable but never public. retired: no new assignments, no archive.","enum":["active","proposed","retired"]},"visibility":{"type":"string","description":"internal terms have no archive, feed or sitemap entry and are not shown on entries; themes and filters may still use them.","enum":["public","internal"]},"labels":{"type":"object","description":"Names per locale. Must contain the site's default locale.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"$ref":"#/$defs/TermLabel"}},"parentId":{"$ref":"#/$defs/Id","description":"Parent term in the same taxonomy; only in hierarchical taxonomies. No cycles."},"position":{"type":"integer","description":"Order among sibling terms (same taxonomy and parent), lowest first, for menus and sidebars. Terms without a position follow, by name.","minimum":0,"maximum":100000},"image":{"$ref":"#/$defs/ImageRef"},"links":{"type":"array","description":"Profile links shown with the term and given to search engines as sameAs, such as an author's pages elsewhere (RFC-0019 §1).","maxItems":10,"items":{"$ref":"#/$defs/Link"}},"slugHistory":{"type":"array","description":"Earlier slugs, oldest first. Old slugs stay reserved in the taxonomy and still resolve when an agent assigns by slug.","items":{"$ref":"#/$defs/Slug"},"minItems":1,"maxItems":50,"uniqueItems":true},"mergedInto":{"$ref":"#/$defs/Id","description":"Set when this term was merged into another active term of the same taxonomy; status is then 'retired'."},"proposedBy":{"$ref":"#/$defs/Actor"},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"metadata":{"$ref":"#/$defs/Metadata"}},"$defs":{"Actor":{"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}},"ActorKind":{"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]},"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"ImageRef":{"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}},"Link":{"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Seo":{"type":"object","description":"Search and social metadata of a routable node (page, term archive).","additionalProperties":false,"properties":{"title":{"type":"string","maxLength":300},"description":{"type":"string","maxLength":500},"noindex":{"type":"boolean"},"canonical":{"type":"string","maxLength":2048},"ogImage":{"$ref":"#/$defs/ImageRef"}}},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"TermLabel":{"type":"object","additionalProperties":false,"required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"slug":{"$ref":"#/$defs/Slug","description":"This locale's archive slug (RFC-0033); absent uses the term's slug. Never set for the site's default locale, whose slug is the term's own; unique per taxonomy and locale."},"altNames":{"type":"array","description":"Synonyms (SKOS altLabel). Matched when an agent or editor assigns by name; never shown as separate terms.","items":{"type":"string","minLength":1,"maxLength":200},"minItems":1,"maxItems":50,"uniqueItems":true},"description":{"type":"string","description":"Markdown introduction shown at the top of the archive.","maxLength":20000},"role":{"type":"string","description":"A short role or title shown with the term, such as an author's \"Senior reporter\" (RFC-0019 §1).","minLength":1,"maxLength":120},"seo":{"$ref":"#/$defs/Seo"}}},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}}}; -const schema278 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; -const schema279 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; -const schema282 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; -const schema283 = {"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"}; -const schema290 = {"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}}; -const schema296 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; -const schema297 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; -const schema299 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; -const schema284 = {"type":"object","additionalProperties":false,"required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"slug":{"$ref":"#/$defs/Slug","description":"This locale's archive slug (RFC-0033); absent uses the term's slug. Never set for the site's default locale, whose slug is the term's own; unique per taxonomy and locale."},"altNames":{"type":"array","description":"Synonyms (SKOS altLabel). Matched when an agent or editor assigns by name; never shown as separate terms.","items":{"type":"string","minLength":1,"maxLength":200},"minItems":1,"maxItems":50,"uniqueItems":true},"description":{"type":"string","description":"Markdown introduction shown at the top of the archive.","maxLength":20000},"role":{"type":"string","description":"A short role or title shown with the term, such as an author's \"Senior reporter\" (RFC-0019 §1).","minLength":1,"maxLength":120},"seo":{"$ref":"#/$defs/Seo"}}}; -const schema286 = {"type":"object","description":"Search and social metadata of a routable node (page, term archive).","additionalProperties":false,"properties":{"title":{"type":"string","maxLength":300},"description":{"type":"string","maxLength":500},"noindex":{"type":"boolean"},"canonical":{"type":"string","maxLength":2048},"ogImage":{"$ref":"#/$defs/ImageRef"}}}; -const schema287 = {"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}}; +export const validateTerm = validate165; +const schema294 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/term.schema.json","title":"Term","description":"One concept in a taxonomy (a category, a tag, a series). Content refers to terms by id, independent of locale; names, synonyms, archive intro and SEO are per locale. Renames, merges and retirements keep the id and leave redirects behind; `proposedBy` records the actor that proposed it (RFC-0002).","type":"object","additionalProperties":false,"required":["schemaVersion","id","siteId","taxonomyId","slug","status","visibility","labels","rev","createdAt","updatedAt"],"properties":{"schemaVersion":{"$ref":"#/$defs/SchemaVersion"},"id":{"$ref":"#/$defs/Id"},"siteId":{"$ref":"#/$defs/Id"},"taxonomyId":{"$ref":"#/$defs/Id"},"slug":{"$ref":"#/$defs/Slug","description":"URL segment of the term's archive; unique within its taxonomy, together with every slug in any term's slugHistory."},"status":{"type":"string","description":"active: usable and public (subject to visibility). proposed: created by an agent, waiting for a person; assignable but never public. retired: no new assignments, no archive.","enum":["active","proposed","retired"]},"visibility":{"type":"string","description":"internal terms have no archive, feed or sitemap entry and are not shown on entries; themes and filters may still use them.","enum":["public","internal"]},"labels":{"type":"object","description":"Names per locale. Must contain the site's default locale.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"$ref":"#/$defs/TermLabel"}},"parentId":{"$ref":"#/$defs/Id","description":"Parent term in the same taxonomy; only in hierarchical taxonomies. No cycles."},"position":{"type":"integer","description":"Order among sibling terms (same taxonomy and parent), lowest first, for menus and sidebars. Terms without a position follow, by name.","minimum":0,"maximum":100000},"image":{"$ref":"#/$defs/ImageRef"},"links":{"type":"array","description":"Profile links shown with the term and given to search engines as sameAs, such as an author's pages elsewhere (RFC-0019 §1).","maxItems":10,"items":{"$ref":"#/$defs/Link"}},"slugHistory":{"type":"array","description":"Earlier slugs, oldest first. Old slugs stay reserved in the taxonomy and still resolve when an agent assigns by slug.","items":{"$ref":"#/$defs/Slug"},"minItems":1,"maxItems":50,"uniqueItems":true},"mergedInto":{"$ref":"#/$defs/Id","description":"Set when this term was merged into another active term of the same taxonomy; status is then 'retired'."},"proposedBy":{"$ref":"#/$defs/Actor"},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"metadata":{"$ref":"#/$defs/Metadata"}},"$defs":{"Actor":{"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}},"ActorKind":{"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]},"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"ImageRef":{"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}},"Link":{"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Seo":{"type":"object","description":"Search and social metadata of a routable node (page, term archive).","additionalProperties":false,"properties":{"title":{"type":"string","maxLength":300},"description":{"type":"string","maxLength":500},"noindex":{"type":"boolean"},"canonical":{"type":"string","maxLength":2048},"ogImage":{"$ref":"#/$defs/ImageRef"}}},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"TermLabel":{"type":"object","additionalProperties":false,"required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"slug":{"$ref":"#/$defs/Slug","description":"This locale's archive slug (RFC-0033); absent uses the term's slug. Never set for the site's default locale, whose slug is the term's own; unique per taxonomy and locale."},"altNames":{"type":"array","description":"Synonyms (SKOS altLabel). Matched when an agent or editor assigns by name; never shown as separate terms.","items":{"type":"string","minLength":1,"maxLength":200},"minItems":1,"maxItems":50,"uniqueItems":true},"description":{"type":"string","description":"Markdown introduction shown at the top of the archive.","maxLength":20000},"role":{"type":"string","description":"A short role or title shown with the term, such as an author's \"Senior reporter\" (RFC-0019 §1).","minLength":1,"maxLength":120},"seo":{"$ref":"#/$defs/Seo"}}},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}}}; +const schema295 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; +const schema296 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; +const schema299 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; +const schema300 = {"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"}; +const schema307 = {"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}}; +const schema313 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema314 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema316 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const schema301 = {"type":"object","additionalProperties":false,"required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"slug":{"$ref":"#/$defs/Slug","description":"This locale's archive slug (RFC-0033); absent uses the term's slug. Never set for the site's default locale, whose slug is the term's own; unique per taxonomy and locale."},"altNames":{"type":"array","description":"Synonyms (SKOS altLabel). Matched when an agent or editor assigns by name; never shown as separate terms.","items":{"type":"string","minLength":1,"maxLength":200},"minItems":1,"maxItems":50,"uniqueItems":true},"description":{"type":"string","description":"Markdown introduction shown at the top of the archive.","maxLength":20000},"role":{"type":"string","description":"A short role or title shown with the term, such as an author's \"Senior reporter\" (RFC-0019 §1).","minLength":1,"maxLength":120},"seo":{"$ref":"#/$defs/Seo"}}}; +const schema303 = {"type":"object","description":"Search and social metadata of a routable node (page, term archive).","additionalProperties":false,"properties":{"title":{"type":"string","maxLength":300},"description":{"type":"string","maxLength":500},"noindex":{"type":"boolean"},"canonical":{"type":"string","maxLength":2048},"ogImage":{"$ref":"#/$defs/ImageRef"}}}; +const schema304 = {"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}}; -function validate152(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate168(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate152.evaluated; +const evaluated0 = validate168.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -21866,16 +24233,16 @@ vErrors.push(err16); } errors++; } -validate152.errors = vErrors; +validate168.errors = vErrors; return errors === 0; } -validate152.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate168.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate151(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate167(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate151.evaluated; +const evaluated0 = validate167.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -21983,8 +24350,8 @@ errors++; } } if(data.ogImage !== undefined){ -if(!(validate152(data.ogImage, {instancePath:instancePath+"/ogImage",parentData:data,parentDataProperty:"ogImage",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate152.errors : vErrors.concat(validate152.errors); +if(!(validate168(data.ogImage, {instancePath:instancePath+"/ogImage",parentData:data,parentDataProperty:"ogImage",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate168.errors : vErrors.concat(validate168.errors); errors = vErrors.length; } } @@ -21999,16 +24366,16 @@ vErrors.push(err8); } errors++; } -validate151.errors = vErrors; +validate167.errors = vErrors; return errors === 0; } -validate151.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate167.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate150(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate166(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate150.evaluated; +const evaluated0 = validate166.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -22274,8 +24641,8 @@ errors++; } } if(data.seo !== undefined){ -if(!(validate151(data.seo, {instancePath:instancePath+"/seo",parentData:data,parentDataProperty:"seo",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate151.errors : vErrors.concat(validate151.errors); +if(!(validate167(data.seo, {instancePath:instancePath+"/seo",parentData:data,parentDataProperty:"seo",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate167.errors : vErrors.concat(validate167.errors); errors = vErrors.length; } } @@ -22290,18 +24657,18 @@ vErrors.push(err21); } errors++; } -validate150.errors = vErrors; +validate166.errors = vErrors; return errors === 0; } -validate150.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate166.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const schema293 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; -const schema294 = {"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]}; +const schema310 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; +const schema311 = {"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]}; -function validate157(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate173(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate157.evaluated; +const evaluated0 = validate173.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -22354,7 +24721,7 @@ vErrors.push(err3); errors++; } if(!(((data0 === "human") || (data0 === "agent")) || (data0 === "system"))){ -const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema294.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema311.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -22445,17 +24812,17 @@ vErrors.push(err11); } errors++; } -validate157.errors = vErrors; +validate173.errors = vErrors; return errors === 0; } -validate157.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate173.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate149(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate165(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ /*# sourceURL="https://cloudhal.dev/schemas/v1/term.schema.json" */; let vErrors = null; let errors = 0; -const evaluated0 = validate149.evaluated; +const evaluated0 = validate165.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -22574,7 +24941,7 @@ vErrors.push(err10); errors++; } for(const key0 in data){ -if(!(func1.call(schema277.properties, key0))){ +if(!(func1.call(schema294.properties, key0))){ const err11 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err11]; @@ -22598,7 +24965,7 @@ vErrors.push(err12); errors++; } if(!(data0 === 1)){ -const err13 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema278.enum},message:"must be equal to one of the allowed values"}; +const err13 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema295.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err13]; } @@ -22801,7 +25168,7 @@ vErrors.push(err30); errors++; } if(!(((data5 === "active") || (data5 === "proposed")) || (data5 === "retired"))){ -const err31 = {instancePath:instancePath+"/status",schemaPath:"#/properties/status/enum",keyword:"enum",params:{allowedValues: schema277.properties.status.enum},message:"must be equal to one of the allowed values"}; +const err31 = {instancePath:instancePath+"/status",schemaPath:"#/properties/status/enum",keyword:"enum",params:{allowedValues: schema294.properties.status.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err31]; } @@ -22824,7 +25191,7 @@ vErrors.push(err32); errors++; } if(!((data6 === "public") || (data6 === "internal"))){ -const err33 = {instancePath:instancePath+"/visibility",schemaPath:"#/properties/visibility/enum",keyword:"enum",params:{allowedValues: schema277.properties.visibility.enum},message:"must be equal to one of the allowed values"}; +const err33 = {instancePath:instancePath+"/visibility",schemaPath:"#/properties/visibility/enum",keyword:"enum",params:{allowedValues: schema294.properties.visibility.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err33]; } @@ -22884,8 +25251,8 @@ errors++; } } for(const key2 in data7){ -if(!(validate150(data7[key2], {instancePath:instancePath+"/labels/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),parentData:data7,parentDataProperty:key2,rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate150.errors : vErrors.concat(validate150.errors); +if(!(validate166(data7[key2], {instancePath:instancePath+"/labels/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),parentData:data7,parentDataProperty:key2,rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate166.errors : vErrors.concat(validate166.errors); errors = vErrors.length; } } @@ -22982,8 +25349,8 @@ errors++; } } if(data.image !== undefined){ -if(!(validate152(data.image, {instancePath:instancePath+"/image",parentData:data,parentDataProperty:"image",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate152.errors : vErrors.concat(validate152.errors); +if(!(validate168(data.image, {instancePath:instancePath+"/image",parentData:data,parentDataProperty:"image",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate168.errors : vErrors.concat(validate168.errors); errors = vErrors.length; } } @@ -23289,8 +25656,8 @@ errors++; } } if(data.proposedBy !== undefined){ -if(!(validate157(data.proposedBy, {instancePath:instancePath+"/proposedBy",parentData:data,parentDataProperty:"proposedBy",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate157.errors : vErrors.concat(validate157.errors); +if(!(validate173(data.proposedBy, {instancePath:instancePath+"/proposedBy",parentData:data,parentDataProperty:"proposedBy",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate173.errors : vErrors.concat(validate173.errors); errors = vErrors.length; } } @@ -23407,27 +25774,27 @@ vErrors.push(err79); } errors++; } -validate149.errors = vErrors; +validate165.errors = vErrors; return errors === 0; } -validate149.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate165.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -export const validateRedirect = validate159; -const schema300 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/redirect.schema.json","title":"Redirect","description":"An HTTP redirect from a path the site no longer serves. Created automatically when a published entry or a term changes slug, when terms are merged or retired, and by people. Snapshotted and published like every other node (RFC-0002).","type":"object","additionalProperties":false,"required":["schemaVersion","id","siteId","from","to","status","match","reason","rev","createdAt","updatedAt"],"properties":{"schemaVersion":{"$ref":"#/$defs/SchemaVersion"},"id":{"$ref":"#/$defs/Id"},"siteId":{"$ref":"#/$defs/Id"},"from":{"type":"string","description":"Request path to redirect: starts with '/', no query, fragment, backslash or trailing slash (root excepted). Unique per site and locale; never a path the site currently serves.","pattern":"^/(?:[^\\s?#/\\\\]+(?:/[^\\s?#/\\\\]+)*)?$","maxLength":1024},"locale":{"$ref":"#/$defs/Locale","description":"The locale whose URLs `from` belongs to (RFC-0033); absent means the site's default locale. `from` is the path inside that locale, without its prefix or domain. Unique per site and locale."},"to":{"type":"string","description":"Target: a site path (may carry a query or fragment) or an absolute https URL. Protocol-relative ('//host') and backslash forms are rejected.","pattern":"^(?:/(?![/\\\\])[^\\s\\\\]*|https://[^\\s\\\\/?#]+(?:[/?#][^\\s\\\\]*)?)$","maxLength":2048},"status":{"type":"integer","description":"301 (permanent) for slug changes, merges and retirements; 302 (temporary) only for manual redirects.","enum":[301,302]},"match":{"type":"string","description":"exact: only `from` itself. prefix: `from` and every path below it; the rest of the path is appended to `to` (archives: /tag/old/page/2 → /tag/new/page/2).","enum":["exact","prefix"]},"reason":{"type":"string","enum":["slug-change","merge","retire","manual"]},"source":{"$ref":"#/$defs/RedirectSource"},"note":{"type":"string","maxLength":500},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"metadata":{"$ref":"#/$defs/Metadata"}},"$defs":{"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"RedirectSource":{"type":"object","description":"The node whose earlier URL this redirect preserves (automatic redirects). Used to collapse chains on later renames and to list a node's old URLs.","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"type":"string","enum":["page","contentEntry","term"]},"id":{"$ref":"#/$defs/Id"}}},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}}}; -const schema301 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; -const schema302 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; -const schema304 = {"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"}; -const schema307 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; -const schema308 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; -const schema310 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; -const pattern159 = new RegExp("^/(?:[^\\s?#/\\\\]+(?:/[^\\s?#/\\\\]+)*)?$", "u"); -const pattern161 = new RegExp("^(?:/(?![/\\\\])[^\\s\\\\]*|https://[^\\s\\\\/?#]+(?:[/?#][^\\s\\\\]*)?)$", "u"); -const schema305 = {"type":"object","description":"The node whose earlier URL this redirect preserves (automatic redirects). Used to collapse chains on later renames and to list a node's old URLs.","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"type":"string","enum":["page","contentEntry","term"]},"id":{"$ref":"#/$defs/Id"}}}; +export const validateRedirect = validate175; +const schema317 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/redirect.schema.json","title":"Redirect","description":"An HTTP redirect from a path the site no longer serves. Created automatically when a published entry or a term changes slug, when terms are merged or retired, and by people. Snapshotted and published like every other node (RFC-0002).","type":"object","additionalProperties":false,"required":["schemaVersion","id","siteId","from","to","status","match","reason","rev","createdAt","updatedAt"],"properties":{"schemaVersion":{"$ref":"#/$defs/SchemaVersion"},"id":{"$ref":"#/$defs/Id"},"siteId":{"$ref":"#/$defs/Id"},"from":{"type":"string","description":"Request path to redirect: starts with '/', no query, fragment, backslash or trailing slash (root excepted). Unique per site and locale; never a path the site currently serves.","pattern":"^/(?:[^\\s?#/\\\\]+(?:/[^\\s?#/\\\\]+)*)?$","maxLength":1024},"locale":{"$ref":"#/$defs/Locale","description":"The locale whose URLs `from` belongs to (RFC-0033); absent means the site's default locale. `from` is the path inside that locale, without its prefix or domain. Unique per site and locale."},"to":{"type":"string","description":"Target: a site path (may carry a query or fragment) or an absolute https URL. Protocol-relative ('//host') and backslash forms are rejected.","pattern":"^(?:/(?![/\\\\])[^\\s\\\\]*|https://[^\\s\\\\/?#]+(?:[/?#][^\\s\\\\]*)?)$","maxLength":2048},"status":{"type":"integer","description":"301 (permanent) for slug changes, merges and retirements; 302 (temporary) only for manual redirects.","enum":[301,302]},"match":{"type":"string","description":"exact: only `from` itself. prefix: `from` and every path below it; the rest of the path is appended to `to` (archives: /tag/old/page/2 → /tag/new/page/2).","enum":["exact","prefix"]},"reason":{"type":"string","enum":["slug-change","merge","retire","manual"]},"source":{"$ref":"#/$defs/RedirectSource"},"note":{"type":"string","maxLength":500},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"metadata":{"$ref":"#/$defs/Metadata"}},"$defs":{"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"RedirectSource":{"type":"object","description":"The node whose earlier URL this redirect preserves (automatic redirects). Used to collapse chains on later renames and to list a node's old URLs.","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"type":"string","enum":["page","contentEntry","term"]},"id":{"$ref":"#/$defs/Id"}}},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}}}; +const schema318 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; +const schema319 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; +const schema321 = {"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"}; +const schema324 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema325 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema327 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const pattern175 = new RegExp("^/(?:[^\\s?#/\\\\]+(?:/[^\\s?#/\\\\]+)*)?$", "u"); +const pattern177 = new RegExp("^(?:/(?![/\\\\])[^\\s\\\\]*|https://[^\\s\\\\/?#]+(?:[/?#][^\\s\\\\]*)?)$", "u"); +const schema322 = {"type":"object","description":"The node whose earlier URL this redirect preserves (automatic redirects). Used to collapse chains on later renames and to list a node's old URLs.","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"type":"string","enum":["page","contentEntry","term"]},"id":{"$ref":"#/$defs/Id"}}}; -function validate160(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate176(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate160.evaluated; +const evaluated0 = validate176.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -23480,7 +25847,7 @@ vErrors.push(err3); errors++; } if(!(((data0 === "page") || (data0 === "contentEntry")) || (data0 === "term"))){ -const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/properties/kind/enum",keyword:"enum",params:{allowedValues: schema305.properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/properties/kind/enum",keyword:"enum",params:{allowedValues: schema322.properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -23546,17 +25913,17 @@ vErrors.push(err9); } errors++; } -validate160.errors = vErrors; +validate176.errors = vErrors; return errors === 0; } -validate160.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate176.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate159(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate175(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ /*# sourceURL="https://cloudhal.dev/schemas/v1/redirect.schema.json" */; let vErrors = null; let errors = 0; -const evaluated0 = validate159.evaluated; +const evaluated0 = validate175.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -23675,7 +26042,7 @@ vErrors.push(err10); errors++; } for(const key0 in data){ -if(!(func1.call(schema300.properties, key0))){ +if(!(func1.call(schema317.properties, key0))){ const err11 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err11]; @@ -23699,7 +26066,7 @@ vErrors.push(err12); errors++; } if(!(data0 === 1)){ -const err13 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema301.enum},message:"must be equal to one of the allowed values"}; +const err13 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema318.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err13]; } @@ -23812,7 +26179,7 @@ vErrors.push(err22); } errors++; } -if(!pattern159.test(data3)){ +if(!pattern175.test(data3)){ const err23 = {instancePath:instancePath+"/from",schemaPath:"#/properties/from/pattern",keyword:"pattern",params:{pattern: "^/(?:[^\\s?#/\\\\]+(?:/[^\\s?#/\\\\]+)*)?$"},message:"must match pattern \""+"^/(?:[^\\s?#/\\\\]+(?:/[^\\s?#/\\\\]+)*)?$"+"\""}; if(vErrors === null){ vErrors = [err23]; @@ -23872,7 +26239,7 @@ vErrors.push(err27); } errors++; } -if(!pattern161.test(data5)){ +if(!pattern177.test(data5)){ const err28 = {instancePath:instancePath+"/to",schemaPath:"#/properties/to/pattern",keyword:"pattern",params:{pattern: "^(?:/(?![/\\\\])[^\\s\\\\]*|https://[^\\s\\\\/?#]+(?:[/?#][^\\s\\\\]*)?)$"},message:"must match pattern \""+"^(?:/(?![/\\\\])[^\\s\\\\]*|https://[^\\s\\\\/?#]+(?:[/?#][^\\s\\\\]*)?)$"+"\""}; if(vErrors === null){ vErrors = [err28]; @@ -23907,7 +26274,7 @@ vErrors.push(err30); errors++; } if(!((data6 === 301) || (data6 === 302))){ -const err31 = {instancePath:instancePath+"/status",schemaPath:"#/properties/status/enum",keyword:"enum",params:{allowedValues: schema300.properties.status.enum},message:"must be equal to one of the allowed values"}; +const err31 = {instancePath:instancePath+"/status",schemaPath:"#/properties/status/enum",keyword:"enum",params:{allowedValues: schema317.properties.status.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err31]; } @@ -23930,7 +26297,7 @@ vErrors.push(err32); errors++; } if(!((data7 === "exact") || (data7 === "prefix"))){ -const err33 = {instancePath:instancePath+"/match",schemaPath:"#/properties/match/enum",keyword:"enum",params:{allowedValues: schema300.properties.match.enum},message:"must be equal to one of the allowed values"}; +const err33 = {instancePath:instancePath+"/match",schemaPath:"#/properties/match/enum",keyword:"enum",params:{allowedValues: schema317.properties.match.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err33]; } @@ -23953,7 +26320,7 @@ vErrors.push(err34); errors++; } if(!((((data8 === "slug-change") || (data8 === "merge")) || (data8 === "retire")) || (data8 === "manual"))){ -const err35 = {instancePath:instancePath+"/reason",schemaPath:"#/properties/reason/enum",keyword:"enum",params:{allowedValues: schema300.properties.reason.enum},message:"must be equal to one of the allowed values"}; +const err35 = {instancePath:instancePath+"/reason",schemaPath:"#/properties/reason/enum",keyword:"enum",params:{allowedValues: schema317.properties.reason.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err35]; } @@ -23964,8 +26331,8 @@ errors++; } } if(data.source !== undefined){ -if(!(validate160(data.source, {instancePath:instancePath+"/source",parentData:data,parentDataProperty:"source",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate160.errors : vErrors.concat(validate160.errors); +if(!(validate176(data.source, {instancePath:instancePath+"/source",parentData:data,parentDataProperty:"source",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate176.errors : vErrors.concat(validate176.errors); errors = vErrors.length; } } @@ -24107,34 +26474,34 @@ vErrors.push(err46); } errors++; } -validate159.errors = vErrors; +validate175.errors = vErrors; return errors === 0; } -validate159.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate175.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -export const validateAsset = validate162; -const schema311 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/asset.schema.json","title":"Asset","description":"A media file of a site (image, video, audio, document, data) as a graph node: its bytes are identified by SHA-256 and stored once per site in the blob store under a content-addressed key, or kept elsewhere (`storage.kind: external`, e.g. a Zenodo or Hugging Face file). Pages, entries and terms refer to it by id (`ImageRef.assetId`, markdown `asset:`). Snapshotted like every node, so a deleted asset's bytes stay while any snapshot references it (RFC-0004).","type":"object","additionalProperties":false,"required":["schemaVersion","id","siteId","kind","mediaType","bytes","sha256","slug","storage","status","uploadedBy","source","rev","createdAt","updatedAt"],"properties":{"schemaVersion":{"$ref":"#/$defs/SchemaVersion"},"id":{"$ref":"#/$defs/Id"},"siteId":{"$ref":"#/$defs/Id"},"kind":{"type":"string","description":"What the file is, derived from the sniffed media type (never chosen by the client). `other` is allowed for external storage only.","enum":["image","video","audio","document","data","other"]},"mediaType":{"type":"string","description":"MIME type of the stored bytes as sniffed by the server (RFC 6838, lower case, no parameters), e.g. image/webp, text/csv. Served as Content-Type.","pattern":"^[a-z0-9][a-z0-9!#$&^_.+-]{0,126}/[a-z0-9][a-z0-9!#$&^_.+-]{0,126}$","maxLength":255},"bytes":{"type":"integer","description":"Size of the stored bytes. For external files: the size the source reports or the uploader declares.","minimum":0},"sha256":{"$ref":"#/$defs/Sha256","description":"SHA-256 of the stored bytes (lower-case hex): the blob key, the public URL prefix, the ETag and the checksum shown with downloads. Unique per site. For external files see `storage.checksum`."},"originalSha256":{"$ref":"#/$defs/Sha256","description":"SHA-256 of the bytes as uploaded, present only when the server changed them (image metadata stripping)."},"slug":{"$ref":"#/$defs/Slug","description":"File name in the public URL, without extension: /media//.. May change; the old URL then answers 301 to the new one."},"storage":{"$ref":"#/$defs/AssetStorage"},"image":{"$ref":"#/$defs/AssetImage"},"title":{"type":"object","description":"Display title per locale (download lists, library search).","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"type":"string","minLength":1,"maxLength":300}},"alt":{"type":"object","description":"Alternative text per locale. An `ImageRef.alt` that is not empty overrides it for one use.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"type":"string","minLength":1,"maxLength":500}},"decorative":{"type":"boolean","description":"The image carries no information and renders with alt=\"\". Absent means false; the store never writes false."},"caption":{"type":"object","description":"Caption per locale (plain text), the default for figures that show this asset.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"type":"string","minLength":1,"maxLength":1000}},"credit":{"type":"string","description":"Who made it, as shown next to it (e.g. 'Photo: Ayşe Yılmaz').","minLength":1,"maxLength":300},"license":{"$ref":"#/$defs/AssetLicense"},"status":{"type":"string","description":"active: listed and pickable. retired: hidden from the library and pickers and cannot be newly referenced; existing references keep rendering.","enum":["active","retired"]},"uploadedBy":{"$ref":"#/$defs/Actor"},"source":{"$ref":"#/$defs/AssetSource"},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"metadata":{"$ref":"#/$defs/Metadata"}},"$defs":{"Actor":{"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}},"ActorKind":{"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]},"AssetImage":{"type":"object","description":"Intrinsic size and placeholders of an image. Present for every image stored as a blob.","additionalProperties":false,"required":["width","height"],"properties":{"width":{"type":"integer","minimum":1,"maximum":100000},"height":{"type":"integer","minimum":1,"maximum":100000},"animated":{"type":"boolean","description":"Animated GIF or WebP: never resized or re-encoded. Absent means false; the store never writes false."},"blurhash":{"type":"string","description":"BlurHash of the image for themes that decode it client-side.","pattern":"^[0-9A-Za-z#$%*+,.:;=?@^_{|}~\\[\\]-]{6,100}$"},"lqip":{"type":"string","description":"Low-quality placeholder: a raster data: URL of at most about 3 KB, shown while the image loads.","pattern":"^data:image/(?:webp|png|jpeg);base64,[A-Za-z0-9+/]+={0,2}$","maxLength":4096},"color":{"type":"string","description":"RFC-0028: the image's dominant colour, lower-case #rrggbb. Painted under the placeholder and in color-fill bars.","pattern":"^#[0-9a-f]{6}$"},"alpha":{"type":"boolean","enum":[true],"description":"RFC-0028: the image may have transparent pixels (read from its header at ingest). Absent means opaque; transparent images never show the LQIP."},"focal":{"type":"object","description":"RFC-0028: where the subject is, from the top-left corner as fractions of the width and height. Cover crops keep it in view.","additionalProperties":false,"required":["x","y"],"properties":{"x":{"type":"number","minimum":0,"maximum":1},"y":{"type":"number","minimum":0,"maximum":1}}}}},"AssetLicense":{"description":"Terms the asset may be reused under: an SPDX license expression or a license URL.","oneOf":[{"type":"object","title":"SpdxLicense","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"type":"string","enum":["spdx"]},"id":{"type":"string","description":"SPDX license identifier or simple expression, e.g. CC-BY-4.0, CC0-1.0, MIT OR Apache-2.0, LicenseRef-AllRightsReserved.","pattern":"^[A-Za-z0-9][A-Za-z0-9.+-]*(?: (?:AND|OR|WITH) [A-Za-z0-9][A-Za-z0-9.+-]*)*$","maxLength":200}}},{"type":"object","title":"UrlLicense","additionalProperties":false,"required":["kind","url"],"properties":{"kind":{"type":"string","enum":["url"]},"url":{"$ref":"#/$defs/HttpsUrl"},"name":{"type":"string","minLength":1,"maxLength":200}}}]},"AssetSource":{"description":"How the asset came in. Immutable after create.","oneOf":[{"type":"object","title":"UploadSource","description":"Bytes uploaded by a person (REST) or an agent (MCP upload_asset).","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["upload"]},"filename":{"type":"string","description":"Original file name as sent (never used as a storage key or served).","pattern":"^[^/\\\\\\u0000-\\u001f\\u007f]+$","minLength":1,"maxLength":255}}},{"type":"object","title":"FetchSource","description":"Bytes the server fetched from an https URL (MCP fetch_asset_url, Studio 'From URL') under the SSRF rules.","additionalProperties":false,"required":["kind","url","fetchedAt"],"properties":{"kind":{"type":"string","enum":["fetch"]},"url":{"$ref":"#/$defs/HttpsUrl"},"fetchedAt":{"$ref":"#/$defs/Timestamp"}}},{"type":"object","title":"ImportSource","description":"Created by a migration or importer (e.g. `cloudhal media import-urls` from an ImageRef.src).","additionalProperties":false,"required":["kind","from"],"properties":{"kind":{"type":"string","enum":["import"]},"from":{"type":"string","description":"The reference it was imported from: a URL or a site path.","minLength":1,"maxLength":2048},"importer":{"$ref":"#/$defs/Slug"}}},{"type":"object","title":"LinkSource","description":"An external file registered by URL without copying its bytes (storage.kind external).","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["link"]}}}]},"AssetStorage":{"description":"Where the bytes live. `blob`: in this deployment's blob store (R2 or the filesystem) under the content-addressed key media//, derived and never stored, so the node is portable between backends. `external`: elsewhere, linked rather than hosted (large research files on Zenodo or Hugging Face).","oneOf":[{"type":"object","title":"BlobStorage","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["blob"]}}},{"type":"object","title":"ExternalStorage","additionalProperties":false,"required":["kind","url","checksum"],"properties":{"kind":{"type":"string","enum":["external"]},"url":{"$ref":"#/$defs/HttpsUrl","description":"Direct download URL of the file."},"landingPage":{"$ref":"#/$defs/HttpsUrl","description":"Human page of the record the file belongs to (e.g. a Zenodo record or a Hugging Face dataset)."},"checksum":{"type":"string","description":"How `sha256` was obtained. computed: the server or `cloudhal media verify` streamed the URL and hashed it. declared: given by the person who linked it, not verified (shown as such).","enum":["computed","declared"]},"verifiedAt":{"$ref":"#/$defs/Timestamp","description":"When the URL last produced bytes with this SHA-256 (checksum `computed`)."}}}]},"HttpsUrl":{"type":"string","description":"Absolute https URL without credentials, whitespace or backslashes.","pattern":"^https://[^\\s\\\\/?#@]+(?:[/?#][^\\s\\\\]*)?$","maxLength":2048},"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Sha256":{"type":"string","description":"Lower-case hex SHA-256 digest.","pattern":"^[0-9a-f]{64}$"},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}}}; -const schema312 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; -const schema313 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; -const schema315 = {"type":"string","description":"Lower-case hex SHA-256 digest.","pattern":"^[0-9a-f]{64}$"}; -const schema317 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; -const schema322 = {"type":"object","description":"Intrinsic size and placeholders of an image. Present for every image stored as a blob.","additionalProperties":false,"required":["width","height"],"properties":{"width":{"type":"integer","minimum":1,"maximum":100000},"height":{"type":"integer","minimum":1,"maximum":100000},"animated":{"type":"boolean","description":"Animated GIF or WebP: never resized or re-encoded. Absent means false; the store never writes false."},"blurhash":{"type":"string","description":"BlurHash of the image for themes that decode it client-side.","pattern":"^[0-9A-Za-z#$%*+,.:;=?@^_{|}~\\[\\]-]{6,100}$"},"lqip":{"type":"string","description":"Low-quality placeholder: a raster data: URL of at most about 3 KB, shown while the image loads.","pattern":"^data:image/(?:webp|png|jpeg);base64,[A-Za-z0-9+/]+={0,2}$","maxLength":4096},"color":{"type":"string","description":"RFC-0028: the image's dominant colour, lower-case #rrggbb. Painted under the placeholder and in color-fill bars.","pattern":"^#[0-9a-f]{6}$"},"alpha":{"type":"boolean","enum":[true],"description":"RFC-0028: the image may have transparent pixels (read from its header at ingest). Absent means opaque; transparent images never show the LQIP."},"focal":{"type":"object","description":"RFC-0028: where the subject is, from the top-left corner as fractions of the width and height. Cover crops keep it in view.","additionalProperties":false,"required":["x","y"],"properties":{"x":{"type":"number","minimum":0,"maximum":1},"y":{"type":"number","minimum":0,"maximum":1}}}}}; -const schema323 = {"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"}; -const schema335 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; -const schema321 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; -const schema338 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; -const pattern165 = new RegExp("^[a-z0-9][a-z0-9!#$&^_.+-]{0,126}/[a-z0-9][a-z0-9!#$&^_.+-]{0,126}$", "u"); -const pattern171 = new RegExp("^[0-9A-Za-z#$%*+,.:;=?@^_{|}~\\[\\]-]{6,100}$", "u"); -const pattern172 = new RegExp("^data:image/(?:webp|png|jpeg);base64,[A-Za-z0-9+/]+={0,2}$", "u"); -const pattern173 = new RegExp("^#[0-9a-f]{6}$", "u"); -const schema318 = {"description":"Where the bytes live. `blob`: in this deployment's blob store (R2 or the filesystem) under the content-addressed key media//, derived and never stored, so the node is portable between backends. `external`: elsewhere, linked rather than hosted (large research files on Zenodo or Hugging Face).","oneOf":[{"type":"object","title":"BlobStorage","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["blob"]}}},{"type":"object","title":"ExternalStorage","additionalProperties":false,"required":["kind","url","checksum"],"properties":{"kind":{"type":"string","enum":["external"]},"url":{"$ref":"#/$defs/HttpsUrl","description":"Direct download URL of the file."},"landingPage":{"$ref":"#/$defs/HttpsUrl","description":"Human page of the record the file belongs to (e.g. a Zenodo record or a Hugging Face dataset)."},"checksum":{"type":"string","description":"How `sha256` was obtained. computed: the server or `cloudhal media verify` streamed the URL and hashed it. declared: given by the person who linked it, not verified (shown as such).","enum":["computed","declared"]},"verifiedAt":{"$ref":"#/$defs/Timestamp","description":"When the URL last produced bytes with this SHA-256 (checksum `computed`)."}}}]}; -const schema319 = {"type":"string","description":"Absolute https URL without credentials, whitespace or backslashes.","pattern":"^https://[^\\s\\\\/?#@]+(?:[/?#][^\\s\\\\]*)?$","maxLength":2048}; -const pattern169 = new RegExp("^https://[^\\s\\\\/?#@]+(?:[/?#][^\\s\\\\]*)?$", "u"); +export const validateAsset = validate178; +const schema328 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/asset.schema.json","title":"Asset","description":"A media file of a site (image, video, audio, document, data) as a graph node: its bytes are identified by SHA-256 and stored once per site in the blob store under a content-addressed key, or kept elsewhere (`storage.kind: external`, e.g. a Zenodo or Hugging Face file). Pages, entries and terms refer to it by id (`ImageRef.assetId`, markdown `asset:`). Snapshotted like every node, so a deleted asset's bytes stay while any snapshot references it (RFC-0004).","type":"object","additionalProperties":false,"required":["schemaVersion","id","siteId","kind","mediaType","bytes","sha256","slug","storage","status","uploadedBy","source","rev","createdAt","updatedAt"],"properties":{"schemaVersion":{"$ref":"#/$defs/SchemaVersion"},"id":{"$ref":"#/$defs/Id"},"siteId":{"$ref":"#/$defs/Id"},"kind":{"type":"string","description":"What the file is, derived from the sniffed media type (never chosen by the client). `other` is allowed for external storage only.","enum":["image","video","audio","document","data","other"]},"mediaType":{"type":"string","description":"MIME type of the stored bytes as sniffed by the server (RFC 6838, lower case, no parameters), e.g. image/webp, text/csv. Served as Content-Type.","pattern":"^[a-z0-9][a-z0-9!#$&^_.+-]{0,126}/[a-z0-9][a-z0-9!#$&^_.+-]{0,126}$","maxLength":255},"bytes":{"type":"integer","description":"Size of the stored bytes. For external files: the size the source reports or the uploader declares.","minimum":0},"sha256":{"$ref":"#/$defs/Sha256","description":"SHA-256 of the stored bytes (lower-case hex): the blob key, the public URL prefix, the ETag and the checksum shown with downloads. Unique per site. For external files see `storage.checksum`."},"originalSha256":{"$ref":"#/$defs/Sha256","description":"SHA-256 of the bytes as uploaded, present only when the server changed them (image metadata stripping)."},"slug":{"$ref":"#/$defs/Slug","description":"File name in the public URL, without extension: /media//.. May change; the old URL then answers 301 to the new one."},"storage":{"$ref":"#/$defs/AssetStorage"},"image":{"$ref":"#/$defs/AssetImage"},"title":{"type":"object","description":"Display title per locale (download lists, library search).","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"type":"string","minLength":1,"maxLength":300}},"alt":{"type":"object","description":"Alternative text per locale. An `ImageRef.alt` that is not empty overrides it for one use.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"type":"string","minLength":1,"maxLength":500}},"decorative":{"type":"boolean","description":"The image carries no information and renders with alt=\"\". Absent means false; the store never writes false."},"caption":{"type":"object","description":"Caption per locale (plain text), the default for figures that show this asset.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"type":"string","minLength":1,"maxLength":1000}},"credit":{"type":"string","description":"Who made it, as shown next to it (e.g. 'Photo: Ayşe Yılmaz').","minLength":1,"maxLength":300},"license":{"$ref":"#/$defs/AssetLicense"},"status":{"type":"string","description":"active: listed and pickable. retired: hidden from the library and pickers and cannot be newly referenced; existing references keep rendering.","enum":["active","retired"]},"uploadedBy":{"$ref":"#/$defs/Actor"},"source":{"$ref":"#/$defs/AssetSource"},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"metadata":{"$ref":"#/$defs/Metadata"}},"$defs":{"Actor":{"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}},"ActorKind":{"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]},"AssetImage":{"type":"object","description":"Intrinsic size and placeholders of an image. Present for every image stored as a blob.","additionalProperties":false,"required":["width","height"],"properties":{"width":{"type":"integer","minimum":1,"maximum":100000},"height":{"type":"integer","minimum":1,"maximum":100000},"animated":{"type":"boolean","description":"Animated GIF or WebP: never resized or re-encoded. Absent means false; the store never writes false."},"blurhash":{"type":"string","description":"BlurHash of the image for themes that decode it client-side.","pattern":"^[0-9A-Za-z#$%*+,.:;=?@^_{|}~\\[\\]-]{6,100}$"},"lqip":{"type":"string","description":"Low-quality placeholder: a raster data: URL of at most about 3 KB, shown while the image loads.","pattern":"^data:image/(?:webp|png|jpeg);base64,[A-Za-z0-9+/]+={0,2}$","maxLength":4096},"color":{"type":"string","description":"RFC-0028: the image's dominant colour, lower-case #rrggbb. Painted under the placeholder and in color-fill bars.","pattern":"^#[0-9a-f]{6}$"},"alpha":{"type":"boolean","enum":[true],"description":"RFC-0028: the image may have transparent pixels (read from its header at ingest). Absent means opaque; transparent images never show the LQIP."},"focal":{"type":"object","description":"RFC-0028: where the subject is, from the top-left corner as fractions of the width and height. Cover crops keep it in view.","additionalProperties":false,"required":["x","y"],"properties":{"x":{"type":"number","minimum":0,"maximum":1},"y":{"type":"number","minimum":0,"maximum":1}}}}},"AssetLicense":{"description":"Terms the asset may be reused under: an SPDX license expression or a license URL.","oneOf":[{"type":"object","title":"SpdxLicense","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"type":"string","enum":["spdx"]},"id":{"type":"string","description":"SPDX license identifier or simple expression, e.g. CC-BY-4.0, CC0-1.0, MIT OR Apache-2.0, LicenseRef-AllRightsReserved.","pattern":"^[A-Za-z0-9][A-Za-z0-9.+-]*(?: (?:AND|OR|WITH) [A-Za-z0-9][A-Za-z0-9.+-]*)*$","maxLength":200}}},{"type":"object","title":"UrlLicense","additionalProperties":false,"required":["kind","url"],"properties":{"kind":{"type":"string","enum":["url"]},"url":{"$ref":"#/$defs/HttpsUrl"},"name":{"type":"string","minLength":1,"maxLength":200}}}]},"AssetSource":{"description":"How the asset came in. Immutable after create.","oneOf":[{"type":"object","title":"UploadSource","description":"Bytes uploaded by a person (REST) or an agent (MCP upload_asset).","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["upload"]},"filename":{"type":"string","description":"Original file name as sent (never used as a storage key or served).","pattern":"^[^/\\\\\\u0000-\\u001f\\u007f]+$","minLength":1,"maxLength":255}}},{"type":"object","title":"FetchSource","description":"Bytes the server fetched from an https URL (MCP fetch_asset_url, Studio 'From URL') under the SSRF rules.","additionalProperties":false,"required":["kind","url","fetchedAt"],"properties":{"kind":{"type":"string","enum":["fetch"]},"url":{"$ref":"#/$defs/HttpsUrl"},"fetchedAt":{"$ref":"#/$defs/Timestamp"}}},{"type":"object","title":"ImportSource","description":"Created by a migration or importer (e.g. `cloudhal media import-urls` from an ImageRef.src).","additionalProperties":false,"required":["kind","from"],"properties":{"kind":{"type":"string","enum":["import"]},"from":{"type":"string","description":"The reference it was imported from: a URL or a site path.","minLength":1,"maxLength":2048},"importer":{"$ref":"#/$defs/Slug"}}},{"type":"object","title":"LinkSource","description":"An external file registered by URL without copying its bytes (storage.kind external).","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["link"]}}}]},"AssetStorage":{"description":"Where the bytes live. `blob`: in this deployment's blob store (R2 or the filesystem) under the content-addressed key media//, derived and never stored, so the node is portable between backends. `external`: elsewhere, linked rather than hosted (large research files on Zenodo or Hugging Face).","oneOf":[{"type":"object","title":"BlobStorage","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["blob"]}}},{"type":"object","title":"ExternalStorage","additionalProperties":false,"required":["kind","url","checksum"],"properties":{"kind":{"type":"string","enum":["external"]},"url":{"$ref":"#/$defs/HttpsUrl","description":"Direct download URL of the file."},"landingPage":{"$ref":"#/$defs/HttpsUrl","description":"Human page of the record the file belongs to (e.g. a Zenodo record or a Hugging Face dataset)."},"checksum":{"type":"string","description":"How `sha256` was obtained. computed: the server or `cloudhal media verify` streamed the URL and hashed it. declared: given by the person who linked it, not verified (shown as such).","enum":["computed","declared"]},"verifiedAt":{"$ref":"#/$defs/Timestamp","description":"When the URL last produced bytes with this SHA-256 (checksum `computed`)."}}}]},"HttpsUrl":{"type":"string","description":"Absolute https URL without credentials, whitespace or backslashes.","pattern":"^https://[^\\s\\\\/?#@]+(?:[/?#][^\\s\\\\]*)?$","maxLength":2048},"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Sha256":{"type":"string","description":"Lower-case hex SHA-256 digest.","pattern":"^[0-9a-f]{64}$"},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}}}; +const schema329 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; +const schema330 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; +const schema332 = {"type":"string","description":"Lower-case hex SHA-256 digest.","pattern":"^[0-9a-f]{64}$"}; +const schema334 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; +const schema339 = {"type":"object","description":"Intrinsic size and placeholders of an image. Present for every image stored as a blob.","additionalProperties":false,"required":["width","height"],"properties":{"width":{"type":"integer","minimum":1,"maximum":100000},"height":{"type":"integer","minimum":1,"maximum":100000},"animated":{"type":"boolean","description":"Animated GIF or WebP: never resized or re-encoded. Absent means false; the store never writes false."},"blurhash":{"type":"string","description":"BlurHash of the image for themes that decode it client-side.","pattern":"^[0-9A-Za-z#$%*+,.:;=?@^_{|}~\\[\\]-]{6,100}$"},"lqip":{"type":"string","description":"Low-quality placeholder: a raster data: URL of at most about 3 KB, shown while the image loads.","pattern":"^data:image/(?:webp|png|jpeg);base64,[A-Za-z0-9+/]+={0,2}$","maxLength":4096},"color":{"type":"string","description":"RFC-0028: the image's dominant colour, lower-case #rrggbb. Painted under the placeholder and in color-fill bars.","pattern":"^#[0-9a-f]{6}$"},"alpha":{"type":"boolean","enum":[true],"description":"RFC-0028: the image may have transparent pixels (read from its header at ingest). Absent means opaque; transparent images never show the LQIP."},"focal":{"type":"object","description":"RFC-0028: where the subject is, from the top-left corner as fractions of the width and height. Cover crops keep it in view.","additionalProperties":false,"required":["x","y"],"properties":{"x":{"type":"number","minimum":0,"maximum":1},"y":{"type":"number","minimum":0,"maximum":1}}}}}; +const schema340 = {"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"}; +const schema352 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema338 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema355 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const pattern181 = new RegExp("^[a-z0-9][a-z0-9!#$&^_.+-]{0,126}/[a-z0-9][a-z0-9!#$&^_.+-]{0,126}$", "u"); +const pattern187 = new RegExp("^[0-9A-Za-z#$%*+,.:;=?@^_{|}~\\[\\]-]{6,100}$", "u"); +const pattern188 = new RegExp("^data:image/(?:webp|png|jpeg);base64,[A-Za-z0-9+/]+={0,2}$", "u"); +const pattern189 = new RegExp("^#[0-9a-f]{6}$", "u"); +const schema335 = {"description":"Where the bytes live. `blob`: in this deployment's blob store (R2 or the filesystem) under the content-addressed key media//, derived and never stored, so the node is portable between backends. `external`: elsewhere, linked rather than hosted (large research files on Zenodo or Hugging Face).","oneOf":[{"type":"object","title":"BlobStorage","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["blob"]}}},{"type":"object","title":"ExternalStorage","additionalProperties":false,"required":["kind","url","checksum"],"properties":{"kind":{"type":"string","enum":["external"]},"url":{"$ref":"#/$defs/HttpsUrl","description":"Direct download URL of the file."},"landingPage":{"$ref":"#/$defs/HttpsUrl","description":"Human page of the record the file belongs to (e.g. a Zenodo record or a Hugging Face dataset)."},"checksum":{"type":"string","description":"How `sha256` was obtained. computed: the server or `cloudhal media verify` streamed the URL and hashed it. declared: given by the person who linked it, not verified (shown as such).","enum":["computed","declared"]},"verifiedAt":{"$ref":"#/$defs/Timestamp","description":"When the URL last produced bytes with this SHA-256 (checksum `computed`)."}}}]}; +const schema336 = {"type":"string","description":"Absolute https URL without credentials, whitespace or backslashes.","pattern":"^https://[^\\s\\\\/?#@]+(?:[/?#][^\\s\\\\]*)?$","maxLength":2048}; +const pattern185 = new RegExp("^https://[^\\s\\\\/?#@]+(?:[/?#][^\\s\\\\]*)?$", "u"); -function validate163(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate179(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate163.evaluated; +const evaluated0 = validate179.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -24181,7 +26548,7 @@ vErrors.push(err2); errors++; } if(!(data0 === "blob")){ -const err3 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema318.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err3 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema335.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err3]; } @@ -24265,7 +26632,7 @@ vErrors.push(err9); errors++; } if(!(data1 === "external")){ -const err10 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema318.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err10 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema335.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err10]; } @@ -24288,7 +26655,7 @@ vErrors.push(err11); } errors++; } -if(!pattern169.test(data2)){ +if(!pattern185.test(data2)){ const err12 = {instancePath:instancePath+"/url",schemaPath:"#/$defs/HttpsUrl/pattern",keyword:"pattern",params:{pattern: "^https://[^\\s\\\\/?#@]+(?:[/?#][^\\s\\\\]*)?$"},message:"must match pattern \""+"^https://[^\\s\\\\/?#@]+(?:[/?#][^\\s\\\\]*)?$"+"\""}; if(vErrors === null){ vErrors = [err12]; @@ -24323,7 +26690,7 @@ vErrors.push(err14); } errors++; } -if(!pattern169.test(data3)){ +if(!pattern185.test(data3)){ const err15 = {instancePath:instancePath+"/landingPage",schemaPath:"#/$defs/HttpsUrl/pattern",keyword:"pattern",params:{pattern: "^https://[^\\s\\\\/?#@]+(?:[/?#][^\\s\\\\]*)?$"},message:"must match pattern \""+"^https://[^\\s\\\\/?#@]+(?:[/?#][^\\s\\\\]*)?$"+"\""}; if(vErrors === null){ vErrors = [err15]; @@ -24358,7 +26725,7 @@ vErrors.push(err17); errors++; } if(!((data4 === "computed") || (data4 === "declared"))){ -const err18 = {instancePath:instancePath+"/checksum",schemaPath:"#/oneOf/1/properties/checksum/enum",keyword:"enum",params:{allowedValues: schema318.oneOf[1].properties.checksum.enum},message:"must be equal to one of the allowed values"}; +const err18 = {instancePath:instancePath+"/checksum",schemaPath:"#/oneOf/1/properties/checksum/enum",keyword:"enum",params:{allowedValues: schema335.oneOf[1].properties.checksum.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err18]; } @@ -24439,19 +26806,19 @@ vErrors = null; } } } -validate163.errors = vErrors; +validate179.errors = vErrors; evaluated0.props = props0; return errors === 0; } -validate163.evaluated = {"dynamicProps":true,"dynamicItems":false}; +validate179.evaluated = {"dynamicProps":true,"dynamicItems":false}; -const schema326 = {"description":"Terms the asset may be reused under: an SPDX license expression or a license URL.","oneOf":[{"type":"object","title":"SpdxLicense","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"type":"string","enum":["spdx"]},"id":{"type":"string","description":"SPDX license identifier or simple expression, e.g. CC-BY-4.0, CC0-1.0, MIT OR Apache-2.0, LicenseRef-AllRightsReserved.","pattern":"^[A-Za-z0-9][A-Za-z0-9.+-]*(?: (?:AND|OR|WITH) [A-Za-z0-9][A-Za-z0-9.+-]*)*$","maxLength":200}}},{"type":"object","title":"UrlLicense","additionalProperties":false,"required":["kind","url"],"properties":{"kind":{"type":"string","enum":["url"]},"url":{"$ref":"#/$defs/HttpsUrl"},"name":{"type":"string","minLength":1,"maxLength":200}}}]}; -const pattern177 = new RegExp("^[A-Za-z0-9][A-Za-z0-9.+-]*(?: (?:AND|OR|WITH) [A-Za-z0-9][A-Za-z0-9.+-]*)*$", "u"); +const schema343 = {"description":"Terms the asset may be reused under: an SPDX license expression or a license URL.","oneOf":[{"type":"object","title":"SpdxLicense","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"type":"string","enum":["spdx"]},"id":{"type":"string","description":"SPDX license identifier or simple expression, e.g. CC-BY-4.0, CC0-1.0, MIT OR Apache-2.0, LicenseRef-AllRightsReserved.","pattern":"^[A-Za-z0-9][A-Za-z0-9.+-]*(?: (?:AND|OR|WITH) [A-Za-z0-9][A-Za-z0-9.+-]*)*$","maxLength":200}}},{"type":"object","title":"UrlLicense","additionalProperties":false,"required":["kind","url"],"properties":{"kind":{"type":"string","enum":["url"]},"url":{"$ref":"#/$defs/HttpsUrl"},"name":{"type":"string","minLength":1,"maxLength":200}}}]}; +const pattern193 = new RegExp("^[A-Za-z0-9][A-Za-z0-9.+-]*(?: (?:AND|OR|WITH) [A-Za-z0-9][A-Za-z0-9.+-]*)*$", "u"); -function validate165(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate181(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate165.evaluated; +const evaluated0 = validate181.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -24508,7 +26875,7 @@ vErrors.push(err3); errors++; } if(!(data0 === "spdx")){ -const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema326.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema343.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -24531,7 +26898,7 @@ vErrors.push(err5); } errors++; } -if(!pattern177.test(data1)){ +if(!pattern193.test(data1)){ const err6 = {instancePath:instancePath+"/id",schemaPath:"#/oneOf/0/properties/id/pattern",keyword:"pattern",params:{pattern: "^[A-Za-z0-9][A-Za-z0-9.+-]*(?: (?:AND|OR|WITH) [A-Za-z0-9][A-Za-z0-9.+-]*)*$"},message:"must match pattern \""+"^[A-Za-z0-9][A-Za-z0-9.+-]*(?: (?:AND|OR|WITH) [A-Za-z0-9][A-Za-z0-9.+-]*)*$"+"\""}; if(vErrors === null){ vErrors = [err6]; @@ -24617,7 +26984,7 @@ vErrors.push(err12); errors++; } if(!(data2 === "url")){ -const err13 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema326.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err13 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema343.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err13]; } @@ -24640,7 +27007,7 @@ vErrors.push(err14); } errors++; } -if(!pattern169.test(data3)){ +if(!pattern185.test(data3)){ const err15 = {instancePath:instancePath+"/url",schemaPath:"#/$defs/HttpsUrl/pattern",keyword:"pattern",params:{pattern: "^https://[^\\s\\\\/?#@]+(?:[/?#][^\\s\\\\]*)?$"},message:"must match pattern \""+"^https://[^\\s\\\\/?#@]+(?:[/?#][^\\s\\\\]*)?$"+"\""}; if(vErrors === null){ vErrors = [err15]; @@ -24743,19 +27110,19 @@ vErrors = null; } } } -validate165.errors = vErrors; +validate181.errors = vErrors; evaluated0.props = props0; return errors === 0; } -validate165.evaluated = {"dynamicProps":true,"dynamicItems":false}; +validate181.evaluated = {"dynamicProps":true,"dynamicItems":false}; -const schema328 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; -const schema329 = {"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]}; +const schema345 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; +const schema346 = {"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]}; -function validate167(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate183(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate167.evaluated; +const evaluated0 = validate183.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -24808,7 +27175,7 @@ vErrors.push(err3); errors++; } if(!(((data0 === "human") || (data0 === "agent")) || (data0 === "system"))){ -const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema329.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema346.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -24899,18 +27266,18 @@ vErrors.push(err11); } errors++; } -validate167.errors = vErrors; +validate183.errors = vErrors; return errors === 0; } -validate167.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate183.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const schema331 = {"description":"How the asset came in. Immutable after create.","oneOf":[{"type":"object","title":"UploadSource","description":"Bytes uploaded by a person (REST) or an agent (MCP upload_asset).","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["upload"]},"filename":{"type":"string","description":"Original file name as sent (never used as a storage key or served).","pattern":"^[^/\\\\\\u0000-\\u001f\\u007f]+$","minLength":1,"maxLength":255}}},{"type":"object","title":"FetchSource","description":"Bytes the server fetched from an https URL (MCP fetch_asset_url, Studio 'From URL') under the SSRF rules.","additionalProperties":false,"required":["kind","url","fetchedAt"],"properties":{"kind":{"type":"string","enum":["fetch"]},"url":{"$ref":"#/$defs/HttpsUrl"},"fetchedAt":{"$ref":"#/$defs/Timestamp"}}},{"type":"object","title":"ImportSource","description":"Created by a migration or importer (e.g. `cloudhal media import-urls` from an ImageRef.src).","additionalProperties":false,"required":["kind","from"],"properties":{"kind":{"type":"string","enum":["import"]},"from":{"type":"string","description":"The reference it was imported from: a URL or a site path.","minLength":1,"maxLength":2048},"importer":{"$ref":"#/$defs/Slug"}}},{"type":"object","title":"LinkSource","description":"An external file registered by URL without copying its bytes (storage.kind external).","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["link"]}}}]}; -const pattern180 = new RegExp("^[^/\\\\\\u0000-\\u001f\\u007f]+$", "u"); +const schema348 = {"description":"How the asset came in. Immutable after create.","oneOf":[{"type":"object","title":"UploadSource","description":"Bytes uploaded by a person (REST) or an agent (MCP upload_asset).","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["upload"]},"filename":{"type":"string","description":"Original file name as sent (never used as a storage key or served).","pattern":"^[^/\\\\\\u0000-\\u001f\\u007f]+$","minLength":1,"maxLength":255}}},{"type":"object","title":"FetchSource","description":"Bytes the server fetched from an https URL (MCP fetch_asset_url, Studio 'From URL') under the SSRF rules.","additionalProperties":false,"required":["kind","url","fetchedAt"],"properties":{"kind":{"type":"string","enum":["fetch"]},"url":{"$ref":"#/$defs/HttpsUrl"},"fetchedAt":{"$ref":"#/$defs/Timestamp"}}},{"type":"object","title":"ImportSource","description":"Created by a migration or importer (e.g. `cloudhal media import-urls` from an ImageRef.src).","additionalProperties":false,"required":["kind","from"],"properties":{"kind":{"type":"string","enum":["import"]},"from":{"type":"string","description":"The reference it was imported from: a URL or a site path.","minLength":1,"maxLength":2048},"importer":{"$ref":"#/$defs/Slug"}}},{"type":"object","title":"LinkSource","description":"An external file registered by URL without copying its bytes (storage.kind external).","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["link"]}}}]}; +const pattern196 = new RegExp("^[^/\\\\\\u0000-\\u001f\\u007f]+$", "u"); -function validate169(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate185(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate169.evaluated; +const evaluated0 = validate185.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -24957,7 +27324,7 @@ vErrors.push(err2); errors++; } if(!(data0 === "upload")){ -const err3 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema331.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err3 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema348.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err3]; } @@ -24990,7 +27357,7 @@ vErrors.push(err5); } errors++; } -if(!pattern180.test(data1)){ +if(!pattern196.test(data1)){ const err6 = {instancePath:instancePath+"/filename",schemaPath:"#/oneOf/0/properties/filename/pattern",keyword:"pattern",params:{pattern: "^[^/\\\\\\u0000-\\u001f\\u007f]+$"},message:"must match pattern \""+"^[^/\\\\\\u0000-\\u001f\\u007f]+$"+"\""}; if(vErrors === null){ vErrors = [err6]; @@ -25086,7 +27453,7 @@ vErrors.push(err13); errors++; } if(!(data2 === "fetch")){ -const err14 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema331.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err14 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema348.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err14]; } @@ -25109,7 +27476,7 @@ vErrors.push(err15); } errors++; } -if(!pattern169.test(data3)){ +if(!pattern185.test(data3)){ const err16 = {instancePath:instancePath+"/url",schemaPath:"#/$defs/HttpsUrl/pattern",keyword:"pattern",params:{pattern: "^https://[^\\s\\\\/?#@]+(?:[/?#][^\\s\\\\]*)?$"},message:"must match pattern \""+"^https://[^\\s\\\\/?#@]+(?:[/?#][^\\s\\\\]*)?$"+"\""}; if(vErrors === null){ vErrors = [err16]; @@ -25227,7 +27594,7 @@ vErrors.push(err24); errors++; } if(!(data5 === "import")){ -const err25 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/2/properties/kind/enum",keyword:"enum",params:{allowedValues: schema331.oneOf[2].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err25 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/2/properties/kind/enum",keyword:"enum",params:{allowedValues: schema348.oneOf[2].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err25]; } @@ -25378,7 +27745,7 @@ vErrors.push(err36); errors++; } if(!(data8 === "link")){ -const err37 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/3/properties/kind/enum",keyword:"enum",params:{allowedValues: schema331.oneOf[3].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err37 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/3/properties/kind/enum",keyword:"enum",params:{allowedValues: schema348.oneOf[3].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err37]; } @@ -25436,18 +27803,18 @@ vErrors = null; } } } -validate169.errors = vErrors; +validate185.errors = vErrors; evaluated0.props = props0; return errors === 0; } -validate169.evaluated = {"dynamicProps":true,"dynamicItems":false}; +validate185.evaluated = {"dynamicProps":true,"dynamicItems":false}; -function validate162(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate178(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ /*# sourceURL="https://cloudhal.dev/schemas/v1/asset.schema.json" */; let vErrors = null; let errors = 0; -const evaluated0 = validate162.evaluated; +const evaluated0 = validate178.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -25606,7 +27973,7 @@ vErrors.push(err14); errors++; } for(const key0 in data){ -if(!(func1.call(schema311.properties, key0))){ +if(!(func1.call(schema328.properties, key0))){ const err15 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err15]; @@ -25630,7 +27997,7 @@ vErrors.push(err16); errors++; } if(!(data0 === 1)){ -const err17 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema312.enum},message:"must be equal to one of the allowed values"}; +const err17 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema329.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err17]; } @@ -25743,7 +28110,7 @@ vErrors.push(err26); errors++; } if(!((((((data3 === "image") || (data3 === "video")) || (data3 === "audio")) || (data3 === "document")) || (data3 === "data")) || (data3 === "other"))){ -const err27 = {instancePath:instancePath+"/kind",schemaPath:"#/properties/kind/enum",keyword:"enum",params:{allowedValues: schema311.properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err27 = {instancePath:instancePath+"/kind",schemaPath:"#/properties/kind/enum",keyword:"enum",params:{allowedValues: schema328.properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err27]; } @@ -25766,7 +28133,7 @@ vErrors.push(err28); } errors++; } -if(!pattern165.test(data4)){ +if(!pattern181.test(data4)){ const err29 = {instancePath:instancePath+"/mediaType",schemaPath:"#/properties/mediaType/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9][a-z0-9!#$&^_.+-]{0,126}/[a-z0-9][a-z0-9!#$&^_.+-]{0,126}$"},message:"must match pattern \""+"^[a-z0-9][a-z0-9!#$&^_.+-]{0,126}/[a-z0-9][a-z0-9!#$&^_.+-]{0,126}$"+"\""}; if(vErrors === null){ vErrors = [err29]; @@ -25816,7 +28183,7 @@ errors++; if(data.sha256 !== undefined){ let data6 = data.sha256; if(typeof data6 === "string"){ -if(!pattern50.test(data6)){ +if(!pattern66.test(data6)){ const err33 = {instancePath:instancePath+"/sha256",schemaPath:"#/$defs/Sha256/pattern",keyword:"pattern",params:{pattern: "^[0-9a-f]{64}$"},message:"must match pattern \""+"^[0-9a-f]{64}$"+"\""}; if(vErrors === null){ vErrors = [err33]; @@ -25841,7 +28208,7 @@ errors++; if(data.originalSha256 !== undefined){ let data7 = data.originalSha256; if(typeof data7 === "string"){ -if(!pattern50.test(data7)){ +if(!pattern66.test(data7)){ const err35 = {instancePath:instancePath+"/originalSha256",schemaPath:"#/$defs/Sha256/pattern",keyword:"pattern",params:{pattern: "^[0-9a-f]{64}$"},message:"must match pattern \""+"^[0-9a-f]{64}$"+"\""}; if(vErrors === null){ vErrors = [err35]; @@ -25909,8 +28276,8 @@ errors++; } } if(data.storage !== undefined){ -if(!(validate163(data.storage, {instancePath:instancePath+"/storage",parentData:data,parentDataProperty:"storage",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate163.errors : vErrors.concat(validate163.errors); +if(!(validate179(data.storage, {instancePath:instancePath+"/storage",parentData:data,parentDataProperty:"storage",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate179.errors : vErrors.concat(validate179.errors); errors = vErrors.length; } } @@ -26034,7 +28401,7 @@ errors++; if(data10.blurhash !== undefined){ let data14 = data10.blurhash; if(typeof data14 === "string"){ -if(!pattern171.test(data14)){ +if(!pattern187.test(data14)){ const err51 = {instancePath:instancePath+"/image/blurhash",schemaPath:"#/$defs/AssetImage/properties/blurhash/pattern",keyword:"pattern",params:{pattern: "^[0-9A-Za-z#$%*+,.:;=?@^_{|}~\\[\\]-]{6,100}$"},message:"must match pattern \""+"^[0-9A-Za-z#$%*+,.:;=?@^_{|}~\\[\\]-]{6,100}$"+"\""}; if(vErrors === null){ vErrors = [err51]; @@ -26069,7 +28436,7 @@ vErrors.push(err53); } errors++; } -if(!pattern172.test(data15)){ +if(!pattern188.test(data15)){ const err54 = {instancePath:instancePath+"/image/lqip",schemaPath:"#/$defs/AssetImage/properties/lqip/pattern",keyword:"pattern",params:{pattern: "^data:image/(?:webp|png|jpeg);base64,[A-Za-z0-9+/]+={0,2}$"},message:"must match pattern \""+"^data:image/(?:webp|png|jpeg);base64,[A-Za-z0-9+/]+={0,2}$"+"\""}; if(vErrors === null){ vErrors = [err54]; @@ -26094,7 +28461,7 @@ errors++; if(data10.color !== undefined){ let data16 = data10.color; if(typeof data16 === "string"){ -if(!pattern173.test(data16)){ +if(!pattern189.test(data16)){ const err56 = {instancePath:instancePath+"/image/color",schemaPath:"#/$defs/AssetImage/properties/color/pattern",keyword:"pattern",params:{pattern: "^#[0-9a-f]{6}$"},message:"must match pattern \""+"^#[0-9a-f]{6}$"+"\""}; if(vErrors === null){ vErrors = [err56]; @@ -26129,7 +28496,7 @@ vErrors.push(err58); errors++; } if(!(data17 === true)){ -const err59 = {instancePath:instancePath+"/image/alpha",schemaPath:"#/$defs/AssetImage/properties/alpha/enum",keyword:"enum",params:{allowedValues: schema322.properties.alpha.enum},message:"must be equal to one of the allowed values"}; +const err59 = {instancePath:instancePath+"/image/alpha",schemaPath:"#/$defs/AssetImage/properties/alpha/enum",keyword:"enum",params:{allowedValues: schema339.properties.alpha.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err59]; } @@ -26604,8 +28971,8 @@ errors++; } } if(data.license !== undefined){ -if(!(validate165(data.license, {instancePath:instancePath+"/license",parentData:data,parentDataProperty:"license",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate165.errors : vErrors.concat(validate165.errors); +if(!(validate181(data.license, {instancePath:instancePath+"/license",parentData:data,parentDataProperty:"license",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate181.errors : vErrors.concat(validate181.errors); errors = vErrors.length; } } @@ -26622,7 +28989,7 @@ vErrors.push(err99); errors++; } if(!((data30 === "active") || (data30 === "retired"))){ -const err100 = {instancePath:instancePath+"/status",schemaPath:"#/properties/status/enum",keyword:"enum",params:{allowedValues: schema311.properties.status.enum},message:"must be equal to one of the allowed values"}; +const err100 = {instancePath:instancePath+"/status",schemaPath:"#/properties/status/enum",keyword:"enum",params:{allowedValues: schema328.properties.status.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err100]; } @@ -26633,14 +29000,14 @@ errors++; } } if(data.uploadedBy !== undefined){ -if(!(validate167(data.uploadedBy, {instancePath:instancePath+"/uploadedBy",parentData:data,parentDataProperty:"uploadedBy",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate167.errors : vErrors.concat(validate167.errors); +if(!(validate183(data.uploadedBy, {instancePath:instancePath+"/uploadedBy",parentData:data,parentDataProperty:"uploadedBy",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate183.errors : vErrors.concat(validate183.errors); errors = vErrors.length; } } if(data.source !== undefined){ -if(!(validate169(data.source, {instancePath:instancePath+"/source",parentData:data,parentDataProperty:"source",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate169.errors : vErrors.concat(validate169.errors); +if(!(validate185(data.source, {instancePath:instancePath+"/source",parentData:data,parentDataProperty:"source",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate185.errors : vErrors.concat(validate185.errors); errors = vErrors.length; } } @@ -26757,30 +29124,30 @@ vErrors.push(err109); } errors++; } -validate162.errors = vErrors; +validate178.errors = vErrors; return errors === 0; } -validate162.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate178.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -export const validateMenu = validate171; -const schema339 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/menu.schema.json","title":"Menu","description":"A named navigation menu of a site (e.g. 'primary', 'footer', 'sidebar'): an ordered tree of items. Items point at pages, entries and terms by id, so renames and slug changes never break them; an item whose target is gone, retired or unpublished is hidden by the renderer, never rendered as a dead link (RFC-0003).","type":"object","additionalProperties":false,"required":["schemaVersion","id","siteId","key","maxDepth","items","rev","createdAt","updatedAt"],"properties":{"schemaVersion":{"$ref":"#/$defs/SchemaVersion"},"id":{"$ref":"#/$defs/Id"},"siteId":{"$ref":"#/$defs/Id"},"key":{"$ref":"#/$defs/Slug","description":"Where themes place the menu ('primary', 'footer', 'sidebar', …). Unique per site and immutable once created."},"maxDepth":{"type":"integer","description":"Deepest level an item may sit at; top-level items are level 1. The store refuses deeper trees.","minimum":1,"maximum":6},"items":{"type":"array","description":"Top-level items in display order. Empty means the site deliberately shows no items here (a missing menu means the theme's derived default).","items":{"$ref":"#/$defs/MenuItem"},"maxItems":100},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"metadata":{"$ref":"#/$defs/Metadata"}},"$defs":{"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"MenuDisplay":{"type":"string","description":"RFC-0025 25d (RFC-0003 amendment): how a top-level item with children opens in the header. `mega`: one wide panel, its children as columns (a group's children are the column's links).","enum":["mega"]},"MenuItem":{"description":"One item, discriminated by `kind` (what it links to). Children, in display order, form the tree; an item has exactly one parent by construction, so the tree has no cycles.","oneOf":[{"type":"object","title":"PageMenuItem","description":"Links to a page by id; the label falls back to the page title.","additionalProperties":false,"required":["id","kind","pageId"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["page"]},"pageId":{"$ref":"#/$defs/Id"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"EntryMenuItem","description":"Links to a content entry by id (at //); the label falls back to the entry title.","additionalProperties":false,"required":["id","kind","entryId"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["contentEntry"]},"entryId":{"$ref":"#/$defs/Id"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"TermMenuItem","description":"Links to a term's archive by term id; the label falls back to the term name in the locale.","additionalProperties":false,"required":["id","kind","termId"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["term"]},"termId":{"$ref":"#/$defs/Id"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"CollectionMenuItem","description":"Links to the index of a content collection, wherever the theme serves it. Collections have no node and no title, so labels are required.","additionalProperties":false,"required":["id","kind","collection","labels"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["collection"]},"collection":{"$ref":"#/$defs/Slug"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"UrlMenuItem","description":"Links to an external https URL. The host is ASCII (internationalised names in punycode) and carries no user-info, so a label cannot hide where the link goes.","additionalProperties":false,"required":["id","kind","href","labels"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["url"]},"href":{"type":"string","pattern":"^https://[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?(?::[0-9]{1,5})?(?:[/?#][^\\s\\\\\\x00-\\x1F\\x7F]*)?$","maxLength":2048},"newTab":{"type":"boolean","description":"Open in a new tab (rendered with rel=\"noopener noreferrer\")."},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"GroupMenuItem","description":"A label without a link that holds other items: a dropdown parent or a sidebar section heading. Hidden when none of its children is shown.","additionalProperties":false,"required":["id","kind","labels","children"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["group"]},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}}]},"MenuLabels":{"type":"object","description":"Item label per locale; plain text, no control characters or surrounding whitespace. A locale without a label falls back to the default locale's label, then to the target's own title or name.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[^\\s\\x00-\\x1F\\x7F](?:[^\\x00-\\x1F\\x7F]*[^\\s\\x00-\\x1F\\x7F])?$"}},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}}}; -const schema340 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; -const schema341 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; -const schema343 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; -const schema363 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; -const schema364 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; -const schema366 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; -const schema344 = {"description":"One item, discriminated by `kind` (what it links to). Children, in display order, form the tree; an item has exactly one parent by construction, so the tree has no cycles.","oneOf":[{"type":"object","title":"PageMenuItem","description":"Links to a page by id; the label falls back to the page title.","additionalProperties":false,"required":["id","kind","pageId"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["page"]},"pageId":{"$ref":"#/$defs/Id"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"EntryMenuItem","description":"Links to a content entry by id (at //); the label falls back to the entry title.","additionalProperties":false,"required":["id","kind","entryId"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["contentEntry"]},"entryId":{"$ref":"#/$defs/Id"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"TermMenuItem","description":"Links to a term's archive by term id; the label falls back to the term name in the locale.","additionalProperties":false,"required":["id","kind","termId"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["term"]},"termId":{"$ref":"#/$defs/Id"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"CollectionMenuItem","description":"Links to the index of a content collection, wherever the theme serves it. Collections have no node and no title, so labels are required.","additionalProperties":false,"required":["id","kind","collection","labels"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["collection"]},"collection":{"$ref":"#/$defs/Slug"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"UrlMenuItem","description":"Links to an external https URL. The host is ASCII (internationalised names in punycode) and carries no user-info, so a label cannot hide where the link goes.","additionalProperties":false,"required":["id","kind","href","labels"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["url"]},"href":{"type":"string","pattern":"^https://[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?(?::[0-9]{1,5})?(?:[/?#][^\\s\\\\\\x00-\\x1F\\x7F]*)?$","maxLength":2048},"newTab":{"type":"boolean","description":"Open in a new tab (rendered with rel=\"noopener noreferrer\")."},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"GroupMenuItem","description":"A label without a link that holds other items: a dropdown parent or a sidebar section heading. Hidden when none of its children is shown.","additionalProperties":false,"required":["id","kind","labels","children"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["group"]},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}}]}; -const schema349 = {"type":"string","description":"RFC-0025 25d (RFC-0003 amendment): how a top-level item with children opens in the header. `mega`: one wide panel, its children as columns (a group's children are the column's links).","enum":["mega"]}; -const pattern197 = new RegExp("^https://[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?(?::[0-9]{1,5})?(?:[/?#][^\\s\\\\\\x00-\\x1F\\x7F]*)?$", "u"); -const schema347 = {"type":"object","description":"Item label per locale; plain text, no control characters or surrounding whitespace. A locale without a label falls back to the default locale's label, then to the target's own title or name.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[^\\s\\x00-\\x1F\\x7F](?:[^\\x00-\\x1F\\x7F]*[^\\s\\x00-\\x1F\\x7F])?$"}}; -const schema348 = {"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"}; -const pattern189 = new RegExp("^[^\\s\\x00-\\x1F\\x7F](?:[^\\x00-\\x1F\\x7F]*[^\\s\\x00-\\x1F\\x7F])?$", "u"); +export const validateMenu = validate187; +const schema356 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/menu.schema.json","title":"Menu","description":"A named navigation menu of a site (e.g. 'primary', 'footer', 'sidebar'): an ordered tree of items. Items point at pages, entries and terms by id, so renames and slug changes never break them; an item whose target is gone, retired or unpublished is hidden by the renderer, never rendered as a dead link (RFC-0003).","type":"object","additionalProperties":false,"required":["schemaVersion","id","siteId","key","maxDepth","items","rev","createdAt","updatedAt"],"properties":{"schemaVersion":{"$ref":"#/$defs/SchemaVersion"},"id":{"$ref":"#/$defs/Id"},"siteId":{"$ref":"#/$defs/Id"},"key":{"$ref":"#/$defs/Slug","description":"Where themes place the menu ('primary', 'footer', 'sidebar', …). Unique per site and immutable once created."},"maxDepth":{"type":"integer","description":"Deepest level an item may sit at; top-level items are level 1. The store refuses deeper trees.","minimum":1,"maximum":6},"items":{"type":"array","description":"Top-level items in display order. Empty means the site deliberately shows no items here (a missing menu means the theme's derived default).","items":{"$ref":"#/$defs/MenuItem"},"maxItems":100},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"metadata":{"$ref":"#/$defs/Metadata"}},"$defs":{"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"MenuDisplay":{"type":"string","description":"RFC-0025 25d (RFC-0003 amendment): how a top-level item with children opens in the header. `mega`: one wide panel, its children as columns (a group's children are the column's links).","enum":["mega"]},"MenuItem":{"description":"One item, discriminated by `kind` (what it links to). Children, in display order, form the tree; an item has exactly one parent by construction, so the tree has no cycles.","oneOf":[{"type":"object","title":"PageMenuItem","description":"Links to a page by id; the label falls back to the page title.","additionalProperties":false,"required":["id","kind","pageId"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["page"]},"pageId":{"$ref":"#/$defs/Id"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"EntryMenuItem","description":"Links to a content entry by id (at //); the label falls back to the entry title.","additionalProperties":false,"required":["id","kind","entryId"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["contentEntry"]},"entryId":{"$ref":"#/$defs/Id"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"TermMenuItem","description":"Links to a term's archive by term id; the label falls back to the term name in the locale.","additionalProperties":false,"required":["id","kind","termId"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["term"]},"termId":{"$ref":"#/$defs/Id"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"CollectionMenuItem","description":"Links to the index of a content collection, wherever the theme serves it. Collections have no node and no title, so labels are required.","additionalProperties":false,"required":["id","kind","collection","labels"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["collection"]},"collection":{"$ref":"#/$defs/Slug"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"UrlMenuItem","description":"Links to an external https URL. The host is ASCII (internationalised names in punycode) and carries no user-info, so a label cannot hide where the link goes.","additionalProperties":false,"required":["id","kind","href","labels"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["url"]},"href":{"type":"string","pattern":"^https://[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?(?::[0-9]{1,5})?(?:[/?#][^\\s\\\\\\x00-\\x1F\\x7F]*)?$","maxLength":2048},"newTab":{"type":"boolean","description":"Open in a new tab (rendered with rel=\"noopener noreferrer\")."},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"GroupMenuItem","description":"A label without a link that holds other items: a dropdown parent or a sidebar section heading. Hidden when none of its children is shown.","additionalProperties":false,"required":["id","kind","labels","children"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["group"]},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}}]},"MenuLabels":{"type":"object","description":"Item label per locale; plain text, no control characters or surrounding whitespace. A locale without a label falls back to the default locale's label, then to the target's own title or name.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[^\\s\\x00-\\x1F\\x7F](?:[^\\x00-\\x1F\\x7F]*[^\\s\\x00-\\x1F\\x7F])?$"}},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}}}; +const schema357 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; +const schema358 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; +const schema360 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; +const schema380 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema381 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema383 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const schema361 = {"description":"One item, discriminated by `kind` (what it links to). Children, in display order, form the tree; an item has exactly one parent by construction, so the tree has no cycles.","oneOf":[{"type":"object","title":"PageMenuItem","description":"Links to a page by id; the label falls back to the page title.","additionalProperties":false,"required":["id","kind","pageId"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["page"]},"pageId":{"$ref":"#/$defs/Id"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"EntryMenuItem","description":"Links to a content entry by id (at //); the label falls back to the entry title.","additionalProperties":false,"required":["id","kind","entryId"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["contentEntry"]},"entryId":{"$ref":"#/$defs/Id"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"TermMenuItem","description":"Links to a term's archive by term id; the label falls back to the term name in the locale.","additionalProperties":false,"required":["id","kind","termId"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["term"]},"termId":{"$ref":"#/$defs/Id"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"CollectionMenuItem","description":"Links to the index of a content collection, wherever the theme serves it. Collections have no node and no title, so labels are required.","additionalProperties":false,"required":["id","kind","collection","labels"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["collection"]},"collection":{"$ref":"#/$defs/Slug"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"UrlMenuItem","description":"Links to an external https URL. The host is ASCII (internationalised names in punycode) and carries no user-info, so a label cannot hide where the link goes.","additionalProperties":false,"required":["id","kind","href","labels"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["url"]},"href":{"type":"string","pattern":"^https://[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?(?::[0-9]{1,5})?(?:[/?#][^\\s\\\\\\x00-\\x1F\\x7F]*)?$","maxLength":2048},"newTab":{"type":"boolean","description":"Open in a new tab (rendered with rel=\"noopener noreferrer\")."},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"GroupMenuItem","description":"A label without a link that holds other items: a dropdown parent or a sidebar section heading. Hidden when none of its children is shown.","additionalProperties":false,"required":["id","kind","labels","children"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["group"]},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}}]}; +const schema366 = {"type":"string","description":"RFC-0025 25d (RFC-0003 amendment): how a top-level item with children opens in the header. `mega`: one wide panel, its children as columns (a group's children are the column's links).","enum":["mega"]}; +const pattern213 = new RegExp("^https://[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?(?::[0-9]{1,5})?(?:[/?#][^\\s\\\\\\x00-\\x1F\\x7F]*)?$", "u"); +const schema364 = {"type":"object","description":"Item label per locale; plain text, no control characters or surrounding whitespace. A locale without a label falls back to the default locale's label, then to the target's own title or name.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[^\\s\\x00-\\x1F\\x7F](?:[^\\x00-\\x1F\\x7F]*[^\\s\\x00-\\x1F\\x7F])?$"}}; +const schema365 = {"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"}; +const pattern205 = new RegExp("^[^\\s\\x00-\\x1F\\x7F](?:[^\\x00-\\x1F\\x7F]*[^\\s\\x00-\\x1F\\x7F])?$", "u"); -function validate173(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate189(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate173.evaluated; +const evaluated0 = validate189.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -26857,7 +29224,7 @@ vErrors.push(err5); } errors++; } -if(!pattern189.test(data0)){ +if(!pattern205.test(data0)){ const err6 = {instancePath:instancePath+"/" + key1.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/additionalProperties/pattern",keyword:"pattern",params:{pattern: "^[^\\s\\x00-\\x1F\\x7F](?:[^\\x00-\\x1F\\x7F]*[^\\s\\x00-\\x1F\\x7F])?$"},message:"must match pattern \""+"^[^\\s\\x00-\\x1F\\x7F](?:[^\\x00-\\x1F\\x7F]*[^\\s\\x00-\\x1F\\x7F])?$"+"\""}; if(vErrors === null){ vErrors = [err6]; @@ -26890,17 +29257,17 @@ vErrors.push(err8); } errors++; } -validate173.errors = vErrors; +validate189.errors = vErrors; return errors === 0; } -validate173.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate189.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const wrapper0 = {validate: validate172}; +const wrapper0 = {validate: validate188}; -function validate172(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate188(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate172.evaluated; +const evaluated0 = validate188.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -27012,7 +29379,7 @@ vErrors.push(err8); errors++; } if(!(data1 === "page")){ -const err9 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema344.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err9 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema361.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err9]; } @@ -27068,8 +29435,8 @@ errors++; } } if(data.labels !== undefined){ -if(!(validate173(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate173.errors : vErrors.concat(validate173.errors); +if(!(validate189(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate189.errors : vErrors.concat(validate189.errors); errors = vErrors.length; } } @@ -27086,7 +29453,7 @@ vErrors.push(err14); errors++; } if(!(data4 === "mega")){ -const err15 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema349.enum},message:"must be equal to one of the allowed values"}; +const err15 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema366.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err15]; } @@ -27257,7 +29624,7 @@ vErrors.push(err28); errors++; } if(!(data8 === "contentEntry")){ -const err29 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema344.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err29 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema361.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err29]; } @@ -27313,8 +29680,8 @@ errors++; } } if(data.labels !== undefined){ -if(!(validate173(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate173.errors : vErrors.concat(validate173.errors); +if(!(validate189(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate189.errors : vErrors.concat(validate189.errors); errors = vErrors.length; } } @@ -27331,7 +29698,7 @@ vErrors.push(err34); errors++; } if(!(data11 === "mega")){ -const err35 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema349.enum},message:"must be equal to one of the allowed values"}; +const err35 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema366.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err35]; } @@ -27509,7 +29876,7 @@ vErrors.push(err48); errors++; } if(!(data15 === "term")){ -const err49 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/2/properties/kind/enum",keyword:"enum",params:{allowedValues: schema344.oneOf[2].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err49 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/2/properties/kind/enum",keyword:"enum",params:{allowedValues: schema361.oneOf[2].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err49]; } @@ -27565,8 +29932,8 @@ errors++; } } if(data.labels !== undefined){ -if(!(validate173(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate173.errors : vErrors.concat(validate173.errors); +if(!(validate189(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate189.errors : vErrors.concat(validate189.errors); errors = vErrors.length; } } @@ -27583,7 +29950,7 @@ vErrors.push(err54); errors++; } if(!(data18 === "mega")){ -const err55 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema349.enum},message:"must be equal to one of the allowed values"}; +const err55 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema366.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err55]; } @@ -27771,7 +30138,7 @@ vErrors.push(err69); errors++; } if(!(data22 === "collection")){ -const err70 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/3/properties/kind/enum",keyword:"enum",params:{allowedValues: schema344.oneOf[3].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err70 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/3/properties/kind/enum",keyword:"enum",params:{allowedValues: schema361.oneOf[3].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err70]; } @@ -27827,8 +30194,8 @@ errors++; } } if(data.labels !== undefined){ -if(!(validate173(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate173.errors : vErrors.concat(validate173.errors); +if(!(validate189(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate189.errors : vErrors.concat(validate189.errors); errors = vErrors.length; } } @@ -27845,7 +30212,7 @@ vErrors.push(err75); errors++; } if(!(data25 === "mega")){ -const err76 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema349.enum},message:"must be equal to one of the allowed values"}; +const err76 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema366.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err76]; } @@ -28033,7 +30400,7 @@ vErrors.push(err90); errors++; } if(!(data29 === "url")){ -const err91 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/4/properties/kind/enum",keyword:"enum",params:{allowedValues: schema344.oneOf[4].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err91 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/4/properties/kind/enum",keyword:"enum",params:{allowedValues: schema361.oneOf[4].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err91]; } @@ -28056,7 +30423,7 @@ vErrors.push(err92); } errors++; } -if(!pattern197.test(data30)){ +if(!pattern213.test(data30)){ const err93 = {instancePath:instancePath+"/href",schemaPath:"#/oneOf/4/properties/href/pattern",keyword:"pattern",params:{pattern: "^https://[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?(?::[0-9]{1,5})?(?:[/?#][^\\s\\\\\\x00-\\x1F\\x7F]*)?$"},message:"must match pattern \""+"^https://[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?(?::[0-9]{1,5})?(?:[/?#][^\\s\\\\\\x00-\\x1F\\x7F]*)?$"+"\""}; if(vErrors === null){ vErrors = [err93]; @@ -28091,8 +30458,8 @@ errors++; } } if(data.labels !== undefined){ -if(!(validate173(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate173.errors : vErrors.concat(validate173.errors); +if(!(validate189(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate189.errors : vErrors.concat(validate189.errors); errors = vErrors.length; } } @@ -28109,7 +30476,7 @@ vErrors.push(err96); errors++; } if(!(data33 === "mega")){ -const err97 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema349.enum},message:"must be equal to one of the allowed values"}; +const err97 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema366.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err97]; } @@ -28297,7 +30664,7 @@ vErrors.push(err111); errors++; } if(!(data37 === "group")){ -const err112 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/5/properties/kind/enum",keyword:"enum",params:{allowedValues: schema344.oneOf[5].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err112 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/5/properties/kind/enum",keyword:"enum",params:{allowedValues: schema361.oneOf[5].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err112]; } @@ -28308,8 +30675,8 @@ errors++; } } if(data.labels !== undefined){ -if(!(validate173(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate173.errors : vErrors.concat(validate173.errors); +if(!(validate189(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate189.errors : vErrors.concat(validate189.errors); errors = vErrors.length; } } @@ -28326,7 +30693,7 @@ vErrors.push(err113); errors++; } if(!(data39 === "mega")){ -const err114 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema349.enum},message:"must be equal to one of the allowed values"}; +const err114 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema366.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err114]; } @@ -28428,18 +30795,18 @@ vErrors = null; } } } -validate172.errors = vErrors; +validate188.errors = vErrors; evaluated0.props = props1; return errors === 0; } -validate172.evaluated = {"dynamicProps":true,"dynamicItems":false}; +validate188.evaluated = {"dynamicProps":true,"dynamicItems":false}; -function validate171(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate187(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ /*# sourceURL="https://cloudhal.dev/schemas/v1/menu.schema.json" */; let vErrors = null; let errors = 0; -const evaluated0 = validate171.evaluated; +const evaluated0 = validate187.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -28538,7 +30905,7 @@ vErrors.push(err8); errors++; } for(const key0 in data){ -if(!(func1.call(schema339.properties, key0))){ +if(!(func1.call(schema356.properties, key0))){ const err9 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err9]; @@ -28562,7 +30929,7 @@ vErrors.push(err10); errors++; } if(!(data0 === 1)){ -const err11 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema340.enum},message:"must be equal to one of the allowed values"}; +const err11 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema357.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err11]; } @@ -28757,8 +31124,8 @@ errors++; } const len0 = data5.length; for(let i0=0; i0.json`.","additionalProperties":false,"required":["rev","hash"],"properties":{"rev":{"$ref":"#/$defs/Revision"},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$"}}},"ReleaseOutcome":{"description":"What happened when the release was due, discriminated by `kind` (it matches the terminal `state`).","oneOf":[{"type":"object","title":"ReleasePublishedOutcome","description":"Applied: `snapshotId` is the new live snapshot (reason `scheduled`), composed from `baseSnapshotId` and the items. `purge` says how the edge cache followed.","additionalProperties":false,"required":["kind","runId","at","snapshotId","baseSnapshotId","latenessSeconds","purge"],"properties":{"kind":{"type":"string","enum":["published"]},"runId":{"$ref":"#/$defs/Id"},"at":{"$ref":"#/$defs/Timestamp"},"snapshotId":{"$ref":"#/$defs/Id"},"baseSnapshotId":{"$ref":"#/$defs/Id"},"latenessSeconds":{"type":"integer","minimum":0},"purge":{"type":"string","enum":["tags","everything","deferred","failed","none"]}}},{"type":"object","title":"ReleaseConflictOutcome","description":"Held: these nodes changed since the release was pinned. Nothing was published.","additionalProperties":false,"required":["kind","at","conflicts"],"properties":{"kind":{"type":"string","enum":["conflict"]},"runId":{"$ref":"#/$defs/Id"},"at":{"$ref":"#/$defs/Timestamp"},"conflicts":{"type":"array","items":{"$ref":"#/$defs/ReleaseConflict"},"minItems":1,"maxItems":100}}},{"type":"object","title":"ReleaseFailedOutcome","description":"Refused at run time; nothing was published.","additionalProperties":false,"required":["kind","at","code"],"properties":{"kind":{"type":"string","enum":["failed"]},"runId":{"$ref":"#/$defs/Id"},"at":{"$ref":"#/$defs/Timestamp"},"code":{"type":"string","enum":["dependency_missing","invalid_snapshot","site_missing","node_body_missing","store_error"]},"message":{"type":"string","maxLength":500,"pattern":"^[^\\x00-\\x1F\\x7F\\u202A-\\u202E\\u2066-\\u2069]*$"}}},{"type":"object","title":"ReleaseMissedOutcome","description":"The scheduler reached the release more than an hour after its time; nothing was published. A person may publish it now.","additionalProperties":false,"required":["kind","at","latenessSeconds"],"properties":{"kind":{"type":"string","enum":["missed"]},"at":{"$ref":"#/$defs/Timestamp"},"latenessSeconds":{"type":"integer","minimum":0}}},{"type":"object","title":"ReleaseExpiredOutcome","description":"No valid approval when the release was due: never approved, the approver can no longer publish it, the agent's `auto` window ended, or its creator left.","additionalProperties":false,"required":["kind","at","reason"],"properties":{"kind":{"type":"string","enum":["expired"]},"at":{"$ref":"#/$defs/Timestamp"},"reason":{"type":"string","enum":["unapproved","approval_lost","auto_window_ended","creator_removed"]}}},{"type":"object","title":"ReleaseCanceledOutcome","description":"Withdrawn by a person or an agent; its nodes are unlocked.","additionalProperties":false,"required":["kind","at","by"],"properties":{"kind":{"type":"string","enum":["canceled"]},"at":{"$ref":"#/$defs/Timestamp"},"by":{"$ref":"#/$defs/Actor"}}}]},"ReleasePerson":{"type":"object","description":"A staff person (`user_…`). Agents never appear here.","additionalProperties":false,"required":["id"],"properties":{"id":{"type":"string","pattern":"^user_[A-Za-z0-9]{6,32}$"},"name":{"type":"string","maxLength":200}}},"ReleaseState":{"type":"string","description":"Lifecycle. `draft`: proposed or being assembled, nothing locked. `awaiting_approval`: waiting for a person; its nodes are locked. `scheduled`: approved (or allowed by `auto` mode) and locked until it runs. `running`: claimed by the scheduler. `published`: applied; `outcome` names the snapshot. `conflict`: a pinned node or its live base changed; held, nothing published. `missed`: the scheduler reached it more than an hour late; nothing published. `expired`: no valid approval at its time. `failed`: refused at run time (e.g. a dependency is missing). `canceled`: withdrawn.","enum":["draft","awaiting_approval","scheduled","running","published","conflict","missed","expired","failed","canceled"]},"ReleaseWhen":{"description":"When the release runs, discriminated by `kind`. Absent while a draft has no time yet.","oneOf":[{"type":"object","title":"ReleaseAt","description":"At an instant. `at` is what the scheduler compares with the clock; when a person entered a wall-clock time, `localTime` keeps it and `at` is recomputed from it and `timeZone` before the release runs, never earlier than the wall-clock time.","additionalProperties":false,"required":["kind","at","timeZone"],"properties":{"kind":{"type":"string","enum":["at"]},"at":{"$ref":"#/$defs/Timestamp"},"timeZone":{"$ref":"#/$defs/TimeZone"},"localTime":{"$ref":"#/$defs/LocalDateTime"}}},{"type":"object","title":"ReleaseOnApproval","description":"As soon as it is approved (a gate-only change such as setting an expiry on live content, or \"publish when approved\").","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["on-approval"]}}}]},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"},"TimeZone":{"type":"string","description":"IANA time zone name from the tz database (RFC-0008), e.g. Europe/Istanbul, America/Argentina/Buenos_Aires, Etc/GMT+3 or UTC. Wall-clock times a person enters are kept with their zone, so the instant is recomputed when governments change the rules. The pattern bounds the shape only; writers also check the name against the runtime's Intl zone list.","pattern":"^(?:UTC|[A-Z][A-Za-z_-]+(?:/[A-Za-z0-9_+-]+){1,2})$","maxLength":64}}}; -const schema465 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; -const schema466 = {"type":"string","description":"A release id: `rel_` and 6 to 32 letters or digits.","pattern":"^rel_[A-Za-z0-9]{6,32}$"}; -const schema467 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; -const schema479 = {"type":"string","description":"Lifecycle. `draft`: proposed or being assembled, nothing locked. `awaiting_approval`: waiting for a person; its nodes are locked. `scheduled`: approved (or allowed by `auto` mode) and locked until it runs. `running`: claimed by the scheduler. `published`: applied; `outcome` names the snapshot. `conflict`: a pinned node or its live base changed; held, nothing published. `missed`: the scheduler reached it more than an hour late; nothing published. `expired`: no valid approval at its time. `failed`: refused at run time (e.g. a dependency is missing). `canceled`: withdrawn.","enum":["draft","awaiting_approval","scheduled","running","published","conflict","missed","expired","failed","canceled"]}; -const schema480 = {"type":"string","description":"`rl1:` and the lower-case hex SHA-256 of the release's canonical item lines (site id, then one line per item sorted by node id: kind, node id, target and base versions). The prefix keeps it apart from the graph state hash and from comment bindings (`cm1:`); approvals store it as their state.","pattern":"^rl1:[0-9a-f]{64}$"}; -const schema476 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; -const schema472 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; -const schema508 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; -const pattern246 = new RegExp("^rel_[A-Za-z0-9]{6,32}$", "u"); -const pattern248 = new RegExp("^[^\\x00-\\x1F\\x7F\\u202A-\\u202E\\u2066-\\u2069]+$", "u"); -const pattern254 = new RegExp("^rl1:[0-9a-f]{64}$", "u"); -const schema468 = {"description":"One pinned change, discriminated by `kind`.","oneOf":[{"type":"object","title":"ReleaseSetItem","description":"Make `target` the live version of the node. `base` is the version that was live when the item was pinned; absent when the node was not live (a new entry or page).","additionalProperties":false,"required":["kind","nodeId","nodeKind","target"],"properties":{"kind":{"type":"string","enum":["set"]},"nodeId":{"$ref":"#/$defs/Id"},"nodeKind":{"$ref":"#/$defs/ReleaseNodeKind"},"target":{"$ref":"#/$defs/ReleaseNodeVersion"},"base":{"$ref":"#/$defs/ReleaseNodeVersion"}}},{"type":"object","title":"ReleaseRemoveItem","description":"Take the node out of the live snapshot. `base` is the live version that is removed.","additionalProperties":false,"required":["kind","nodeId","nodeKind","base"],"properties":{"kind":{"type":"string","enum":["remove"]},"nodeId":{"$ref":"#/$defs/Id"},"nodeKind":{"$ref":"#/$defs/ReleaseNodeKind"},"base":{"$ref":"#/$defs/ReleaseNodeVersion"}}}]}; -const schema470 = {"type":"string","description":"Kinds of graph node a release may carry. The site node is never one of them.","enum":["page","contentEntry","taxonomy","term","redirect","menu","asset"]}; -const schema471 = {"type":"object","description":"One version of a node: its revision and the SHA-256 of its canonical JSON, whose body the blob store keeps under `nodes/.json`.","additionalProperties":false,"required":["rev","hash"],"properties":{"rev":{"$ref":"#/$defs/Revision"},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$"}}}; +export const validateRelease = validate211; +const schema481 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/release.schema.json","title":"Release","description":"A scheduled content change set (RFC-0008): node versions pinned by id, rev and content hash, applied on top of whatever is live at the release's time. A single scheduled publish is a one-item release. Each item records the version that was live when the release was pinned (`base`); if a pinned node or its live base changed before the release runs, the release is held as a conflict instead of overwriting anything. A person's approval is bound to `hash` (`rl1:`), which digests the items only, so a person may move an approved release without re-approving it while an agent's move or any content change voids the approval. Releases live in D1 next to the graph; they are not graph nodes, never enter a snapshot and are not part of site bundles.","type":"object","additionalProperties":false,"required":["schemaVersion","id","siteId","items","state","hash","createdBy","createdAt","updatedAt","rev"],"properties":{"schemaVersion":{"$ref":"#/$defs/SchemaVersion"},"id":{"$ref":"#/$defs/ReleaseId"},"siteId":{"$ref":"#/$defs/Id"},"title":{"type":"string","description":"Optional name shown in Studio (\"Election night\"). A one-item release without a title is shown under its entry's or page's title.","minLength":1,"maxLength":200,"pattern":"^[^\\x00-\\x1F\\x7F\\u202A-\\u202E\\u2066-\\u2069]+$"},"items":{"type":"array","description":"The pinned changes, at most one per node. A release never contains the site node: site settings, domains and structure are published the ordinary way.","items":{"$ref":"#/$defs/ReleaseItem"},"minItems":1,"maxItems":100},"when":{"$ref":"#/$defs/ReleaseWhen"},"state":{"$ref":"#/$defs/ReleaseState"},"hash":{"$ref":"#/$defs/ReleaseHash"},"approval":{"$ref":"#/$defs/ReleaseApproval"},"outcome":{"$ref":"#/$defs/ReleaseOutcome"},"createdBy":{"$ref":"#/$defs/Actor"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"rev":{"$ref":"#/$defs/Revision"},"metadata":{"$ref":"#/$defs/Metadata"}},"$defs":{"Actor":{"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}},"ActorKind":{"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]},"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"LocalDateTime":{"type":"string","description":"A wall-clock date and time without an offset, to the minute (YYYY-MM-DDTHH:mm), as a person entered it; always paired with a TimeZone (RFC-0008).","pattern":"^[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])T(?:[01][0-9]|2[0-3]):[0-5][0-9]$"},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"ReleaseApproval":{"description":"Why the release may run, discriminated by `kind`.","oneOf":[{"type":"object","title":"ReleasePersonApproval","description":"A person approved these items (bound to `hash`) for `executeAt`, either by deciding an agent's request (`approvalId`) or by scheduling it themselves. The person's rights are checked again when the release runs.","additionalProperties":false,"required":["kind","by","at"],"properties":{"kind":{"type":"string","enum":["person"]},"by":{"$ref":"#/$defs/ReleasePerson"},"at":{"$ref":"#/$defs/Timestamp"},"executeAt":{"$ref":"#/$defs/Timestamp"},"approvalId":{"$ref":"#/$defs/Id"}}},{"type":"object","title":"ReleaseAutoApproval","description":"An agent key in `auto` mode scheduled a low-risk release without a person (ADR-0015). Valid only while the release runs before `windowEnds`: the end of the key's elevation, or 24 hours after scheduling, whichever comes first.","additionalProperties":false,"required":["kind","by","keyId","at","windowEnds"],"properties":{"kind":{"type":"string","enum":["auto"]},"by":{"$ref":"#/$defs/Actor"},"keyId":{"type":"string","pattern":"^[A-Za-z0-9_-]{6,64}$"},"at":{"$ref":"#/$defs/Timestamp"},"windowEnds":{"$ref":"#/$defs/Timestamp"}}}]},"ReleaseConflict":{"type":"object","description":"A node whose live version was not the pinned base when the release ran. `expected` absent: the node was not expected to be live; `found` absent: it was not live.","additionalProperties":false,"required":["nodeId"],"properties":{"nodeId":{"$ref":"#/$defs/Id"},"expected":{"$ref":"#/$defs/ReleaseNodeVersion"},"found":{"$ref":"#/$defs/ReleaseNodeVersion"}}},"ReleaseHash":{"type":"string","description":"`rl1:` and the lower-case hex SHA-256 of the release's canonical item lines (site id, then one line per item sorted by node id: kind, node id, target and base versions). The prefix keeps it apart from the graph state hash and from comment bindings (`cm1:`); approvals store it as their state.","pattern":"^rl1:[0-9a-f]{64}$"},"ReleaseId":{"type":"string","description":"A release id: `rel_` and 6 to 32 letters or digits.","pattern":"^rel_[A-Za-z0-9]{6,32}$"},"ReleaseItem":{"description":"One pinned change, discriminated by `kind`.","oneOf":[{"type":"object","title":"ReleaseSetItem","description":"Make `target` the live version of the node. `base` is the version that was live when the item was pinned; absent when the node was not live (a new entry or page).","additionalProperties":false,"required":["kind","nodeId","nodeKind","target"],"properties":{"kind":{"type":"string","enum":["set"]},"nodeId":{"$ref":"#/$defs/Id"},"nodeKind":{"$ref":"#/$defs/ReleaseNodeKind"},"target":{"$ref":"#/$defs/ReleaseNodeVersion"},"base":{"$ref":"#/$defs/ReleaseNodeVersion"}}},{"type":"object","title":"ReleaseRemoveItem","description":"Take the node out of the live snapshot. `base` is the live version that is removed.","additionalProperties":false,"required":["kind","nodeId","nodeKind","base"],"properties":{"kind":{"type":"string","enum":["remove"]},"nodeId":{"$ref":"#/$defs/Id"},"nodeKind":{"$ref":"#/$defs/ReleaseNodeKind"},"base":{"$ref":"#/$defs/ReleaseNodeVersion"}}}]},"ReleaseNodeKind":{"type":"string","description":"Kinds of graph node a release may carry. The site node is never one of them.","enum":["page","contentEntry","taxonomy","term","redirect","menu","asset"]},"ReleaseNodeVersion":{"type":"object","description":"One version of a node: its revision and the SHA-256 of its canonical JSON, whose body the blob store keeps under `nodes/.json`.","additionalProperties":false,"required":["rev","hash"],"properties":{"rev":{"$ref":"#/$defs/Revision"},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$"}}},"ReleaseOutcome":{"description":"What happened when the release was due, discriminated by `kind` (it matches the terminal `state`).","oneOf":[{"type":"object","title":"ReleasePublishedOutcome","description":"Applied: `snapshotId` is the new live snapshot (reason `scheduled`), composed from `baseSnapshotId` and the items. `purge` says how the edge cache followed.","additionalProperties":false,"required":["kind","runId","at","snapshotId","baseSnapshotId","latenessSeconds","purge"],"properties":{"kind":{"type":"string","enum":["published"]},"runId":{"$ref":"#/$defs/Id"},"at":{"$ref":"#/$defs/Timestamp"},"snapshotId":{"$ref":"#/$defs/Id"},"baseSnapshotId":{"$ref":"#/$defs/Id"},"latenessSeconds":{"type":"integer","minimum":0},"purge":{"type":"string","enum":["tags","everything","deferred","failed","none"]}}},{"type":"object","title":"ReleaseConflictOutcome","description":"Held: these nodes changed since the release was pinned. Nothing was published.","additionalProperties":false,"required":["kind","at","conflicts"],"properties":{"kind":{"type":"string","enum":["conflict"]},"runId":{"$ref":"#/$defs/Id"},"at":{"$ref":"#/$defs/Timestamp"},"conflicts":{"type":"array","items":{"$ref":"#/$defs/ReleaseConflict"},"minItems":1,"maxItems":100}}},{"type":"object","title":"ReleaseFailedOutcome","description":"Refused at run time; nothing was published.","additionalProperties":false,"required":["kind","at","code"],"properties":{"kind":{"type":"string","enum":["failed"]},"runId":{"$ref":"#/$defs/Id"},"at":{"$ref":"#/$defs/Timestamp"},"code":{"type":"string","enum":["dependency_missing","invalid_snapshot","site_missing","node_body_missing","store_error"]},"message":{"type":"string","maxLength":500,"pattern":"^[^\\x00-\\x1F\\x7F\\u202A-\\u202E\\u2066-\\u2069]*$"}}},{"type":"object","title":"ReleaseMissedOutcome","description":"The scheduler reached the release more than an hour after its time; nothing was published. A person may publish it now.","additionalProperties":false,"required":["kind","at","latenessSeconds"],"properties":{"kind":{"type":"string","enum":["missed"]},"at":{"$ref":"#/$defs/Timestamp"},"latenessSeconds":{"type":"integer","minimum":0}}},{"type":"object","title":"ReleaseExpiredOutcome","description":"No valid approval when the release was due: never approved, the approver can no longer publish it, the agent's `auto` window ended, or its creator left.","additionalProperties":false,"required":["kind","at","reason"],"properties":{"kind":{"type":"string","enum":["expired"]},"at":{"$ref":"#/$defs/Timestamp"},"reason":{"type":"string","enum":["unapproved","approval_lost","auto_window_ended","creator_removed"]}}},{"type":"object","title":"ReleaseCanceledOutcome","description":"Withdrawn by a person or an agent; its nodes are unlocked.","additionalProperties":false,"required":["kind","at","by"],"properties":{"kind":{"type":"string","enum":["canceled"]},"at":{"$ref":"#/$defs/Timestamp"},"by":{"$ref":"#/$defs/Actor"}}}]},"ReleasePerson":{"type":"object","description":"A staff person (`user_…`). Agents never appear here.","additionalProperties":false,"required":["id"],"properties":{"id":{"type":"string","pattern":"^user_[A-Za-z0-9]{6,32}$"},"name":{"type":"string","maxLength":200}}},"ReleaseState":{"type":"string","description":"Lifecycle. `draft`: proposed or being assembled, nothing locked. `awaiting_approval`: waiting for a person; its nodes are locked. `scheduled`: approved (or allowed by `auto` mode) and locked until it runs. `running`: claimed by the scheduler. `published`: applied; `outcome` names the snapshot. `conflict`: a pinned node or its live base changed; held, nothing published. `missed`: the scheduler reached it more than an hour late; nothing published. `expired`: no valid approval at its time. `failed`: refused at run time (e.g. a dependency is missing). `canceled`: withdrawn.","enum":["draft","awaiting_approval","scheduled","running","published","conflict","missed","expired","failed","canceled"]},"ReleaseWhen":{"description":"When the release runs, discriminated by `kind`. Absent while a draft has no time yet.","oneOf":[{"type":"object","title":"ReleaseAt","description":"At an instant. `at` is what the scheduler compares with the clock; when a person entered a wall-clock time, `localTime` keeps it and `at` is recomputed from it and `timeZone` before the release runs, never earlier than the wall-clock time.","additionalProperties":false,"required":["kind","at","timeZone"],"properties":{"kind":{"type":"string","enum":["at"]},"at":{"$ref":"#/$defs/Timestamp"},"timeZone":{"$ref":"#/$defs/TimeZone"},"localTime":{"$ref":"#/$defs/LocalDateTime"}}},{"type":"object","title":"ReleaseOnApproval","description":"As soon as it is approved (a gate-only change such as setting an expiry on live content, or \"publish when approved\").","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["on-approval"]}}}]},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"},"TimeZone":{"type":"string","description":"IANA time zone name from the tz database (RFC-0008), e.g. Europe/Istanbul, America/Argentina/Buenos_Aires, Etc/GMT+3 or UTC. Wall-clock times a person enters are kept with their zone, so the instant is recomputed when governments change the rules. The pattern bounds the shape only; writers also check the name against the runtime's Intl zone list.","pattern":"^(?:UTC|[A-Z][A-Za-z_-]+(?:/[A-Za-z0-9_+-]+){1,2})$","maxLength":64}}}; +const schema482 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; +const schema483 = {"type":"string","description":"A release id: `rel_` and 6 to 32 letters or digits.","pattern":"^rel_[A-Za-z0-9]{6,32}$"}; +const schema484 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; +const schema496 = {"type":"string","description":"Lifecycle. `draft`: proposed or being assembled, nothing locked. `awaiting_approval`: waiting for a person; its nodes are locked. `scheduled`: approved (or allowed by `auto` mode) and locked until it runs. `running`: claimed by the scheduler. `published`: applied; `outcome` names the snapshot. `conflict`: a pinned node or its live base changed; held, nothing published. `missed`: the scheduler reached it more than an hour late; nothing published. `expired`: no valid approval at its time. `failed`: refused at run time (e.g. a dependency is missing). `canceled`: withdrawn.","enum":["draft","awaiting_approval","scheduled","running","published","conflict","missed","expired","failed","canceled"]}; +const schema497 = {"type":"string","description":"`rl1:` and the lower-case hex SHA-256 of the release's canonical item lines (site id, then one line per item sorted by node id: kind, node id, target and base versions). The prefix keeps it apart from the graph state hash and from comment bindings (`cm1:`); approvals store it as their state.","pattern":"^rl1:[0-9a-f]{64}$"}; +const schema493 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema489 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema525 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const pattern262 = new RegExp("^rel_[A-Za-z0-9]{6,32}$", "u"); +const pattern264 = new RegExp("^[^\\x00-\\x1F\\x7F\\u202A-\\u202E\\u2066-\\u2069]+$", "u"); +const pattern270 = new RegExp("^rl1:[0-9a-f]{64}$", "u"); +const schema485 = {"description":"One pinned change, discriminated by `kind`.","oneOf":[{"type":"object","title":"ReleaseSetItem","description":"Make `target` the live version of the node. `base` is the version that was live when the item was pinned; absent when the node was not live (a new entry or page).","additionalProperties":false,"required":["kind","nodeId","nodeKind","target"],"properties":{"kind":{"type":"string","enum":["set"]},"nodeId":{"$ref":"#/$defs/Id"},"nodeKind":{"$ref":"#/$defs/ReleaseNodeKind"},"target":{"$ref":"#/$defs/ReleaseNodeVersion"},"base":{"$ref":"#/$defs/ReleaseNodeVersion"}}},{"type":"object","title":"ReleaseRemoveItem","description":"Take the node out of the live snapshot. `base` is the live version that is removed.","additionalProperties":false,"required":["kind","nodeId","nodeKind","base"],"properties":{"kind":{"type":"string","enum":["remove"]},"nodeId":{"$ref":"#/$defs/Id"},"nodeKind":{"$ref":"#/$defs/ReleaseNodeKind"},"base":{"$ref":"#/$defs/ReleaseNodeVersion"}}}]}; +const schema487 = {"type":"string","description":"Kinds of graph node a release may carry. The site node is never one of them.","enum":["page","contentEntry","taxonomy","term","redirect","menu","asset"]}; +const schema488 = {"type":"object","description":"One version of a node: its revision and the SHA-256 of its canonical JSON, whose body the blob store keeps under `nodes/.json`.","additionalProperties":false,"required":["rev","hash"],"properties":{"rev":{"$ref":"#/$defs/Revision"},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$"}}}; -function validate197(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate213(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate197.evaluated; +const evaluated0 = validate213.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -34446,7 +36813,7 @@ errors++; if(data.hash !== undefined){ let data1 = data.hash; if(typeof data1 === "string"){ -if(!pattern50.test(data1)){ +if(!pattern66.test(data1)){ const err5 = {instancePath:instancePath+"/hash",schemaPath:"#/properties/hash/pattern",keyword:"pattern",params:{pattern: "^[0-9a-f]{64}$"},message:"must match pattern \""+"^[0-9a-f]{64}$"+"\""}; if(vErrors === null){ vErrors = [err5]; @@ -34479,16 +36846,16 @@ vErrors.push(err7); } errors++; } -validate197.errors = vErrors; +validate213.errors = vErrors; return errors === 0; } -validate197.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate213.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate196(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate212(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate196.evaluated; +const evaluated0 = validate212.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -34565,7 +36932,7 @@ vErrors.push(err5); errors++; } if(!(data0 === "set")){ -const err6 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema468.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err6 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema485.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err6]; } @@ -34633,7 +37000,7 @@ vErrors.push(err11); errors++; } if(!(((((((data2 === "page") || (data2 === "contentEntry")) || (data2 === "taxonomy")) || (data2 === "term")) || (data2 === "redirect")) || (data2 === "menu")) || (data2 === "asset"))){ -const err12 = {instancePath:instancePath+"/nodeKind",schemaPath:"#/$defs/ReleaseNodeKind/enum",keyword:"enum",params:{allowedValues: schema470.enum},message:"must be equal to one of the allowed values"}; +const err12 = {instancePath:instancePath+"/nodeKind",schemaPath:"#/$defs/ReleaseNodeKind/enum",keyword:"enum",params:{allowedValues: schema487.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err12]; } @@ -34644,14 +37011,14 @@ errors++; } } if(data.target !== undefined){ -if(!(validate197(data.target, {instancePath:instancePath+"/target",parentData:data,parentDataProperty:"target",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate197.errors : vErrors.concat(validate197.errors); +if(!(validate213(data.target, {instancePath:instancePath+"/target",parentData:data,parentDataProperty:"target",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate213.errors : vErrors.concat(validate213.errors); errors = vErrors.length; } } if(data.base !== undefined){ -if(!(validate197(data.base, {instancePath:instancePath+"/base",parentData:data,parentDataProperty:"base",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate197.errors : vErrors.concat(validate197.errors); +if(!(validate213(data.base, {instancePath:instancePath+"/base",parentData:data,parentDataProperty:"base",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate213.errors : vErrors.concat(validate213.errors); errors = vErrors.length; } } @@ -34739,7 +37106,7 @@ vErrors.push(err19); errors++; } if(!(data5 === "remove")){ -const err20 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema468.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err20 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema485.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err20]; } @@ -34807,7 +37174,7 @@ vErrors.push(err25); errors++; } if(!(((((((data7 === "page") || (data7 === "contentEntry")) || (data7 === "taxonomy")) || (data7 === "term")) || (data7 === "redirect")) || (data7 === "menu")) || (data7 === "asset"))){ -const err26 = {instancePath:instancePath+"/nodeKind",schemaPath:"#/$defs/ReleaseNodeKind/enum",keyword:"enum",params:{allowedValues: schema470.enum},message:"must be equal to one of the allowed values"}; +const err26 = {instancePath:instancePath+"/nodeKind",schemaPath:"#/$defs/ReleaseNodeKind/enum",keyword:"enum",params:{allowedValues: schema487.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err26]; } @@ -34818,8 +37185,8 @@ errors++; } } if(data.base !== undefined){ -if(!(validate197(data.base, {instancePath:instancePath+"/base",parentData:data,parentDataProperty:"base",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate197.errors : vErrors.concat(validate197.errors); +if(!(validate213(data.base, {instancePath:instancePath+"/base",parentData:data,parentDataProperty:"base",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate213.errors : vErrors.concat(validate213.errors); errors = vErrors.length; } } @@ -34869,21 +37236,21 @@ vErrors = null; } } } -validate196.errors = vErrors; +validate212.errors = vErrors; evaluated0.props = props0; return errors === 0; } -validate196.evaluated = {"dynamicProps":true,"dynamicItems":false}; +validate212.evaluated = {"dynamicProps":true,"dynamicItems":false}; -const schema475 = {"description":"When the release runs, discriminated by `kind`. Absent while a draft has no time yet.","oneOf":[{"type":"object","title":"ReleaseAt","description":"At an instant. `at` is what the scheduler compares with the clock; when a person entered a wall-clock time, `localTime` keeps it and `at` is recomputed from it and `timeZone` before the release runs, never earlier than the wall-clock time.","additionalProperties":false,"required":["kind","at","timeZone"],"properties":{"kind":{"type":"string","enum":["at"]},"at":{"$ref":"#/$defs/Timestamp"},"timeZone":{"$ref":"#/$defs/TimeZone"},"localTime":{"$ref":"#/$defs/LocalDateTime"}}},{"type":"object","title":"ReleaseOnApproval","description":"As soon as it is approved (a gate-only change such as setting an expiry on live content, or \"publish when approved\").","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["on-approval"]}}}]}; -const schema477 = {"type":"string","description":"IANA time zone name from the tz database (RFC-0008), e.g. Europe/Istanbul, America/Argentina/Buenos_Aires, Etc/GMT+3 or UTC. Wall-clock times a person enters are kept with their zone, so the instant is recomputed when governments change the rules. The pattern bounds the shape only; writers also check the name against the runtime's Intl zone list.","pattern":"^(?:UTC|[A-Z][A-Za-z_-]+(?:/[A-Za-z0-9_+-]+){1,2})$","maxLength":64}; -const schema478 = {"type":"string","description":"A wall-clock date and time without an offset, to the minute (YYYY-MM-DDTHH:mm), as a person entered it; always paired with a TimeZone (RFC-0008).","pattern":"^[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])T(?:[01][0-9]|2[0-3]):[0-5][0-9]$"}; -const pattern253 = new RegExp("^[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])T(?:[01][0-9]|2[0-3]):[0-5][0-9]$", "u"); +const schema492 = {"description":"When the release runs, discriminated by `kind`. Absent while a draft has no time yet.","oneOf":[{"type":"object","title":"ReleaseAt","description":"At an instant. `at` is what the scheduler compares with the clock; when a person entered a wall-clock time, `localTime` keeps it and `at` is recomputed from it and `timeZone` before the release runs, never earlier than the wall-clock time.","additionalProperties":false,"required":["kind","at","timeZone"],"properties":{"kind":{"type":"string","enum":["at"]},"at":{"$ref":"#/$defs/Timestamp"},"timeZone":{"$ref":"#/$defs/TimeZone"},"localTime":{"$ref":"#/$defs/LocalDateTime"}}},{"type":"object","title":"ReleaseOnApproval","description":"As soon as it is approved (a gate-only change such as setting an expiry on live content, or \"publish when approved\").","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["on-approval"]}}}]}; +const schema494 = {"type":"string","description":"IANA time zone name from the tz database (RFC-0008), e.g. Europe/Istanbul, America/Argentina/Buenos_Aires, Etc/GMT+3 or UTC. Wall-clock times a person enters are kept with their zone, so the instant is recomputed when governments change the rules. The pattern bounds the shape only; writers also check the name against the runtime's Intl zone list.","pattern":"^(?:UTC|[A-Z][A-Za-z_-]+(?:/[A-Za-z0-9_+-]+){1,2})$","maxLength":64}; +const schema495 = {"type":"string","description":"A wall-clock date and time without an offset, to the minute (YYYY-MM-DDTHH:mm), as a person entered it; always paired with a TimeZone (RFC-0008).","pattern":"^[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])T(?:[01][0-9]|2[0-3]):[0-5][0-9]$"}; +const pattern269 = new RegExp("^[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])T(?:[01][0-9]|2[0-3]):[0-5][0-9]$", "u"); -function validate202(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate218(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate202.evaluated; +const evaluated0 = validate218.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -34950,7 +37317,7 @@ vErrors.push(err4); errors++; } if(!(data0 === "at")){ -const err5 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema475.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err5 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema492.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err5]; } @@ -34998,7 +37365,7 @@ vErrors.push(err8); } errors++; } -if(!pattern66.test(data2)){ +if(!pattern82.test(data2)){ const err9 = {instancePath:instancePath+"/timeZone",schemaPath:"#/$defs/TimeZone/pattern",keyword:"pattern",params:{pattern: "^(?:UTC|[A-Z][A-Za-z_-]+(?:/[A-Za-z0-9_+-]+){1,2})$"},message:"must match pattern \""+"^(?:UTC|[A-Z][A-Za-z_-]+(?:/[A-Za-z0-9_+-]+){1,2})$"+"\""}; if(vErrors === null){ vErrors = [err9]; @@ -35023,7 +37390,7 @@ errors++; if(data.localTime !== undefined){ let data3 = data.localTime; if(typeof data3 === "string"){ -if(!pattern253.test(data3)){ +if(!pattern269.test(data3)){ const err11 = {instancePath:instancePath+"/localTime",schemaPath:"#/$defs/LocalDateTime/pattern",keyword:"pattern",params:{pattern: "^[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])T(?:[01][0-9]|2[0-3]):[0-5][0-9]$"},message:"must match pattern \""+"^[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])T(?:[01][0-9]|2[0-3]):[0-5][0-9]$"+"\""}; if(vErrors === null){ vErrors = [err11]; @@ -35099,7 +37466,7 @@ vErrors.push(err16); errors++; } if(!(data4 === "on-approval")){ -const err17 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema475.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err17 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema492.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err17]; } @@ -35155,22 +37522,22 @@ vErrors = null; } } } -validate202.errors = vErrors; +validate218.errors = vErrors; evaluated0.props = props0; return errors === 0; } -validate202.evaluated = {"dynamicProps":true,"dynamicItems":false}; +validate218.evaluated = {"dynamicProps":true,"dynamicItems":false}; -const schema481 = {"description":"Why the release may run, discriminated by `kind`.","oneOf":[{"type":"object","title":"ReleasePersonApproval","description":"A person approved these items (bound to `hash`) for `executeAt`, either by deciding an agent's request (`approvalId`) or by scheduling it themselves. The person's rights are checked again when the release runs.","additionalProperties":false,"required":["kind","by","at"],"properties":{"kind":{"type":"string","enum":["person"]},"by":{"$ref":"#/$defs/ReleasePerson"},"at":{"$ref":"#/$defs/Timestamp"},"executeAt":{"$ref":"#/$defs/Timestamp"},"approvalId":{"$ref":"#/$defs/Id"}}},{"type":"object","title":"ReleaseAutoApproval","description":"An agent key in `auto` mode scheduled a low-risk release without a person (ADR-0015). Valid only while the release runs before `windowEnds`: the end of the key's elevation, or 24 hours after scheduling, whichever comes first.","additionalProperties":false,"required":["kind","by","keyId","at","windowEnds"],"properties":{"kind":{"type":"string","enum":["auto"]},"by":{"$ref":"#/$defs/Actor"},"keyId":{"type":"string","pattern":"^[A-Za-z0-9_-]{6,64}$"},"at":{"$ref":"#/$defs/Timestamp"},"windowEnds":{"$ref":"#/$defs/Timestamp"}}}]}; -const schema482 = {"type":"object","description":"A staff person (`user_…`). Agents never appear here.","additionalProperties":false,"required":["id"],"properties":{"id":{"type":"string","pattern":"^user_[A-Za-z0-9]{6,32}$"},"name":{"type":"string","maxLength":200}}}; -const pattern258 = new RegExp("^[A-Za-z0-9_-]{6,64}$", "u"); -const schema486 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; -const schema487 = {"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]}; +const schema498 = {"description":"Why the release may run, discriminated by `kind`.","oneOf":[{"type":"object","title":"ReleasePersonApproval","description":"A person approved these items (bound to `hash`) for `executeAt`, either by deciding an agent's request (`approvalId`) or by scheduling it themselves. The person's rights are checked again when the release runs.","additionalProperties":false,"required":["kind","by","at"],"properties":{"kind":{"type":"string","enum":["person"]},"by":{"$ref":"#/$defs/ReleasePerson"},"at":{"$ref":"#/$defs/Timestamp"},"executeAt":{"$ref":"#/$defs/Timestamp"},"approvalId":{"$ref":"#/$defs/Id"}}},{"type":"object","title":"ReleaseAutoApproval","description":"An agent key in `auto` mode scheduled a low-risk release without a person (ADR-0015). Valid only while the release runs before `windowEnds`: the end of the key's elevation, or 24 hours after scheduling, whichever comes first.","additionalProperties":false,"required":["kind","by","keyId","at","windowEnds"],"properties":{"kind":{"type":"string","enum":["auto"]},"by":{"$ref":"#/$defs/Actor"},"keyId":{"type":"string","pattern":"^[A-Za-z0-9_-]{6,64}$"},"at":{"$ref":"#/$defs/Timestamp"},"windowEnds":{"$ref":"#/$defs/Timestamp"}}}]}; +const schema499 = {"type":"object","description":"A staff person (`user_…`). Agents never appear here.","additionalProperties":false,"required":["id"],"properties":{"id":{"type":"string","pattern":"^user_[A-Za-z0-9]{6,32}$"},"name":{"type":"string","maxLength":200}}}; +const pattern274 = new RegExp("^[A-Za-z0-9_-]{6,64}$", "u"); +const schema503 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; +const schema504 = {"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]}; -function validate205(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate221(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate205.evaluated; +const evaluated0 = validate221.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -35223,7 +37590,7 @@ vErrors.push(err3); errors++; } if(!(((data0 === "human") || (data0 === "agent")) || (data0 === "system"))){ -const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema487.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema504.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -35314,16 +37681,16 @@ vErrors.push(err11); } errors++; } -validate205.errors = vErrors; +validate221.errors = vErrors; return errors === 0; } -validate205.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate221.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate204(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate220(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate204.evaluated; +const evaluated0 = validate220.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -35390,7 +37757,7 @@ vErrors.push(err4); errors++; } if(!(data0 === "person")){ -const err5 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema481.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err5 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema498.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err5]; } @@ -35428,7 +37795,7 @@ errors++; if(data1.id !== undefined){ let data2 = data1.id; if(typeof data2 === "string"){ -if(!pattern232.test(data2)){ +if(!pattern248.test(data2)){ const err8 = {instancePath:instancePath+"/by/id",schemaPath:"#/$defs/ReleasePerson/properties/id/pattern",keyword:"pattern",params:{pattern: "^user_[A-Za-z0-9]{6,32}$"},message:"must match pattern \""+"^user_[A-Za-z0-9]{6,32}$"+"\""}; if(vErrors === null){ vErrors = [err8]; @@ -35676,7 +38043,7 @@ vErrors.push(err28); errors++; } if(!(data7 === "auto")){ -const err29 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema481.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err29 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema498.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err29]; } @@ -35687,15 +38054,15 @@ errors++; } } if(data.by !== undefined){ -if(!(validate205(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate205.errors : vErrors.concat(validate205.errors); +if(!(validate221(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate221.errors : vErrors.concat(validate221.errors); errors = vErrors.length; } } if(data.keyId !== undefined){ let data9 = data.keyId; if(typeof data9 === "string"){ -if(!pattern258.test(data9)){ +if(!pattern274.test(data9)){ const err30 = {instancePath:instancePath+"/keyId",schemaPath:"#/oneOf/1/properties/keyId/pattern",keyword:"pattern",params:{pattern: "^[A-Za-z0-9_-]{6,64}$"},message:"must match pattern \""+"^[A-Za-z0-9_-]{6,64}$"+"\""}; if(vErrors === null){ vErrors = [err30]; @@ -35813,20 +38180,20 @@ vErrors = null; } } } -validate204.errors = vErrors; +validate220.errors = vErrors; evaluated0.props = props0; return errors === 0; } -validate204.evaluated = {"dynamicProps":true,"dynamicItems":false}; +validate220.evaluated = {"dynamicProps":true,"dynamicItems":false}; -const schema491 = {"description":"What happened when the release was due, discriminated by `kind` (it matches the terminal `state`).","oneOf":[{"type":"object","title":"ReleasePublishedOutcome","description":"Applied: `snapshotId` is the new live snapshot (reason `scheduled`), composed from `baseSnapshotId` and the items. `purge` says how the edge cache followed.","additionalProperties":false,"required":["kind","runId","at","snapshotId","baseSnapshotId","latenessSeconds","purge"],"properties":{"kind":{"type":"string","enum":["published"]},"runId":{"$ref":"#/$defs/Id"},"at":{"$ref":"#/$defs/Timestamp"},"snapshotId":{"$ref":"#/$defs/Id"},"baseSnapshotId":{"$ref":"#/$defs/Id"},"latenessSeconds":{"type":"integer","minimum":0},"purge":{"type":"string","enum":["tags","everything","deferred","failed","none"]}}},{"type":"object","title":"ReleaseConflictOutcome","description":"Held: these nodes changed since the release was pinned. Nothing was published.","additionalProperties":false,"required":["kind","at","conflicts"],"properties":{"kind":{"type":"string","enum":["conflict"]},"runId":{"$ref":"#/$defs/Id"},"at":{"$ref":"#/$defs/Timestamp"},"conflicts":{"type":"array","items":{"$ref":"#/$defs/ReleaseConflict"},"minItems":1,"maxItems":100}}},{"type":"object","title":"ReleaseFailedOutcome","description":"Refused at run time; nothing was published.","additionalProperties":false,"required":["kind","at","code"],"properties":{"kind":{"type":"string","enum":["failed"]},"runId":{"$ref":"#/$defs/Id"},"at":{"$ref":"#/$defs/Timestamp"},"code":{"type":"string","enum":["dependency_missing","invalid_snapshot","site_missing","node_body_missing","store_error"]},"message":{"type":"string","maxLength":500,"pattern":"^[^\\x00-\\x1F\\x7F\\u202A-\\u202E\\u2066-\\u2069]*$"}}},{"type":"object","title":"ReleaseMissedOutcome","description":"The scheduler reached the release more than an hour after its time; nothing was published. A person may publish it now.","additionalProperties":false,"required":["kind","at","latenessSeconds"],"properties":{"kind":{"type":"string","enum":["missed"]},"at":{"$ref":"#/$defs/Timestamp"},"latenessSeconds":{"type":"integer","minimum":0}}},{"type":"object","title":"ReleaseExpiredOutcome","description":"No valid approval when the release was due: never approved, the approver can no longer publish it, the agent's `auto` window ended, or its creator left.","additionalProperties":false,"required":["kind","at","reason"],"properties":{"kind":{"type":"string","enum":["expired"]},"at":{"$ref":"#/$defs/Timestamp"},"reason":{"type":"string","enum":["unapproved","approval_lost","auto_window_ended","creator_removed"]}}},{"type":"object","title":"ReleaseCanceledOutcome","description":"Withdrawn by a person or an agent; its nodes are unlocked.","additionalProperties":false,"required":["kind","at","by"],"properties":{"kind":{"type":"string","enum":["canceled"]},"at":{"$ref":"#/$defs/Timestamp"},"by":{"$ref":"#/$defs/Actor"}}}]}; -const pattern265 = new RegExp("^[^\\x00-\\x1F\\x7F\\u202A-\\u202E\\u2066-\\u2069]*$", "u"); -const schema498 = {"type":"object","description":"A node whose live version was not the pinned base when the release ran. `expected` absent: the node was not expected to be live; `found` absent: it was not live.","additionalProperties":false,"required":["nodeId"],"properties":{"nodeId":{"$ref":"#/$defs/Id"},"expected":{"$ref":"#/$defs/ReleaseNodeVersion"},"found":{"$ref":"#/$defs/ReleaseNodeVersion"}}}; +const schema508 = {"description":"What happened when the release was due, discriminated by `kind` (it matches the terminal `state`).","oneOf":[{"type":"object","title":"ReleasePublishedOutcome","description":"Applied: `snapshotId` is the new live snapshot (reason `scheduled`), composed from `baseSnapshotId` and the items. `purge` says how the edge cache followed.","additionalProperties":false,"required":["kind","runId","at","snapshotId","baseSnapshotId","latenessSeconds","purge"],"properties":{"kind":{"type":"string","enum":["published"]},"runId":{"$ref":"#/$defs/Id"},"at":{"$ref":"#/$defs/Timestamp"},"snapshotId":{"$ref":"#/$defs/Id"},"baseSnapshotId":{"$ref":"#/$defs/Id"},"latenessSeconds":{"type":"integer","minimum":0},"purge":{"type":"string","enum":["tags","everything","deferred","failed","none"]}}},{"type":"object","title":"ReleaseConflictOutcome","description":"Held: these nodes changed since the release was pinned. Nothing was published.","additionalProperties":false,"required":["kind","at","conflicts"],"properties":{"kind":{"type":"string","enum":["conflict"]},"runId":{"$ref":"#/$defs/Id"},"at":{"$ref":"#/$defs/Timestamp"},"conflicts":{"type":"array","items":{"$ref":"#/$defs/ReleaseConflict"},"minItems":1,"maxItems":100}}},{"type":"object","title":"ReleaseFailedOutcome","description":"Refused at run time; nothing was published.","additionalProperties":false,"required":["kind","at","code"],"properties":{"kind":{"type":"string","enum":["failed"]},"runId":{"$ref":"#/$defs/Id"},"at":{"$ref":"#/$defs/Timestamp"},"code":{"type":"string","enum":["dependency_missing","invalid_snapshot","site_missing","node_body_missing","store_error"]},"message":{"type":"string","maxLength":500,"pattern":"^[^\\x00-\\x1F\\x7F\\u202A-\\u202E\\u2066-\\u2069]*$"}}},{"type":"object","title":"ReleaseMissedOutcome","description":"The scheduler reached the release more than an hour after its time; nothing was published. A person may publish it now.","additionalProperties":false,"required":["kind","at","latenessSeconds"],"properties":{"kind":{"type":"string","enum":["missed"]},"at":{"$ref":"#/$defs/Timestamp"},"latenessSeconds":{"type":"integer","minimum":0}}},{"type":"object","title":"ReleaseExpiredOutcome","description":"No valid approval when the release was due: never approved, the approver can no longer publish it, the agent's `auto` window ended, or its creator left.","additionalProperties":false,"required":["kind","at","reason"],"properties":{"kind":{"type":"string","enum":["expired"]},"at":{"$ref":"#/$defs/Timestamp"},"reason":{"type":"string","enum":["unapproved","approval_lost","auto_window_ended","creator_removed"]}}},{"type":"object","title":"ReleaseCanceledOutcome","description":"Withdrawn by a person or an agent; its nodes are unlocked.","additionalProperties":false,"required":["kind","at","by"],"properties":{"kind":{"type":"string","enum":["canceled"]},"at":{"$ref":"#/$defs/Timestamp"},"by":{"$ref":"#/$defs/Actor"}}}]}; +const pattern281 = new RegExp("^[^\\x00-\\x1F\\x7F\\u202A-\\u202E\\u2066-\\u2069]*$", "u"); +const schema515 = {"type":"object","description":"A node whose live version was not the pinned base when the release ran. `expected` absent: the node was not expected to be live; `found` absent: it was not live.","additionalProperties":false,"required":["nodeId"],"properties":{"nodeId":{"$ref":"#/$defs/Id"},"expected":{"$ref":"#/$defs/ReleaseNodeVersion"},"found":{"$ref":"#/$defs/ReleaseNodeVersion"}}}; -function validate209(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate225(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate209.evaluated; +const evaluated0 = validate225.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -35902,14 +38269,14 @@ errors++; } } if(data.expected !== undefined){ -if(!(validate197(data.expected, {instancePath:instancePath+"/expected",parentData:data,parentDataProperty:"expected",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate197.errors : vErrors.concat(validate197.errors); +if(!(validate213(data.expected, {instancePath:instancePath+"/expected",parentData:data,parentDataProperty:"expected",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate213.errors : vErrors.concat(validate213.errors); errors = vErrors.length; } } if(data.found !== undefined){ -if(!(validate197(data.found, {instancePath:instancePath+"/found",parentData:data,parentDataProperty:"found",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate197.errors : vErrors.concat(validate197.errors); +if(!(validate213(data.found, {instancePath:instancePath+"/found",parentData:data,parentDataProperty:"found",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate213.errors : vErrors.concat(validate213.errors); errors = vErrors.length; } } @@ -35924,16 +38291,16 @@ vErrors.push(err6); } errors++; } -validate209.errors = vErrors; +validate225.errors = vErrors; return errors === 0; } -validate209.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate225.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate208(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate224(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate208.evaluated; +const evaluated0 = validate224.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -36040,7 +38407,7 @@ vErrors.push(err8); errors++; } if(!(data0 === "published")){ -const err9 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema491.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err9 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema508.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err9]; } @@ -36248,7 +38615,7 @@ vErrors.push(err26); errors++; } if(!(((((data6 === "tags") || (data6 === "everything")) || (data6 === "deferred")) || (data6 === "failed")) || (data6 === "none"))){ -const err27 = {instancePath:instancePath+"/purge",schemaPath:"#/oneOf/0/properties/purge/enum",keyword:"enum",params:{allowedValues: schema491.oneOf[0].properties.purge.enum},message:"must be equal to one of the allowed values"}; +const err27 = {instancePath:instancePath+"/purge",schemaPath:"#/oneOf/0/properties/purge/enum",keyword:"enum",params:{allowedValues: schema508.oneOf[0].properties.purge.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err27]; } @@ -36332,7 +38699,7 @@ vErrors.push(err33); errors++; } if(!(data7 === "conflict")){ -const err34 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema491.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err34 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema508.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err34]; } @@ -36437,8 +38804,8 @@ errors++; } const len0 = data10.length; for(let i0=0; i0 Date: Wed, 11 Nov 2026 04:05:39 +0300 Subject: [PATCH 2/4] feat(core-ts): gate and pure core for reference and repeater fields (RFC-0045 step 1, #114) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gate: checkFieldDefinitions and checkBundle's new assertFieldTypes refuse reference and repeater definitions, and markers naming them, with fields_unsupported unless a capability allows them; none is passed yet, so site writes (Studio, MCP, API), field operations and every importSite bundle (import, pull, push, --replace, --revert, restores, edition copies) refuse. Pure core in core-ts/src/references/ and fields/: the §1 definition rules (targets, where, itemLabel, sub-field and leaf caps), props compilation and value checks for both types, per-item requiredToPublish, too_many_references, resolveFields skipping unknown types, the strict fieldsJsonSchema profile with strictSchemaCost, pickMember and referenceIssues. Dispatch tables: FIELD_QUERY_OPS and structured data get empty entries; EntryFieldsForm shows a read-only stub; FIELD_TYPES stays the nine enabled types (EnabledFieldType). MCP: update_collection's $defs come from defsClosure and list only enabled types; a test checks every tool schema's $refs resolve. API: 409 for fields_unsupported, 422 for the other two new codes. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com> --- .changeset/reference-repeater-contract.md | 25 + .../studio/src/components/fields/FieldRow.tsx | 6 +- .../components/listings/ListFieldsSection.tsx | 6 +- packages/api/src/errors.ts | 5 + packages/api/src/field-operations.ts | 4 +- packages/api/src/testing-fields.ts | 32 ++ packages/cli/src/commands/fields.ts | 4 +- .../valid/site/collection-stage4.json | 34 +- packages/core-ts/src/errors.ts | 9 +- .../core-ts/src/fields-operations/plan.ts | 10 +- packages/core-ts/src/fields/definitions.ts | 19 +- packages/core-ts/src/fields/index.ts | 50 ++ .../core-ts/src/fields/json-schema-strict.ts | 76 +++ packages/core-ts/src/fields/json-schema.ts | 74 ++- packages/core-ts/src/fields/props.ts | 44 +- packages/core-ts/src/fields/query.ts | 3 + packages/core-ts/src/fields/refusal.ts | 6 +- packages/core-ts/src/fields/resolve.ts | 15 +- packages/core-ts/src/fields/types.ts | 47 +- packages/core-ts/src/fields/value-checks.ts | 43 +- packages/core-ts/src/fields/values.ts | 9 + .../core-ts/src/references/definitions.ts | 240 +++++++++ packages/core-ts/src/references/issues.ts | 98 ++++ .../core-ts/src/references/json-schema.ts | 71 +++ packages/core-ts/src/references/members.ts | 72 +++ packages/core-ts/src/references/values.ts | 87 +++ .../core-ts/src/structured-data/values.ts | 3 + packages/core-ts/src/sync-checks.ts | 34 +- .../core-ts/src/testing-reference-gate.ts | 144 +++++ packages/core-ts/src/testing.ts | 2 + .../core-ts/test/fields-definitions.test.ts | 4 +- packages/core-ts/test/fields-query.test.ts | 6 +- packages/core-ts/test/references-core.test.ts | 507 ++++++++++++++++++ packages/mcp-server/src/collection-fields.ts | 33 +- packages/mcp-server/src/field-hints.ts | 7 + packages/mcp-server/src/schema-transforms.ts | 38 ++ packages/mcp-server/test/tool-schemas.test.ts | 146 +++++ packages/ui/src/blocks/EntryFieldsForm.tsx | 20 + packages/ui/test/entry-fields-form.test.tsx | 37 ++ 39 files changed, 2009 insertions(+), 61 deletions(-) create mode 100644 .changeset/reference-repeater-contract.md create mode 100644 packages/core-ts/src/fields/json-schema-strict.ts create mode 100644 packages/core-ts/src/references/definitions.ts create mode 100644 packages/core-ts/src/references/issues.ts create mode 100644 packages/core-ts/src/references/json-schema.ts create mode 100644 packages/core-ts/src/references/members.ts create mode 100644 packages/core-ts/src/references/values.ts create mode 100644 packages/core-ts/src/testing-reference-gate.ts create mode 100644 packages/core-ts/test/references-core.test.ts create mode 100644 packages/mcp-server/test/tool-schemas.test.ts diff --git a/.changeset/reference-repeater-contract.md b/.changeset/reference-repeater-contract.md new file mode 100644 index 00000000..63e67108 --- /dev/null +++ b/.changeset/reference-repeater-contract.md @@ -0,0 +1,25 @@ +--- +"@cloudhal/core-contracts": minor +"@cloudhal/core-ts": patch +"@cloudhal/mcp-server": patch +"@cloudhal/api": patch +--- + +The contract gains two field types for collections, `reference` and `repeater` (RFC-0045 §1, rollout step 1, #114). Every write path still refuses them, so existing sites behave exactly as before. + +- **Contract.** `FieldDefinition` gains `ReferenceField` and `RepeaterField`. A new `SubFieldDefinition` union holds a repeater's sub-fields: scalar values only, so a repeater cannot contain a repeater. `FieldMigration.fromType` gains both types, and `FieldMigration` gains an optional `sub`. There is no `schemaVersion` bump, and existing documents stay valid. +- **The gate.** Until rollout step 7 enables them, a reference or repeater definition, or a migration marker that names one, is refused with `fields_unsupported`: API status 409, with an MCP hint. The refusal covers: + - site creates and updates, so Studio, MCP and the API; + - field operations; + - every bundle `importSite` takes: import, pull, push, `cloudhal import --replace` and `--revert`, snapshot restores and edition copies. + + `checkBundle` gained `assertFieldTypes`, which refuses the types when no capability is given. +- **Pure core** (`@cloudhal/core-ts/fields`), with no effect on stored data yet: + - the definition rules: `reference_target_unknown`, `where`, `itemLabel`, and caps on sub-fields and leaves; + - value checks for both types, the per-item `requiredToPublish`, and `too_many_references`; + - `resolveFields` skips types it does not know; + - `fieldsJsonSchema(…, { strict: true })` and `strictSchemaCost`; + - `pickMember`, `referenceIssues`, and `FIELD_QUERY_OPS` and structured-data entries that make neither type listable or mappable. + + `FIELD_TYPES` keeps the nine enabled types (`EnabledFieldType`). `ALL_FIELD_TYPES` lists all eleven. +- **MCP.** `update_collection`'s input now builds its `$defs` with `defsClosure` and offers only the enabled types. A test checks that every `$ref` in every tool's input and output schema resolves. Before this change, `pnpm gen` alone would have left the input pointing at `$defs` it did not carry. diff --git a/apps/studio/src/components/fields/FieldRow.tsx b/apps/studio/src/components/fields/FieldRow.tsx index 669910ef..dc8a4e2e 100644 --- a/apps/studio/src/components/fields/FieldRow.tsx +++ b/apps/studio/src/components/fields/FieldRow.tsx @@ -1,4 +1,5 @@ import type { FieldDefinition } from "@cloudhal/core-contracts"; +import { isReferenceFieldType } from "@cloudhal/core-ts/fields"; import { Badge, Button, DropdownMenu, Text, Tooltip } from "@cloudhal/ui"; import { ArrowDown, @@ -59,7 +60,10 @@ export function FieldRow({ {def.key} - {t(`fields.types.${def.type}`)} + {/* RFC-0045: reference and repeater get their words with their blocks (rollout step 6). */} + {!isReferenceFieldType(def.type) && ( + {t(`fields.types.${def.type}`)} + )} {def.localized && {t("fields.builder.localized")}} {def.type !== "boolean" && def.requiredToPublish && ( {t("fields.editor.neededToPublish")} diff --git a/apps/studio/src/components/listings/ListFieldsSection.tsx b/apps/studio/src/components/listings/ListFieldsSection.tsx index 449c60a9..974af2ce 100644 --- a/apps/studio/src/components/listings/ListFieldsSection.tsx +++ b/apps/studio/src/components/listings/ListFieldsSection.tsx @@ -1,5 +1,5 @@ import type { CollectionSettings, FieldDefinition, Site } from "@cloudhal/core-contracts"; -import { fieldLabel } from "@cloudhal/core-ts/fields"; +import { fieldLabel, isReferenceFieldType } from "@cloudhal/core-ts/fields"; import { Badge, Button, Link, SectionEmpty, Select, Text } from "@cloudhal/ui"; import { ArrowDown, ArrowUp, Columns, Trash } from "@phosphor-icons/react"; import { type ReactNode, useEffect, useState } from "react"; @@ -181,7 +181,9 @@ function ListFieldRow({ {fieldKey} - {def && {t(`fields.types.${def.type}`)}} + {def && !isReferenceFieldType(def.type) && ( + {t(`fields.types.${def.type}`)} + )} {def && sortable(def) && ( {t("listings.listFields.sortable")} )} diff --git a/packages/api/src/errors.ts b/packages/api/src/errors.ts index 8f040fe2..1e99617e 100644 --- a/packages/api/src/errors.ts +++ b/packages/api/src/errors.ts @@ -91,6 +91,11 @@ export const INVARIANT_STATUS: Record { + const venue = { + key: "venue", + type: "reference", + labels: { en: { label: "Venue" } }, + localized: false, + collections: ["posts"], + }; + const speakers = { + key: "speakers", + type: "repeater", + labels: { en: { label: "Speakers" } }, + localized: false, + fields: [{ key: "name", type: "text", labels: { en: { label: "Name" } } }], + }; + for (const def of [venue, speakers]) + for (const query of ["?dryRun=1", ""]) { + const refused = await put({ fields: [def] }, query); + expect(refused.status, JSON.stringify(refused.body)).toBe(409); + expect(refused.body.error).toBe("fields_unsupported"); + } + expect((await site()).collections?.events).toBeUndefined(); + // and no field operation turns a field into one (the API takes the nine types only) + expect((await put(EVENTS)).status).toBe(200); + const ops = `/api/sites/${siteId}/collections/events/fields/operations`; + const retype = await call(ops, { + method: "POST", + body: JSON.stringify({ op: "change_field_type", key: "venue", type: "reference" }), + }); + expect(retype.status).toBe(400); + }); + it("field operations: dry run, run, status, continue and revert (RFC-0041 §6)", async () => { expect((await put(EVENTS)).status).toBe(200); const made = await create({ venue: "Hall", level: "expert" }); diff --git a/packages/cli/src/commands/fields.ts b/packages/cli/src/commands/fields.ts index 8910069c..74e8574f 100644 --- a/packages/cli/src/commands/fields.ts +++ b/packages/cli/src/commands/fields.ts @@ -17,7 +17,7 @@ import { revertFieldOperation, runFieldOperation, } from "@cloudhal/core-ts"; -import { FIELD_TYPES, type FieldType } from "@cloudhal/core-ts/fields"; +import { type EnabledFieldType, FIELD_TYPES, type FieldType } from "@cloudhal/core-ts/fields"; import { openLocalStore } from "@cloudhal/core-ts/local"; import { humanizeSlug } from "@cloudhal/core-ts/names"; import type { CliCommand } from "../cli/types.ts"; @@ -90,7 +90,7 @@ export function parseFieldsArgs( }; case "retype": { need(a, b); - if (!FIELD_TYPES.includes(b as FieldType)) + if (!FIELD_TYPES.includes(b as EnabledFieldType)) throw new Error(`the type is one of ${FIELD_TYPES.join(", ")}`); const op: FieldOperation = { op: "change_field_type", diff --git a/packages/core-contracts/fixtures/valid/site/collection-stage4.json b/packages/core-contracts/fixtures/valid/site/collection-stage4.json index f7ecd172..f2252a57 100644 --- a/packages/core-contracts/fixtures/valid/site/collection-stage4.json +++ b/packages/core-contracts/fixtures/valid/site/collection-stage4.json @@ -138,7 +138,7 @@ "localized": true, "fields": [ { - "key": "title", + "key": "heading", "type": "text", "labels": { "en": { @@ -201,7 +201,7 @@ "id": "snap_01HZX8K3Q9MG", "fromKey": "program", "fromType": "text", - "sub": "title" + "sub": "heading" } } ] @@ -220,6 +220,34 @@ } ] }, - "people": {} + "people": { + "fields": [ + { + "key": "role", + "type": "select", + "labels": { + "en": { + "label": "Role" + } + }, + "localized": false, + "options": [ + { + "value": "speaker", + "labels": { + "en": "Speaker" + } + }, + { + "value": "staff", + "labels": { + "en": "Staff" + } + } + ] + } + ], + "listFields": ["role"] + } } } diff --git a/packages/core-ts/src/errors.ts b/packages/core-ts/src/errors.ts index a4cf4901..3d6b90fe 100644 --- a/packages/core-ts/src/errors.ts +++ b/packages/core-ts/src/errors.ts @@ -210,7 +210,14 @@ export type InvariantCode = /** §2: a structured-data preset that does not fit the type's table or the collection's fields. */ | "structured_data_invalid" /** §3: `expiry.field` is not a date or datetime field of the collection. */ - | "invalid_expiry"; + | "invalid_expiry" + // ── RFC-0045 (collections stage 4) ── + /** §1: a reference or repeater definition; refused on every write path until rollout step 7. */ + | "fields_unsupported" + /** §1: a reference's `collections` names a collection the site neither declares nor uses. */ + | "reference_target_unknown" + /** §1: more than 100 reference values in one entry, across fields and repeater items. */ + | "too_many_references"; export class InvariantError extends Error { override readonly name = "InvariantError"; diff --git a/packages/core-ts/src/fields-operations/plan.ts b/packages/core-ts/src/fields-operations/plan.ts index 0d6ce54b..1bb508ab 100644 --- a/packages/core-ts/src/fields-operations/plan.ts +++ b/packages/core-ts/src/fields-operations/plan.ts @@ -6,7 +6,7 @@ */ import type { FieldDefinition, FieldMigration, Site } from "@cloudhal/core-contracts"; import { InvariantError } from "../errors.ts"; -import { RESERVED_FIELD_KEYS } from "../fields/types.ts"; +import { isReferenceFieldType, RESERVED_FIELD_KEYS } from "../fields/types.ts"; import type { Cond } from "../node-sql.ts"; import { renameSettingsReferences } from "../store/collection-settings.ts"; import { FIELD_OPERATIONS, type FieldOperation, finalKey, operationKey } from "./types.ts"; @@ -14,7 +14,10 @@ import { FIELD_OPERATIONS, type FieldOperation, finalKey, operationKey } from ". const KEY = /^[a-z][A-Za-z0-9]{0,39}$/; const COLLECTION = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; -function refuse(code: "unknown_field" | "field_key_duplicate" | "field_key_reserved", msg: string) { +function refuse( + code: "unknown_field" | "field_key_duplicate" | "field_key_reserved" | "fields_unsupported", + msg: string, +) { return new InvariantError(code, msg, { issues: [] }); } @@ -90,6 +93,9 @@ export function targetDefinition(def: FieldDefinition, op: FieldOperation): Fiel /** Checks the request against the collection; throws when it can never run as written. */ export function checkOperation(site: Site, op: FieldOperation): void { const { defs, def } = currentDefinition(site, op); + // RFC-0045: no operation turns a field into, or rewrites, a reference or repeater yet. + if (op.op === "change_field_type" && isReferenceFieldType(op.type)) + throw refuse("fields_unsupported", `${op.type} fields are not enabled on this server yet`); if (op.op === "rename_field") { if (!KEY.test(op.to)) throw refuse("field_key_reserved", `${op.to} is not a field key (lower camelCase)`); diff --git a/packages/core-ts/src/fields/definitions.ts b/packages/core-ts/src/fields/definitions.ts index dfc12aa9..a8c1108a 100644 --- a/packages/core-ts/src/fields/definitions.ts +++ b/packages/core-ts/src/fields/definitions.ts @@ -5,6 +5,11 @@ * count; this module never reads entries. */ import type { CollectionSettings, FieldDefinition, Site } from "@cloudhal/core-contracts"; +import { + type ReferenceDefinitionOptions, + referenceDefinitionIssues, + subFieldCount, +} from "../references/definitions.ts"; import { FIELD_LIMITS, type FieldIssue, @@ -173,14 +178,22 @@ const OPERATION: Record = { * guard for changes no plain edit may make (type, localization, `multiple`, the migration marker); * removals and removed options are listed by `fieldDefinitionChanges`, since refusing them depends * on stored values. + * + * RFC-0045: `reference` and `repeater` definitions are refused (`fields_unsupported`) unless + * `options.referenceTypes` says the writer's capability allows them; then their own rules run. + * Sub-fields count toward the per-site cap. */ -export function checkFieldDefinitions(site: SiteFields, before?: SiteFields): FieldIssue[] { +export function checkFieldDefinitions( + site: SiteFields, + before?: SiteFields, + options: ReferenceDefinitionOptions = {}, +): FieldIssue[] { const collections: Collections = site.collections ?? {}; - const out: FieldIssue[] = []; + const out: FieldIssue[] = referenceDefinitionIssues(site, options); let total = 0; for (const [key, settings] of Object.entries(collections)) { if (!settings) continue; - total += settings.fields?.length ?? 0; + total += (settings.fields?.length ?? 0) + subFieldCount(settings.fields); out.push(...checkCollection(key, settings)); } if (total > FIELD_LIMITS.fieldsPerSite) diff --git a/packages/core-ts/src/fields/index.ts b/packages/core-ts/src/fields/index.ts index ae0a27d4..a280d869 100644 --- a/packages/core-ts/src/fields/index.ts +++ b/packages/core-ts/src/fields/index.ts @@ -4,7 +4,43 @@ * express, values compiled to ADR-0030's props subset (`validateProps`), the JSON Schema agents * receive, and the values readers see (`resolveFields`); RFC-0042's listings by field (`query.ts`) * and derived expiry gates (`gate.ts`). Worker-safe and Temporal-free. + * + * RFC-0045 (`../references/`): the `reference` and `repeater` types' gate and definition rules, + * values, strict JSON Schema profile, `pickMember` and `referenceIssues`. Both types are refused on + * every write path (`fields_unsupported`) until rollout step 7. */ + +export { + type FieldTypeSupport, + type ReferenceDefinitionOptions, + referenceDefinitionIssues, + referenceTypesSupported, + subFieldCount, + unsupportedFieldTypeIssues, +} from "../references/definitions.ts"; +export { + hasReferenceFields, + type ReferenceIssue, + type ReferenceIssueReason, + type ReferenceSnapshot, + referenceIssues, +} from "../references/issues.ts"; +export { + isMemberOf, + type MemberLike, + memberGroup, + membersByGroup, + type PickedMember, + type PickMemberOptions, + pickMember, +} from "../references/members.ts"; +export { + type ReferenceUse, + referenceCountIssues, + referenceUses, + referenceValues, + requiredSubFieldIssues, +} from "../references/values.ts"; export { type Conversion, type ConvertContext, @@ -36,11 +72,14 @@ export { export { type FormatFieldOptions, formatFieldValue } from "./format.ts"; export { dayStartIn, effectiveUntil, expiryInstant, fieldGateInstant, nextDay } from "./gate.ts"; export { + type FieldsJsonSchemaOptions, fieldJsonSchema, fieldsJsonSchema, JSON_SCHEMA_KEYWORDS, type JsonSchema, + type JsonType, } from "./json-schema.ts"; +export { type StrictCost, strictFieldsSchema, strictSchemaCost } from "./json-schema-strict.ts"; export { convertContext, type FieldMigrationKind, @@ -57,7 +96,9 @@ export { DATE_PATTERN, fieldPropSchema, fieldsPropsSchema, + GROUP_ID_PATTERN, INSTANT_PATTERN, + subFieldPropSchema, } from "./props.ts"; export { canOrderBy, @@ -92,6 +133,8 @@ export { translatableFieldValues, } from "./translation.ts"; export { + ALL_FIELD_TYPES, + type EnabledFieldType, FIELD_LIMITS, FIELD_TYPES, type FieldIssue, @@ -100,9 +143,16 @@ export { fieldDefinitions, fieldHelp, fieldLabel, + isKnownFieldType, + isReferenceFieldType, optionLabel, + REFERENCE_FIELD_TYPES, RESERVED_FIELD_KEYS, + type ReferenceFieldDefinition, + type ReferenceFieldType, + type RepeaterFieldDefinition, type SiteFields, + type SubFieldType, } from "./types.ts"; export { isCalendarDay, diff --git a/packages/core-ts/src/fields/json-schema-strict.ts b/packages/core-ts/src/fields/json-schema-strict.ts new file mode 100644 index 00000000..859de793 --- /dev/null +++ b/packages/core-ts/src/fields/json-schema-strict.ts @@ -0,0 +1,76 @@ +/** + * RFC-0045 §6: the strict profile of the fields schema (`fieldsSchemaStrict`) and what it costs a + * provider (`strictCost`). OpenAI's strict mode needs every property required, optional ones as a + * `null` union, so every property that the open profile leaves optional becomes `[T, "null"]` (with + * `null` in its `enum`). At the top level `null` means "leave as it is"; inside a repeater item it + * means "unset", since a given list replaces the whole list. Properties the open profile already + * requires (a datetime's `at` and `timeZone`) stay as they are. Array items are never nullable. + */ +import type { JsonSchema } from "./json-schema.ts"; + +/** What a strict schema spends of a provider's limits (OpenAI: 5,000 properties, 10 levels, 1,000 enum values; Anthropic: 16 unions per request). */ +export interface StrictCost { + /** Property entries in every object. */ + properties: number; + /** Nesting levels, the root counting 1 (fields → list → item → value is 4). */ + depth: number; + /** `enum` entries, `null` included. */ + enumValues: number; + /** Schemas whose `type` is a list (`[T, "null"]`). */ + unions: number; +} + +const ROOT = + "Typed custom fields of this collection, for strict structured output (fieldsStrict): every key is required, null leaves the stored value as it is (on create: no value), and clearFields removes a value."; +const ITEM_NULLS = + "Every key is required: null unsets that value in this item, since a given list replaces the whole stored list."; + +function nullable(schema: JsonSchema): JsonSchema { + const t = schema.type; + const type: JsonSchema["type"] = typeof t === "string" ? [t, "null"] : t; + const out: JsonSchema = { ...schema, type }; + if (schema.enum) out.enum = [...schema.enum.filter((v) => v !== null), null]; + return out; +} + +function strictObject(schema: JsonSchema, isItem: boolean): JsonSchema { + const required = new Set(schema.required ?? []); + const properties: Record = {}; + for (const [key, child] of Object.entries(schema.properties ?? {})) { + const strict = strictNode(child); + properties[key] = required.has(key) ? strict : nullable(strict); + } + const out: JsonSchema = { ...schema, properties, required: Object.keys(properties) }; + if (isItem) out.description = out.description ? `${out.description} ${ITEM_NULLS}` : ITEM_NULLS; + return out; +} + +function strictNode(schema: JsonSchema): JsonSchema { + if (schema.type === "object") return strictObject(schema, false); + if (schema.type === "array" && schema.items) { + const items = schema.items.type === "object" ? strictObject(schema.items, true) : schema.items; + return { ...schema, items }; + } + return schema; +} + +/** The strict profile of an open-profile fields schema (`fieldsJsonSchema`'s output). */ +export function strictFieldsSchema(open: JsonSchema): JsonSchema { + return { ...strictObject(open, false), description: ROOT }; +} + +/** Counts `schema`'s properties, depth, enum values and unions (`list_collections` `strictCost`). */ +export function strictSchemaCost(schema: JsonSchema): StrictCost { + const cost: StrictCost = { properties: 0, depth: 0, enumValues: 0, unions: 0 }; + const walk = (s: JsonSchema, level: number): void => { + cost.depth = Math.max(cost.depth, level); + cost.enumValues += s.enum?.length ?? 0; + if (typeof s.type !== "string") cost.unions += 1; + const children = Object.values(s.properties ?? {}); + cost.properties += children.length; + for (const c of children) walk(c, level + 1); + if (s.items) walk(s.items, level + 1); + }; + walk(schema, 1); + return cost; +} diff --git a/packages/core-ts/src/fields/json-schema.ts b/packages/core-ts/src/fields/json-schema.ts index 2682932d..4579285e 100644 --- a/packages/core-ts/src/fields/json-schema.ts +++ b/packages/core-ts/src/fields/json-schema.ts @@ -6,17 +6,27 @@ * `description`. Top-level properties are not required. Numeric and length limits, which * Anthropic's strict mode drops, are written into each `description`; the checker enforces them. * Generated from definitions only, never from text an agent wrote. + * + * RFC-0045 §6: references are group id strings (or lists of them) and repeaters arrays of closed + * flat objects; the root is always an object, with no `$ref` and no recursion. With + * `{ strict: true }` the same schema in its strict profile (`json-schema-strict.ts`). */ -import type { FieldDefinition } from "@cloudhal/core-contracts"; +import type { FieldDefinition, SubFieldDefinition } from "@cloudhal/core-contracts"; +import { type EmitWords, emitReference, emitRepeater } from "../references/json-schema.ts"; +import { strictFieldsSchema } from "./json-schema-strict.ts"; import { ASSET_ID_PATTERN } from "./props.ts"; -import { FIELD_LIMITS, fieldHelp, fieldLabel, optionLabel } from "./types.ts"; +import { FIELD_LIMITS, fieldHelp, fieldLabel, isKnownFieldType, optionLabel } from "./types.ts"; + +export type JsonType = "object" | "string" | "number" | "integer" | "boolean" | "array"; /** The subset of JSON Schema this emitter writes. */ export interface JsonSchema { - type: "object" | "string" | "number" | "integer" | "boolean" | "array"; + /** A list only in the strict profile: `[T, "null"]`. */ + type: JsonType | readonly [JsonType, "null"]; title?: string; description?: string; - enum?: string[]; + /** `null` only in the strict profile. */ + enum?: (string | null)[]; pattern?: string; format?: "date" | "date-time"; items?: JsonSchema; @@ -39,13 +49,17 @@ export const JSON_SCHEMA_KEYWORDS: readonly string[] = [ "additionalProperties", ]; -interface Words { - locale: string; - defaultLocale: string; -} +type Words = EmitWords; type Emit = (def: FieldDefinition, words: Words) => { schema: JsonSchema; notes: string[] }; +export interface FieldsJsonSchemaOptions { + /** The strict profile (`fieldsSchemaStrict`): every property required, optional ones nullable. */ + strict?: boolean | undefined; + /** A collection's name for reference targets ("Places"); absent: its key. */ + collectionName?: ((key: string) => string | undefined) | undefined; +} + const EMIT: Record = { text: (def) => { const max = (def.type === "text" && def.maxLength) || FIELD_LIMITS.textLength; @@ -118,6 +132,33 @@ const EMIT: Record = { } : { schema: one, notes: [`One of: ${listed}.`] }; }, + reference: (def, words) => + def.type === "reference" + ? emitReference(def, words) + : { schema: { type: "string" }, notes: [] }, + repeater: (def, words) => + def.type === "repeater" + ? emitRepeater(def, words, subFieldJsonSchema) + : { schema: { type: "array" }, notes: [] }, +}; + +/** A repeater sub-field's schema: its type's, with its label, help and limits (no localization note). */ +function subFieldJsonSchema(sub: SubFieldDefinition, words: Words): JsonSchema { + const def = sub as unknown as FieldDefinition; + const { schema, notes } = EMIT[sub.type](def, words); + const help = fieldHelp(def, words.locale, words.defaultLocale); + if (sub.type !== "boolean" && sub.requiredToPublish) notes.push("Needed to publish."); + return { + ...schema, + title: fieldLabel(def, words.locale, words.defaultLocale), + description: [help, ...notes].filter(Boolean).join(" "), + }; +} + +const SHARED: Partial> = { + reference: + "Set on the original only; every language version shows the targets in its own language.", + repeater: "Set on the original only; every language version shows this list.", }; /** One field's schema, with its label as `title` and help plus limits as `description`. */ @@ -125,11 +166,13 @@ export function fieldJsonSchema( def: FieldDefinition, locale: string, defaultLocale = locale, + options: Pick = {}, ): JsonSchema { - const words = { locale, defaultLocale }; + const words: Words = { locale, defaultLocale, collectionName: options.collectionName }; const { schema, notes } = EMIT[def.type](def, words); const help = fieldHelp(def, locale, defaultLocale); - if (!def.localized) notes.push("Set on the original only; translations read it from there."); + if (!def.localized) + notes.push(SHARED[def.type] ?? "Set on the original only; translations read it from there."); if (def.type !== "boolean" && def.requiredToPublish) notes.push("Needed to publish."); return { ...schema, @@ -140,20 +183,25 @@ export function fieldJsonSchema( /** * The schema of a collection's `fields` object, in `locale` (labels fall back to - * `defaultLocale`, then any). Every property is optional; omit a key to leave it unset. + * `defaultLocale`, then any). Every property is optional; omit a key to leave it unset. With + * `strict`, its strict profile. A definition of a type this version does not know is left out. */ export function fieldsJsonSchema( defs: readonly FieldDefinition[], locale: string, defaultLocale = locale, + options: FieldsJsonSchemaOptions = {}, ): JsonSchema { const properties: Record = {}; - for (const def of defs) properties[def.key] = fieldJsonSchema(def, locale, defaultLocale); - return { + for (const def of defs) + if (isKnownFieldType(def.type)) + properties[def.key] = fieldJsonSchema(def, locale, defaultLocale, options); + const open: JsonSchema = { type: "object", description: "Typed custom fields of this collection. Every field is optional; leave a key out to leave it unset.", properties, additionalProperties: false, }; + return options.strict ? strictFieldsSchema(open) : open; } diff --git a/packages/core-ts/src/fields/props.ts b/packages/core-ts/src/fields/props.ts index f1eaeb93..f0b71a37 100644 --- a/packages/core-ts/src/fields/props.ts +++ b/packages/core-ts/src/fields/props.ts @@ -4,8 +4,14 @@ * checks. What the subset cannot say (calendar days, zones, repeats, C0 controls, rich text over * the subset's 10 000-character string ceiling) is in `values.ts`. */ -import type { FieldDefinition } from "@cloudhal/core-contracts"; -import type { Localized, PropSchema, PropsSchema } from "../blocks.ts"; +import type { FieldDefinition, SubFieldDefinition } from "@cloudhal/core-contracts"; +import type { + FlatObjectPropSchema, + Localized, + PropSchema, + PropsSchema, + ScalarPropSchema, +} from "../blocks.ts"; import { FIELD_LIMITS } from "./types.ts"; /** `YYYY-MM-DD`; whether the day exists is checked in code. */ @@ -17,6 +23,8 @@ export const INSTANT_PATTERN = export const TIME_ZONE_SHAPE = "^(?:UTC|[A-Z][A-Za-z_-]+(?:/[A-Za-z0-9_+-]+){1,2})$"; /** A bare asset id, `common.schema.json#/$defs/Id` with the `asset` prefix. */ export const ASSET_ID_PATTERN = "^asset_[A-Za-z0-9]{6,32}$"; +/** RFC-0045: a translation group id, an entry id (`common.schema.json#/$defs/Id`, `entry` prefix). */ +export const GROUP_ID_PATTERN = "^entry_[A-Za-z0-9]{6,32}$"; /** The subset's `Localized`: per-locale words when `en` is among them, else the first one. */ function localized( @@ -28,7 +36,10 @@ function localized( return entries[0]?.[1] ?? fallback; } -function annotations(def: FieldDefinition): { title: Localized; description?: Localized } { +function annotations(def: Pick): { + title: Localized; + description?: Localized; +} { const labels: Record = {}; const help: Record = {}; for (const [locale, l] of Object.entries(def.labels)) { @@ -81,8 +92,35 @@ const COMPILE: Record = { ? { type: "array", items: one, maxItems: values.length } : one; }, + // RFC-0045: a group id, or a list of them (repeats are checked in value-checks.ts). + reference: (def) => { + const one = { type: "string" as const, pattern: GROUP_ID_PATTERN }; + return def.type === "reference" && def.multiple + ? { type: "array", items: one, maxItems: def.maxItems ?? FIELD_LIMITS.listItems } + : one; + }, + // RFC-0045: ADR-0030's array of flat objects; every sub-field compiles to a scalar. + repeater: (def) => { + const subs = def.type === "repeater" ? def.fields : []; + const items: FlatObjectPropSchema = { + type: "object", + properties: Object.fromEntries(subs.map((sub) => [sub.key, subFieldPropSchema(sub)])), + additionalProperties: false, + }; + const maxItems = (def.type === "repeater" && def.maxItems) || FIELD_LIMITS.listItems; + return { type: "array", items, maxItems }; + }, }; +/** + * A repeater sub-field's prop schema: its type's compiler (a sub-field is a field without + * `localized` and `migration`, and never `multiple`), with its labels as `title`/`description`. + */ +export function subFieldPropSchema(sub: SubFieldDefinition): ScalarPropSchema { + const def = sub as unknown as FieldDefinition; + return { ...COMPILE[sub.type](def), ...annotations(sub) } as ScalarPropSchema; +} + const fieldCache = new WeakMap(); const listCache = new WeakMap(); diff --git a/packages/core-ts/src/fields/query.ts b/packages/core-ts/src/fields/query.ts index 2ebdeb69..fbe45c41 100644 --- a/packages/core-ts/src/fields/query.ts +++ b/packages/core-ts/src/fields/query.ts @@ -60,6 +60,9 @@ export const FIELD_QUERY_OPS: Readonly([ "field_bounds", "too_many_fields", "field_change_needs_operation", + "fields_unsupported", + "reference_target_unknown", ]); const asInvariant = (code: FieldIssueCode): InvariantCode => @@ -26,6 +29,7 @@ export function fieldRefusalCode(issues: readonly FieldIssue[]): InvariantCode { const first = issues[0]; if (!first) return "invalid_fields"; if (issues.every((i) => i.code === first.code)) return asInvariant(first.code); + if (issues.some((i) => i.code === "fields_unsupported")) return "fields_unsupported"; if (issues.some((i) => i.code === "field_change_needs_operation")) return "field_change_needs_operation"; const definition = issues.find((i) => DEFINITION_CODES.has(i.code)); diff --git a/packages/core-ts/src/fields/resolve.ts b/packages/core-ts/src/fields/resolve.ts index 3fcb067c..c87c1726 100644 --- a/packages/core-ts/src/fields/resolve.ts +++ b/packages/core-ts/src/fields/resolve.ts @@ -8,12 +8,21 @@ * - during a field operation the `migration` marker is read through: a missing key falls back to * `fromKey`; a value still in `fromType` that does not fit the new type is converted by RFC-0041 * §6's closed table (`conversions.ts`), and resolves to `undefined` with an issue only when the - * table cannot convert it; a field being removed is already gone. + * table cannot convert it; a field being removed is already gone; + * - RFC-0045: a reference resolves to its group ids and a repeater to its items, each sub-value + * checked as a field is (loading targets is the renderer's job); a definition of a type this + * version does not know is skipped, so a newer writer's type never breaks an older reader. */ import type { ContentEntry, FieldDefinition } from "@cloudhal/core-contracts"; import { convertFieldValue } from "./conversions.ts"; import { convertContext, isBeingRemoved } from "./migration.ts"; -import { type FieldIssue, fieldDefinitions, isRecord, type SiteFields } from "./types.ts"; +import { + type FieldIssue, + fieldDefinitions, + isKnownFieldType, + isRecord, + type SiteFields, +} from "./types.ts"; import { valueIssues } from "./value-checks.ts"; export interface ResolvedField { @@ -72,7 +81,7 @@ export function resolveFields( ): ResolvedFields { const out: ResolvedFields = {}; for (const def of fieldDefinitions(site, entry.collection)) { - if (isBeingRemoved(def)) continue; + if (isBeingRemoved(def) || !isKnownFieldType(def.type)) continue; const { value, fromOriginal } = pick(def, entry, original); if (value === undefined) { out[def.key] = { def, value: undefined, fromOriginal: false }; diff --git a/packages/core-ts/src/fields/types.ts b/packages/core-ts/src/fields/types.ts index d58c5064..9e731e13 100644 --- a/packages/core-ts/src/fields/types.ts +++ b/packages/core-ts/src/fields/types.ts @@ -2,10 +2,17 @@ * RFC-0041 / ADR-0043: shared names of typed custom fields — the issue shape, the limits, the * reserved keys and small helpers. Worker-safe, no imports beyond contract types. */ -import type { FieldDefinition, Site } from "@cloudhal/core-contracts"; +import type { FieldDefinition, Site, SubFieldDefinition } from "@cloudhal/core-contracts"; export type FieldType = FieldDefinition["type"]; +export type SubFieldType = SubFieldDefinition["type"]; export type SelectFieldDefinition = Extract; +export type ReferenceFieldDefinition = Extract; +export type RepeaterFieldDefinition = Extract; +/** RFC-0045: the two stage-4 types, refused on every write path until rollout step 7. */ +export type ReferenceFieldType = "reference" | "repeater"; +/** The types every writer accepts today (`FIELD_TYPES`). */ +export type EnabledFieldType = Exclude; /** What field code reads of a site: its collection settings. */ export interface SiteFields { collections?: Site["collections"] | undefined; @@ -15,8 +22,11 @@ export interface SiteFields { defaultLocale?: string | undefined; } -/** The nine closed types, in the contract's order. */ -export const FIELD_TYPES: readonly FieldType[] = [ +/** + * The nine types every writer accepts, in the contract's order: what Studio, MCP, the API and the + * CLI offer. The stage-4 types (`REFERENCE_FIELD_TYPES`) are not among them until rollout step 7. + */ +export const FIELD_TYPES: readonly EnabledFieldType[] = [ "text", "richText", "number", @@ -28,6 +38,21 @@ export const FIELD_TYPES: readonly FieldType[] = [ "select", ]; +/** RFC-0045: `reference` and `repeater`, gated (`fields_unsupported`) until rollout step 7. */ +export const REFERENCE_FIELD_TYPES: readonly ReferenceFieldType[] = ["reference", "repeater"]; + +/** Every type of the contract, in its order (`FIELD_TYPES` then `REFERENCE_FIELD_TYPES`). */ +export const ALL_FIELD_TYPES: readonly FieldType[] = [...FIELD_TYPES, ...REFERENCE_FIELD_TYPES]; + +const KNOWN_TYPES: ReadonlySet = new Set(ALL_FIELD_TYPES); + +/** True for a type of this contract version; a newer writer's type is not (RFC-0045 §4). */ +export const isKnownFieldType = (type: string): type is FieldType => KNOWN_TYPES.has(type); + +/** True for `reference` and `repeater`. */ +export const isReferenceFieldType = (type: string): type is ReferenceFieldType => + type === "reference" || type === "repeater"; + /** RFC-0041 §2, §4 and Security: every limit in one place. */ export const FIELD_LIMITS = { /** Per collection (also in the schema's `maxItems`). */ @@ -43,6 +68,12 @@ export const FIELD_LIMITS = { richTextLength: 50_000, urlLength: 2048, selectOptions: 50, + /** RFC-0045: a multiple reference's or a repeater's `maxItems` when absent. */ + listItems: 20, + /** RFC-0045: fields plus repeater sub-fields per collection. */ + leavesPerCollection: 100, + /** RFC-0045: reference values per entry, across fields and repeater items. */ + referencesPerEntry: 100, } as const; /** @@ -109,7 +140,15 @@ export type FieldIssueCode = /** Definitions: a change that could lose or reinterpret values (§6). */ | "field_change_needs_operation" /** Reading: a value that no longer fits its definition resolves to `undefined`. */ - | "field_value_unreadable"; + | "field_value_unreadable" + /** RFC-0045: a reference or repeater definition this server does not accept yet. */ + | "fields_unsupported" + /** RFC-0045: a reference's `collections` names a collection the site does not have. */ + | "reference_target_unknown" + /** RFC-0045: a reference's `where` does not fit its target collection's list fields. */ + | "invalid_query" + /** RFC-0045: more than 100 reference values in one entry. */ + | "too_many_references"; /** One problem, at a slash path (`fields/startsAt/at`, `collections/events/fields/2/key`). */ export interface FieldIssue { diff --git a/packages/core-ts/src/fields/value-checks.ts b/packages/core-ts/src/fields/value-checks.ts index 9b69d016..72647ce1 100644 --- a/packages/core-ts/src/fields/value-checks.ts +++ b/packages/core-ts/src/fields/value-checks.ts @@ -125,8 +125,48 @@ const EXTRA: Partial> = { const list = value as string[]; return new Set(list).size === list.length ? [] : [issue(path, "each option at most once")]; }, + // RFC-0045 §1: a multiple reference names each group once. + reference(def, value, path) { + if (def.type !== "reference" || !def.multiple) return []; + const list = value as string[]; + return new Set(list).size === list.length ? [] : [issue(path, "each entry at most once")]; + }, + // RFC-0045 §1: no empty item; each sub-value gets its type's own checks. + repeater(def, value, path, options) { + if (def.type !== "repeater" || !Array.isArray(value)) return []; + const out: FieldIssue[] = []; + (value as Record[]).forEach((item, i) => { + if (Object.keys(item).length === 0) out.push(issue(`${path}/${i}`, "an item needs a value")); + for (const sub of def.fields) { + const v = item[sub.key]; + const extra = EXTRA[sub.type]; + if (v !== undefined && extra) + out.push( + ...extra(sub as unknown as FieldDefinition, v, `${path}/${i}/${sub.key}`, options), + ); + } + }); + return out; + }, }; +/** + * What the props subset checks: rich text, top-level or in a repeater item, as "" (the subset caps + * strings at 10 000 characters; rich text's own 50 000 limit is checked by `EXTRA`). + */ +function forSubset(def: FieldDefinition, value: unknown): unknown { + if (def.type === "richText") return typeof value === "string" ? "" : value; + if (def.type !== "repeater" || !Array.isArray(value)) return value; + const rich = def.fields.filter((s) => s.type === "richText").map((s) => s.key); + if (rich.length === 0) return value; + return value.map((item) => { + if (typeof item !== "object" || item === null || Array.isArray(item)) return item; + const copy: Record = { ...item }; + for (const key of rich) if (typeof copy[key] === "string") copy[key] = ""; + return copy; + }); +} + /** Problems with one value (`path` is where it sits, e.g. `fields/startsAt`). */ export function valueIssues( def: FieldDefinition, @@ -136,8 +176,7 @@ export function valueIssues( ): FieldIssue[] { if (value === null) return [issue(path, "null is never stored: leave the key out to remove the value")]; - // The subset caps strings at 10 000 characters; rich text's own limit is checked below. - const typed = def.type === "richText" && typeof value === "string" ? "" : value; + const typed = forSubset(def, value); const found = validateProps( { type: "object", properties: { v: fieldPropSchema(def) }, additionalProperties: false }, { v: typed }, diff --git a/packages/core-ts/src/fields/values.ts b/packages/core-ts/src/fields/values.ts index 17b18d8c..cbf6f221 100644 --- a/packages/core-ts/src/fields/values.ts +++ b/packages/core-ts/src/fields/values.ts @@ -7,6 +7,7 @@ */ import type { FieldDefinition, PublishStatus } from "@cloudhal/core-contracts"; import { type BlockRegistry, createBlockRegistry } from "../blocks.ts"; +import { referenceCountIssues, requiredSubFieldIssues } from "../references/values.ts"; import { type FieldsEntry, type FieldsOriginal, hasFieldValue, resolveFields } from "./resolve.ts"; import { FIELD_LIMITS, type FieldIssue, jsonBytes, type SiteFields, sameJson } from "./types.ts"; import { valueIssues } from "./value-checks.ts"; @@ -82,6 +83,12 @@ export function checkFieldValues( } out.push(...valueIssues(def, value, path, { blocks })); } + // RFC-0045: the per-entry reference cap, over every value (once one changed). + const touchesReferences = changedKeys(input).some((k) => { + const type = byKey.get(k)?.type; + return type === "reference" || type === "repeater"; + }); + if (out.length === 0 && touchesReferences) out.push(...referenceCountIssues(defs, input.values)); return out; } @@ -103,6 +110,8 @@ export function checkRequiredToPublish( const out: FieldIssue[] = []; for (const [key, field] of Object.entries(resolveFields(site, entry, original))) { const def = field.def; + // RFC-0045: a repeater's required sub-fields need a value in every item. + out.push(...requiredSubFieldIssues(def, field.value, `fields/${key}`)); if (def.type === "boolean" || def.requiredToPublish !== true) continue; if (!hasFieldValue(field.value)) out.push({ diff --git a/packages/core-ts/src/references/definitions.ts b/packages/core-ts/src/references/definitions.ts new file mode 100644 index 00000000..b804cd82 --- /dev/null +++ b/packages/core-ts/src/references/definitions.ts @@ -0,0 +1,240 @@ +/** + * RFC-0045 §1: the gate that refuses `reference` and `repeater` definitions (`fields_unsupported`) + * on every write path until rollout step 7, and the definition rules for both types that JSON + * Schema cannot express. Pure and Worker-safe; `checkFieldDefinitions` runs both. + */ +import type { FieldDefinition, Site, SubFieldDefinition } from "@cloudhal/core-contracts"; +import { BUILT_IN_COLLECTION } from "../collections.ts"; +import { checkFieldQuery } from "../fields/query.ts"; +import { + FIELD_LIMITS, + type FieldIssue, + isReferenceFieldType, + RESERVED_FIELD_KEYS, + type SiteFields, +} from "../fields/types.ts"; + +type Collections = NonNullable; + +/** + * The capability a writer asks before it accepts reference or repeater definitions, from a store + * option, `CopySiteOptions`, the site Worker's `fields: 2` or the in-process `FIELDS_SUPPORTED` + * (rollout steps 2 and 7). Unlike `GateSupport`, an absent capability refuses. + */ +export interface FieldTypeSupport { + /** True when the renderer serving `siteId` reads reference and repeater fields. */ + supported(siteId: string): boolean | Promise; +} + +/** True only when a capability is given and says yes for the site. */ +export async function referenceTypesSupported( + siteId: string, + support: FieldTypeSupport | undefined, +): Promise { + return support !== undefined && (await support.supported(siteId)); +} + +export interface ReferenceDefinitionOptions { + /** The writer's `FieldTypeSupport` said yes for this site. Absent or false: both types are refused. */ + referenceTypes?: boolean | undefined; + /** Collections inferred from entries (RFC-0040): valid reference targets though not declared. */ + inUse?: Iterable | undefined; +} + +const unsupported = (path: string, what: string): FieldIssue => ({ + path, + code: "fields_unsupported", + message: `${what}: reference and repeater fields are not enabled on this server yet (RFC-0045)`, +}); + +/** + * The gate: one issue per definition of a stage-4 type, and per migration marker that names one + * (`fromType`, `sub`), so no write path stores a definition a renderer of this version cannot read. + */ +export function unsupportedFieldTypeIssues(site: SiteFields): FieldIssue[] { + const out: FieldIssue[] = []; + for (const [key, settings] of Object.entries(site.collections ?? {})) + (settings?.fields ?? []).forEach((def, i) => { + const at = `collections/${key}/fields/${i}`; + if (isReferenceFieldType(def.type)) + out.push(unsupported(`${at}/type`, `${def.key} is a ${def.type} field`)); + else if ( + (def.migration?.fromType && isReferenceFieldType(def.migration.fromType)) || + def.migration?.sub !== undefined + ) + out.push( + unsupported( + `${at}/migration`, + `${def.key} converts from a ${def.migration.fromType ?? "repeater"} field`, + ), + ); + }); + return out; +} + +/** Sub-fields of a collection's repeaters. */ +export function subFieldCount(fields: readonly FieldDefinition[] | undefined): number { + let n = 0; + for (const def of fields ?? []) if (def.type === "repeater") n += def.fields.length; + return n; +} + +const LABELLING = new Set(["text", "select", "reference"]); + +function knownCollections(collections: Collections, inUse: Iterable | undefined) { + return new Set([...Object.keys(collections), BUILT_IN_COLLECTION, ...(inUse ?? [])]); +} + +function targetIssues( + names: readonly string[], + known: ReadonlySet, + at: string, +): FieldIssue[] { + return names.flatMap((name, i) => + known.has(name) + ? [] + : [ + { + path: `${at}/collections/${i}`, + code: "reference_target_unknown", + message: `${name} is not a collection of this site`, + }, + ], + ); +} + +function whereIssues( + def: Extract, + collections: Collections, + at: string, +): FieldIssue[] { + const where = def.where; + if (!where) return []; + const issue = (path: string, message: string): FieldIssue => ({ + path, + code: "invalid_query", + message, + }); + const [target, ...more] = def.collections; + if (more.length > 0 || target === undefined) + return [issue(`${at}/where`, "where needs exactly one target collection")]; + const out: FieldIssue[] = []; + where.forEach((c, i) => { + if (c.value === "now" || c.values?.includes("now")) + out.push( + issue(`${at}/where/${i}`, "where cannot use now: it is checked when a target is chosen"), + ); + }); + const settings = collections[target]; + for (const q of checkFieldQuery(settings?.fields ?? [], settings?.listFields, { where })) + out.push(issue(`${at}/${q.path}`, q.message)); + return out; +} + +function subFieldIssues( + def: Extract, + known: ReadonlySet, + at: string, +): FieldIssue[] { + const out: FieldIssue[] = []; + const seen = new Set(); + def.fields.forEach((sub, i) => { + const here = `${at}/fields/${i}`; + if (RESERVED_FIELD_KEYS.has(sub.key)) + out.push({ + path: `${here}/key`, + code: "field_key_reserved", + message: `${sub.key} is reserved`, + }); + if (seen.has(sub.key)) + out.push({ + path: `${here}/key`, + code: "field_key_duplicate", + message: `${sub.key} is used by another sub-field of ${def.key}`, + }); + seen.add(sub.key); + if ( + sub.type === "number" && + sub.minimum !== undefined && + sub.maximum !== undefined && + sub.minimum > sub.maximum + ) + out.push({ + path: `${here}/minimum`, + code: "field_bounds", + message: "minimum is above maximum", + }); + if (sub.type === "select") { + const values = new Set(); + sub.options.forEach((o, j) => { + if (values.has(o.value)) + out.push({ + path: `${here}/options/${j}/value`, + code: "field_option_duplicate", + message: `option ${o.value} is listed twice`, + }); + values.add(o.value); + }); + } + if (sub.type === "reference") out.push(...targetIssues(sub.collections, known, here)); + }); + const label = def.itemLabel; + if (label !== undefined && !def.fields.some((s) => s.key === label && LABELLING.has(s.type))) + out.push({ + path: `${at}/itemLabel`, + code: "unknown_field", + message: `${label} is not a text, select or reference sub-field of ${def.key}`, + }); + return out; +} + +/** The rules of one reference or repeater definition (none for the other types). */ +function definitionIssues( + def: FieldDefinition, + collections: Collections, + known: ReadonlySet, + at: string, +): FieldIssue[] { + if (def.type === "reference") { + const out = targetIssues(def.collections, known, at); + if (def.maxItems !== undefined && def.multiple !== true) + out.push({ + path: `${at}/maxItems`, + code: "field_bounds", + message: "maxItems needs multiple", + }); + return [...out, ...whereIssues(def, collections, at)]; + } + if (def.type === "repeater") return subFieldIssues(def, known, at); + return []; +} + +/** + * RFC-0045 §1's code rules: targets are collections of the site, `where` only with one target + * collection and on its list fields without `now`, `maxItems` only with `multiple`, sub-field keys + * unique and not reserved, `itemLabel` a text, select or reference sub-field, and at most 100 + * leaves (fields plus sub-fields) per collection. Without `referenceTypes`, the gate's issues only. + */ +export function referenceDefinitionIssues( + site: SiteFields, + options: ReferenceDefinitionOptions = {}, +): FieldIssue[] { + if (!options.referenceTypes) return unsupportedFieldTypeIssues(site); + const collections: Collections = site.collections ?? {}; + const known = knownCollections(collections, options.inUse); + const out: FieldIssue[] = []; + for (const [key, settings] of Object.entries(collections)) { + const fields = settings?.fields ?? []; + const leaves = fields.length + subFieldCount(fields); + if (leaves > FIELD_LIMITS.leavesPerCollection) + out.push({ + path: `collections/${key}/fields`, + code: "too_many_fields", + message: `at most ${FIELD_LIMITS.leavesPerCollection} fields and sub-fields per collection (${leaves})`, + }); + fields.forEach((def, i) => { + out.push(...definitionIssues(def, collections, known, `collections/${key}/fields/${i}`)); + }); + } + return out; +} diff --git a/packages/core-ts/src/references/issues.ts b/packages/core-ts/src/references/issues.ts new file mode 100644 index 00000000..fbb75d21 --- /dev/null +++ b/packages/core-ts/src/references/issues.ts @@ -0,0 +1,98 @@ +/** + * RFC-0045 §2 "Publish warns, never fails": `referenceIssues` lists the references of published + * entries that will not resolve at an instant. Studio's publish dialog, the MCP `publish` tool's + * approval text, `cloudhal snapshot --publish` and `cloudhal import --dry-run` show it; nothing + * refuses on it. Pure: the caller passes a snapshot's site and entries, and `now`. + */ +import type { ContentEntry, Site } from "@cloudhal/core-contracts"; +import { derivedGates, entryVisibility, originalsOf } from "../fields/entry-gates.ts"; +import { resolveFields } from "../fields/resolve.ts"; +import { isReferenceFieldType } from "../fields/types.ts"; +import { membersByGroup, pickMember } from "./members.ts"; +import { type ReferenceUse, referenceUses } from "./values.ts"; + +/** + * Why a reference does not resolve: + * - `missing`: the group has no member in this site in a collection the field allows; + * - `hidden`: it has, but none the published site shows at `now` (draft, archived, gated); + * - `locale`: shown members exist, but none in the referrer's locale, and the site does not fall + * back to its default locale (or has no member there either). + */ +export type ReferenceIssueReason = "missing" | "hidden" | "locale"; + +export interface ReferenceIssue { + /** The published entry holding (or inheriting) the reference. */ + entryId: string; + collection: string; + locale: string; + /** Where the value sits: `fields/venue`, `fields/speakers/1/person`. */ + path: string; + groupId: string; + reason: ReferenceIssueReason; +} + +/** What `referenceIssues` reads of a snapshot: its site and every entry in it. */ +export interface ReferenceSnapshot { + site: Pick; + entries: readonly ContentEntry[]; +} + +/** True when some collection of the site has a reference or repeater field. */ +export function hasReferenceFields(site: Pick): boolean { + return Object.values(site.collections ?? {}).some((c) => + (c?.fields ?? []).some((d) => isReferenceFieldType(d.type)), + ); +} + +type Groups = ReadonlyMap; + +/** Why `use` does not resolve; undefined when it does. */ +function unresolved( + use: ReferenceUse, + locale: string, + view: { all: Groups; visible: Groups; site: ReferenceSnapshot["site"] }, +): ReferenceIssueReason | undefined { + const shown = view.visible.get(use.groupId) ?? []; + const pick = { + locale, + defaultLocale: view.site.defaultLocale, + fallback: view.site.localization?.fallback, + collections: use.collections, + }; + if (pickMember(use.groupId, shown, pick)) return undefined; + const allowed = (m: ContentEntry) => use.collections.includes(m.collection); + if (!(view.all.get(use.groupId) ?? []).some(allowed)) return "missing"; + return shown.some(allowed) ? "locale" : "hidden"; +} + +/** + * The references of entries shown at `now` (published, gates open, not hidden by a derived gate; + * `unlist`ed entries count) that resolve to nothing, by the locale rule (`pickMember`) over the + * entries shown at `now`. A translation's inherited values are checked in its own locale. + */ +export function referenceIssues(snapshot: ReferenceSnapshot, now: number): ReferenceIssue[] { + const { site, entries } = snapshot; + if (!hasReferenceFields(site)) return []; + const derived = derivedGates(site, entries); + const shown = entries.filter((e) => entryVisibility(e, derived.get(e.id), now) !== "hidden"); + const view = { all: membersByGroup(entries), visible: membersByGroup(shown), site }; + const originalOf = originalsOf(entries); + const out: ReferenceIssue[] = []; + for (const entry of shown) + for (const [key, field] of Object.entries(resolveFields(site, entry, originalOf(entry)))) { + if (!isReferenceFieldType(field.def.type)) continue; + for (const use of referenceUses(field.def, field.value, `fields/${key}`)) { + const reason = unresolved(use, entry.locale, view); + if (reason) + out.push({ + entryId: entry.id, + collection: entry.collection, + locale: entry.locale, + path: use.path, + groupId: use.groupId, + reason, + }); + } + } + return out; +} diff --git a/packages/core-ts/src/references/json-schema.ts b/packages/core-ts/src/references/json-schema.ts new file mode 100644 index 00000000..e16c1fac --- /dev/null +++ b/packages/core-ts/src/references/json-schema.ts @@ -0,0 +1,71 @@ +/** + * RFC-0045 §6: the open-profile JSON Schema of reference and repeater fields, for + * `fieldsJsonSchema`. A reference is a group id string (or a list of them); a repeater an array of + * closed flat objects. No `$ref`, no recursion, depth at most 4; limits go into `description`. + */ +import type { FieldDefinition, SubFieldDefinition } from "@cloudhal/core-contracts"; +import type { JsonSchema } from "../fields/json-schema.ts"; +import { GROUP_ID_PATTERN } from "../fields/props.ts"; +import { FIELD_LIMITS } from "../fields/types.ts"; + +export interface EmitWords { + locale: string; + defaultLocale: string; + /** A collection's name for agents, e.g. "Places"; absent: the key alone. */ + collectionName?: ((key: string) => string | undefined) | undefined; +} + +export interface Emitted { + schema: JsonSchema; + notes: string[]; +} + +/** One sub-field's schema with its title and description (the caller emits the scalar types). */ +export type EmitSub = (sub: SubFieldDefinition, words: EmitWords) => JsonSchema; + +/** "Places (places) or People (people)". */ +function targets(collections: readonly string[], words: EmitWords): string { + const named = collections.map((key) => { + const name = words.collectionName?.(key); + return name && name !== key ? `${name} (${key})` : key; + }); + return named.length > 1 + ? `${named.slice(0, -1).join(", ")} or ${named[named.length - 1]}` + : (named[0] ?? ""); +} + +const ONE = (): JsonSchema => ({ type: "string", pattern: GROUP_ID_PATTERN }); + +/** A reference: a group id, or with `multiple` a list of them. */ +export function emitReference( + def: Pick< + Extract, + "collections" | "multiple" | "maxItems" + >, + words: EmitWords, +): Emitted { + const where = `An entry in ${targets(def.collections, words)} of this site, by the groupId list_entries returns (an entry's own id is replaced by its group's).`; + if (!def.multiple) return { schema: ONE(), notes: [where] }; + const max = def.maxItems ?? FIELD_LIMITS.listItems; + return { + schema: { type: "array", items: ONE() }, + notes: [ + `An ordered list of at most ${max} entries in ${targets(def.collections, words)} of this site, by groupId, each at most once.`, + ], + }; +} + +/** A repeater: an ordered list of closed flat objects, one property per sub-field. */ +export function emitRepeater( + def: Extract, + words: EmitWords, + emitSub: EmitSub, +): Emitted { + const properties: Record = {}; + for (const sub of def.fields) properties[sub.key] = emitSub(sub, words); + const max = def.maxItems ?? FIELD_LIMITS.listItems; + return { + schema: { type: "array", items: { type: "object", properties, additionalProperties: false } }, + notes: [`An ordered list of at most ${max} items; an item holds at least one value.`], + }; +} diff --git a/packages/core-ts/src/references/members.ts b/packages/core-ts/src/references/members.ts new file mode 100644 index 00000000..ff1563bf --- /dev/null +++ b/packages/core-ts/src/references/members.ts @@ -0,0 +1,72 @@ +/** + * RFC-0045 §2 "Membership" and §3 "The locale rule": which node of this site a reference to a + * translation group shows. One function, `pickMember`, for the renderer, Studio's preview and + * `referenceIssues`. Pure; callers pass the members their view shows (gates applied). + */ +import type { ContentEntry, Localization } from "@cloudhal/core-contracts"; + +/** What membership and the locale rule read of a node. */ +export type MemberLike = Pick & { + translation?: Pick, "group"> | undefined; +}; + +/** A node's translation group: `COALESCE(translation.group, id)`, the one rule every probe uses. */ +export const memberGroup = (node: Pick): string => + node.translation?.group ?? node.id; + +/** True when `node` is a member of group `groupId`. */ +export const isMemberOf = ( + node: Pick, + groupId: string, +): boolean => memberGroup(node) === groupId; + +export interface PickMemberOptions { + /** The page's locale. */ + locale: string; + /** The site's default locale. */ + defaultLocale: string; + /** `Site.localization.fallback`; absent reads as `none`. */ + fallback?: Localization["fallback"] | undefined; + /** The field's `collections`: only members in one of them count. */ + collections: readonly string[]; +} + +export interface PickedMember { + member: T; + /** The member is the default locale's, shown under `default-with-notice`; themes set `lang`. */ + fromFallbackLocale: boolean; +} + +/** + * The member of `groupId` a reference shows: among `members` of the group in an allowed + * collection, the one in the page's locale; else, under `default-with-notice`, the default + * locale's (flagged); else none. The first such member wins (a group has one node per locale). + */ +export function pickMember( + groupId: string, + members: Iterable, + options: PickMemberOptions, +): PickedMember | undefined { + const allowed = new Set(options.collections); + let fallback: T | undefined; + for (const m of members) { + if (!isMemberOf(m, groupId) || !allowed.has(m.collection)) continue; + if (m.locale === options.locale) return { member: m, fromFallbackLocale: false }; + if (fallback === undefined && m.locale === options.defaultLocale) fallback = m; + } + return fallback !== undefined && options.fallback === "default-with-notice" + ? { member: fallback, fromFallbackLocale: true } + : undefined; +} + +/** Members by group, for repeated `pickMember` calls over one view. */ +export function membersByGroup(members: Iterable): Map { + const out = new Map(); + for (const m of members) { + const g = memberGroup(m); + const list = out.get(g); + if (list) list.push(m); + else out.set(g, [m]); + } + return out; +} diff --git a/packages/core-ts/src/references/values.ts b/packages/core-ts/src/references/values.ts new file mode 100644 index 00000000..1e130ddb --- /dev/null +++ b/packages/core-ts/src/references/values.ts @@ -0,0 +1,87 @@ +/** + * RFC-0045 §1: the entry-level value rules of references and repeaters: at most 100 reference + * values per entry (across fields and repeater items), and a repeater's `requiredToPublish` + * sub-fields in every item. Pure; the store's target guard (rollout step 2) checks the targets. + */ +import type { FieldDefinition } from "@cloudhal/core-contracts"; +import { FIELD_LIMITS, type FieldIssue, isRecord } from "../fields/types.ts"; + +/** "Has a value" for one sub-value: not absent, not an empty string. */ +const hasSubValue = (v: unknown): boolean => + v !== undefined && v !== null && !(typeof v === "string" && v.trim() === ""); + +/** One reference value where it sits: `fields/venue`, `fields/speakers/1/person`. */ +export interface ReferenceUse { + path: string; + groupId: string; + /** The collections the field (or sub-field) allows. */ + collections: readonly string[]; +} + +/** The reference values a reference or repeater value holds, in order (other types: none). */ +export function referenceUses(def: FieldDefinition, value: unknown, path: string): ReferenceUse[] { + if (def.type === "reference") { + const { collections } = def; + if (typeof value === "string") return [{ path, groupId: value, collections }]; + if (!Array.isArray(value)) return []; + return value.flatMap((v, i) => + typeof v === "string" ? [{ path: `${path}/${i}`, groupId: v, collections }] : [], + ); + } + if (def.type !== "repeater" || !Array.isArray(value)) return []; + const subs = def.fields.filter((s) => s.type === "reference"); + const out: ReferenceUse[] = []; + value.forEach((item, i) => { + if (!isRecord(item)) return; + for (const sub of subs) { + const v = item[sub.key]; + if (typeof v === "string" && sub.type === "reference") + out.push({ path: `${path}/${i}/${sub.key}`, groupId: v, collections: sub.collections }); + } + }); + return out; +} + +/** The group ids a reference or repeater value names, in order (other types: none). */ +export function referenceValues(def: FieldDefinition, value: unknown): string[] { + return referenceUses(def, value, "").map((u) => u.groupId); +} + +/** The 100-references cap over an entry's stored values (unknown keys count nothing). */ +export function referenceCountIssues( + defs: readonly FieldDefinition[], + values: Readonly>, +): FieldIssue[] { + let n = 0; + for (const def of defs) + if (Object.hasOwn(values, def.key)) n += referenceValues(def, values[def.key]).length; + if (n <= FIELD_LIMITS.referencesPerEntry) return []; + return [ + { + path: "fields", + code: "too_many_references", + message: `${n} references; an entry holds at most ${FIELD_LIMITS.referencesPerEntry} across its fields and repeater items`, + }, + ]; +} + +/** A resolved repeater's `requiredToPublish` sub-fields missing in an item, at their item paths. */ +export function requiredSubFieldIssues( + def: FieldDefinition, + value: unknown, + path: string, +): FieldIssue[] { + if (def.type !== "repeater" || !Array.isArray(value)) return []; + const required = def.fields.filter((s) => s.type !== "boolean" && s.requiredToPublish === true); + const out: FieldIssue[] = []; + value.forEach((item, i) => { + for (const sub of required) + if (!isRecord(item) || !hasSubValue(item[sub.key])) + out.push({ + path: `${path}/${i}/${sub.key}`, + code: "required_field", + message: `${def.key} item ${i + 1} needs ${sub.key} before the entry goes to review or is published`, + }); + }); + return out; +} diff --git a/packages/core-ts/src/structured-data/values.ts b/packages/core-ts/src/structured-data/values.ts index abb06e02..15b63210 100644 --- a/packages/core-ts/src/structured-data/values.ts +++ b/packages/core-ts/src/structured-data/values.ts @@ -197,6 +197,9 @@ const FIELD: Record = { select(value, _def, { spec }) { return fromVocabulary(spec, value); }, + // RFC-0045: no value until rollout step 8 (`ReferenceValueSource`); a repeater never maps. + reference: () => undefined, + repeater: () => undefined, }; function imageUrl(image: { assetId?: string; src?: string }, ctx: StructuredDataContext) { diff --git a/packages/core-ts/src/sync-checks.ts b/packages/core-ts/src/sync-checks.ts index a2e588ed..d7e9cc92 100644 --- a/packages/core-ts/src/sync-checks.ts +++ b/packages/core-ts/src/sync-checks.ts @@ -2,8 +2,8 @@ * The checks a site bundle passes before anything of it is written (`planImport`): the format, the * site node, rows of this site only, every row a valid contract document whose columns agree with * its body (precompiled validators only, AGENTS.md rule 6), rows the node batch's keys accept, - * gates, the time zone, the media and takedowns the bundle lists, and every node body a snapshot - * references. Worker-safe; not exported from the package (`planImport` is the public surface). + * gates, field types (RFC-0045), the time zone, the media and takedowns the bundle lists, and every + * node body a snapshot references. Worker-safe; not exported from the package (`planImport` is the public surface). */ import { type Asset, assertValid, type NodeRef } from "@cloudhal/core-contracts"; import { @@ -14,9 +14,15 @@ import { isTimeZone, } from "./availability.ts"; import { InvariantError } from "./errors.ts"; +import { refuseFieldIssues } from "./fields/refusal.ts"; import { nodeHash } from "./hash.ts"; import { validateMenuTree } from "./menu.ts"; import { isNodeKind, NODE_SPEC, type NodeKind, type NodeTypes } from "./nodes.ts"; +import { + type FieldTypeSupport, + referenceTypesSupported, + unsupportedFieldTypeIssues, +} from "./references/definitions.ts"; import { type BundleMedia, type NodeRow, @@ -247,14 +253,33 @@ export async function assertIdsFree(to: StoragePair, bundle: SiteBundle): Promis } } +/** + * RFC-0045 §1: a bundle whose site defines a reference or repeater field (or a marker converting + * one) is refused (`fields_unsupported`) unless `support` says the site's renderer reads them. + * Without a capability the types are refused, unlike `assertGates`. One check for `importSite`, + * so for pull, push, `cloudhal import --replace` and `--revert`, and snapshot restores. + */ +export async function assertFieldTypes( + siteId: string, + site: NodeRow, + support: FieldTypeSupport | undefined, +): Promise { + const body = JSON.parse(site.body) as Parameters[0]; + const issues = unsupportedFieldTypeIssues(body); + if (issues.length === 0 || (await referenceTypesSupported(siteId, support))) return; + refuseFieldIssues(issues, { siteId }); +} + /** * The checks that need no target, in `importSite`'s order: the format, the site node, rows of this - * site only, valid rows (`assertValidRows`), distinct rows (`assertDistinctRows`) and gates. - * Returns the validated site row. + * site only, valid rows (`assertValidRows`), distinct rows (`assertDistinctRows`), gates and + * field types (`assertFieldTypes`; no `fieldTypes` refuses the RFC-0045 types). Returns the + * validated site row. */ export async function checkBundle( bundle: SiteBundle, gates: GateSupport | undefined, + fieldTypes?: FieldTypeSupport | undefined, ): Promise { if (bundle.format !== SITE_BUNDLE_FORMAT) throw new Error(`unsupported bundle format ${bundle.format}`); @@ -269,6 +294,7 @@ export async function checkBundle( await assertValidRows(siteId, bundle); assertDistinctRows(bundle); await assertGates(siteId, bundle, gates); + await assertFieldTypes(siteId, siteRow, fieldTypes); return siteRow; } diff --git a/packages/core-ts/src/testing-reference-gate.ts b/packages/core-ts/src/testing-reference-gate.ts new file mode 100644 index 00000000..bdf3c07e --- /dev/null +++ b/packages/core-ts/src/testing-reference-gate.ts @@ -0,0 +1,144 @@ +/** + * RFC-0045 rollout step 1: the gate. Until rollout step 7 no write path stores a `reference` or + * `repeater` definition (`fields_unsupported`): site creates and updates (Studio, MCP and the API + * all write through them), field operations, and every bundle `importSite` takes (import, pull, + * push, `cloudhal import --replace` and `--revert`, snapshot restores and edition copies all go + * through its `checkBundle`). Called by `describeSiteGraphStore`, so it runs on every backend. + */ +import type { Actor, FieldDefinition, Site } from "@cloudhal/core-contracts"; +import { describe, expect, it } from "vitest"; +import { InvariantError } from "./errors.ts"; +import { runFieldOperation } from "./fields-operations/index.ts"; +import { canonicalJson, nodeHash } from "./hash.ts"; +import type { SiteGraphStore } from "./store.ts"; +import { exportSite, importSite, type SiteBundle } from "./sync.ts"; +import type { SuiteContext } from "./testing-taxonomy.ts"; + +const person: Actor = { kind: "human", id: "user_01HZX8K3Q9CD", name: "Halil" }; +const L = (label: string) => ({ en: { label } }); + +const TEXT: FieldDefinition = { key: "room", type: "text", labels: L("Room"), localized: true }; +const VENUE: FieldDefinition = { + key: "venue", + type: "reference", + labels: L("Venue"), + localized: false, + collections: ["places"], +}; +const SPEAKERS: FieldDefinition = { + key: "speakers", + type: "repeater", + labels: L("Speakers"), + localized: false, + fields: [{ key: "name", type: "text", labels: L("Name") }], +}; + +async function code(p: Promise): Promise { + try { + await p; + } catch (e) { + if (e instanceof InvariantError) return e.code; + throw e; + } + throw new Error("expected a rejection"); +} + +const events = (fields: FieldDefinition[]): NonNullable => ({ + events: { fields: fields as [FieldDefinition, ...FieldDefinition[]] }, + places: {}, +}); + +/** `bundle` with its site node's `collections` replaced (body and hash kept consistent). */ +async function withCollections(bundle: SiteBundle, collections: Site["collections"]) { + const copy = structuredClone(bundle); + const row = copy.nodes.find((n) => n.kind === "site"); + if (!row) throw new Error("no site row"); + const body = { ...(JSON.parse(row.body) as Site), collections }; + row.body = canonicalJson(body); + row.hash = await nodeHash(body); + return copy; +} + +export function referenceGateSuite(ctx: SuiteContext): void { + describe("RFC-0045 step 1: reference and repeater types are refused on every write path", () => { + let n = 0; + const store = (): SiteGraphStore => ctx.store(); + const storage = () => ({ sql: ctx.sql(), blobs: ctx.blobs() }); + + async function setup(collections: Site["collections"] = events([TEXT])): Promise { + n++; + const project = await store().createProject({ + name: "Gate", + slug: `ref-gate-${n}`, + ownerId: person.id, + siteIds: [], + }); + return store().createSite({ + projectId: project.id, + name: "Gate", + slug: `ref-gate-${n}`, + defaultLocale: "en", + locales: ["en", "tr"], + domains: [], + pageIds: [], + theme: { name: "blog" }, + ...(collections ? { collections } : {}), + }); + } + + it("refuses them when a site is created or updated", async () => { + expect(await code(setup(events([VENUE])))).toBe("fields_unsupported"); + const site = await setup(); + for (const def of [VENUE, SPEAKERS]) + expect(await code(store().updateSite({ ...site, collections: events([TEXT, def]) }))).toBe( + "fields_unsupported", + ); + // a dry run refuses too, and nothing was written + expect( + await code( + store().updateSite({ ...site, collections: events([TEXT, VENUE]) }, { dryRun: true }), + ), + ).toBe("fields_unsupported"); + expect((await store().getSite(site.id))?.collections).toEqual(site.collections); + }); + + it("refuses a field operation that turns a field into one", async () => { + const site = await setup(); + for (const type of ["reference", "repeater"] as const) + expect( + await code( + runFieldOperation( + store(), + site.id, + { op: "change_field_type", collection: "events", key: "room", type }, + { actor: person }, + ), + ), + ).toBe("fields_unsupported"); + expect((await store().getSite(site.id))?.collections).toEqual(site.collections); + }); + + it("refuses bundles that define them: merge and replace imports alike", async () => { + const site = await setup(); + const bundle = await exportSite(storage(), site.id); + for (const def of [VENUE, SPEAKERS]) { + const tainted = await withCollections(bundle, events([TEXT, def])); + expect(await code(importSite(storage(), tainted))).toBe("fields_unsupported"); + expect(await code(importSite(storage(), tainted, { mode: "replace" }))).toBe( + "fields_unsupported", + ); + } + // a marker converting from one is a stage-4 artefact too + const marker = await withCollections( + bundle, + events([{ ...TEXT, migration: { id: "snap_01HZX8K3Q9MG", fromType: "repeater" } }]), + ); + expect(await code(importSite(storage(), marker, { mode: "replace" }))).toBe( + "fields_unsupported", + ); + expect((await store().getSite(site.id))?.collections).toEqual(site.collections); + // the untouched bundle still imports + expect((await importSite(storage(), bundle)).nodes.unchanged).toBe(true); + }); + }); +} diff --git a/packages/core-ts/src/testing.ts b/packages/core-ts/src/testing.ts index 207b0bc5..4fdc30b8 100644 --- a/packages/core-ts/src/testing.ts +++ b/packages/core-ts/src/testing.ts @@ -17,6 +17,7 @@ import { fieldsSuite } from "./testing-fields.ts"; import { listingsSuite } from "./testing-listings.ts"; import { menuSuite } from "./testing-menu.ts"; import { permalinksSuite } from "./testing-permalinks.ts"; +import { referenceGateSuite } from "./testing-reference-gate.ts"; import { racingDriver, type SuiteContext, taxonomySuite } from "./testing-taxonomy.ts"; export interface StoreFactory { @@ -275,6 +276,7 @@ export function describeSiteGraphStore(label: string, factory: StoreFactory): vo collectionsSuite(suite); fieldsSuite(suite); fieldOperationsSuite(suite); + referenceGateSuite(suite); listingsSuite(suite); entryQuerySuite(suite); }); diff --git a/packages/core-ts/test/fields-definitions.test.ts b/packages/core-ts/test/fields-definitions.test.ts index 307f0f4e..86720ed6 100644 --- a/packages/core-ts/test/fields-definitions.test.ts +++ b/packages/core-ts/test/fields-definitions.test.ts @@ -33,7 +33,9 @@ describe("checkFieldDefinitions", () => { for (const name of await readdir(FIXTURES)) { const data = JSON.parse(await readFile(path.join(FIXTURES, name), "utf8")) as Site; expect(isValid("site", data), name).toBe(true); - expect(checkFieldDefinitions(data), name).toEqual([]); + // RFC-0045: stage-4 definitions pass only once a writer's capability allows them. + expect(checkFieldDefinitions(data, undefined, { referenceTypes: true }), name).toEqual([]); + if (name !== "collection-stage4.json") expect(checkFieldDefinitions(data), name).toEqual([]); } }); diff --git a/packages/core-ts/test/fields-query.test.ts b/packages/core-ts/test/fields-query.test.ts index d210c9a2..12960d39 100644 --- a/packages/core-ts/test/fields-query.test.ts +++ b/packages/core-ts/test/fields-query.test.ts @@ -15,7 +15,7 @@ import { parseFieldQuery, } from "../src/fields/query.ts"; import { type ResolvedFields, resolveFields } from "../src/fields/resolve.ts"; -import { FIELD_TYPES } from "../src/fields/types.ts"; +import { ALL_FIELD_TYPES, FIELD_TYPES } from "../src/fields/types.ts"; const L = { en: { label: "x" } }; const DEFS: FieldDefinition[] = [ @@ -74,7 +74,9 @@ const issues = (query: FieldQuery, listFields: string[] = ALL) => describe("the operator table (§4)", () => { it("covers every field type, and only number, date and datetime sort", () => { - expect(Object.keys(FIELD_QUERY_OPS).sort()).toEqual([...FIELD_TYPES].sort()); + expect(Object.keys(FIELD_QUERY_OPS).sort()).toEqual([...ALL_FIELD_TYPES].sort()); + // RFC-0045: neither stage-4 type is listable yet. + expect([FIELD_QUERY_OPS.reference, FIELD_QUERY_OPS.repeater]).toEqual([[], []]); expect(FIELD_TYPES.filter(canOrderBy)).toEqual(["number", "date", "datetime"]); expect(FIELD_QUERY_OPS.boolean).toEqual(["eq"]); expect(FIELD_QUERY_OPS.richText).toEqual(["set", "unset"]); diff --git a/packages/core-ts/test/references-core.test.ts b/packages/core-ts/test/references-core.test.ts new file mode 100644 index 00000000..6eb60c8c --- /dev/null +++ b/packages/core-ts/test/references-core.test.ts @@ -0,0 +1,507 @@ +/** + * RFC-0045 rollout step 1: the pure core of reference and repeater fields — the gate, the + * definition rules, values, `resolveFields`, the agents' JSON Schema (open and strict), + * `pickMember` and `referenceIssues`. Store, renderer and tool behaviour come in later steps. + */ +import type { ContentEntry, FieldDefinition, Site } from "@cloudhal/core-contracts"; +import { describe, expect, it } from "vitest"; +import { validateProps } from "../src/blocks.ts"; +import { + checkFieldDefinitions, + checkFieldValues, + checkRequiredToPublish, + FIELD_LIMITS, + fieldPropSchema, + fieldRefusalCode, + fieldsJsonSchema, + JSON_SCHEMA_KEYWORDS, + type JsonSchema, + pickMember, + referenceIssues, + resolveFields, + strictSchemaCost, + unsupportedFieldTypeIssues, + valueIssues, +} from "../src/fields/index.ts"; + +const L = (label: string) => ({ en: { label } }); +const G1 = "entry_01HZX8K3Q9P1"; +const G2 = "entry_01HZX8K3Q9P2"; + +const VENUE = { + key: "venue", + type: "reference", + labels: L("Venue"), + localized: false, + collections: ["places"], +} as const satisfies FieldDefinition; +const PARTNERS = { + key: "partners", + type: "reference", + labels: L("Partners"), + localized: true, + collections: ["places", "people"], + multiple: true, + maxItems: 3, +} as const satisfies FieldDefinition; +const SPEAKERS = { + key: "speakers", + type: "repeater", + labels: L("Speakers"), + localized: false, + maxItems: 4, + fields: [ + { + key: "person", + type: "reference", + labels: L("Person"), + requiredToPublish: true, + collections: ["people"], + }, + { + key: "role", + type: "select", + labels: L("Role"), + options: [ + { value: "host", labels: { en: "Host" } }, + { value: "speaker", labels: { en: "Speaker" } }, + { value: "panelist", labels: { en: "Panelist" } }, + ], + }, + { key: "talk", type: "text", labels: L("Talk"), maxLength: 200 }, + { key: "abstract", type: "richText", labels: L("Abstract") }, + { key: "day", type: "date", labels: L("Day") }, + { key: "slides", type: "url", labels: L("Slides") }, + ], +} as const satisfies FieldDefinition; +const STARTS = { + key: "startsAt", + type: "datetime", + labels: L("Starts"), + localized: false, +} as const satisfies FieldDefinition; + +type Collections = NonNullable; +const siteWith = (events: FieldDefinition[], more: Collections = {}) => ({ + defaultLocale: "en", + collections: { + events: { fields: events }, + places: { labels: { en: { singular: "Place", plural: "Places" } } }, + people: { + fields: [ + { + key: "team", + type: "select", + labels: L("Team"), + localized: false, + options: [{ value: "core", labels: { en: "Core" } }], + }, + ], + listFields: ["team"], + }, + ...more, + } as unknown as Collections, +}); +const codes = (issues: readonly { code: string }[]) => issues.map((i) => i.code); +const ENABLED = { referenceTypes: true }; + +describe("the gate (fields_unsupported)", () => { + it("refuses both types, and markers naming them, without a capability", () => { + const site = siteWith([VENUE, SPEAKERS]); + const issues = checkFieldDefinitions(site); + expect(issues.map((i) => [i.path, i.code])).toEqual([ + ["collections/events/fields/0/type", "fields_unsupported"], + ["collections/events/fields/1/type", "fields_unsupported"], + ]); + expect(fieldRefusalCode(issues)).toBe("fields_unsupported"); + const marker = (migration: object) => + ({ + ...STARTS, + type: "text", + migration: { id: "snap_01HZX8K3Q9MG", ...migration }, + }) as FieldDefinition; + expect( + codes(unsupportedFieldTypeIssues(siteWith([marker({ fromType: "reference" })]))), + ).toEqual(["fields_unsupported"]); + expect(codes(unsupportedFieldTypeIssues(siteWith([marker({ sub: "talk" })])))).toEqual([ + "fields_unsupported", + ]); + expect(unsupportedFieldTypeIssues(siteWith([marker({ fromType: "date" })]))).toEqual([]); + }); + + it("wins the refusal code over other definition problems", () => { + const issues = checkFieldDefinitions(siteWith([VENUE, { ...STARTS, key: "title" }])); + expect(codes(issues)).toEqual(["fields_unsupported", "field_key_reserved"]); + expect(fieldRefusalCode(issues)).toBe("fields_unsupported"); + }); + + it("changes nothing for sites without the new types", () => { + expect(checkFieldDefinitions(siteWith([STARTS]))).toEqual([]); + expect(checkFieldDefinitions(siteWith([STARTS]), undefined, ENABLED)).toEqual([]); + }); +}); + +describe("definition rules (with the capability)", () => { + const check = (events: FieldDefinition[], more?: Collections, inUse?: string[]) => + checkFieldDefinitions(siteWith(events, more), undefined, { ...ENABLED, inUse }); + + it("accepts declared, inferred and built-in target collections only", () => { + expect(check([VENUE, PARTNERS, SPEAKERS])).toEqual([]); + expect(check([{ ...VENUE, collections: ["posts"] }])).toEqual([]); + const venue = { ...VENUE, collections: ["halls"] } as FieldDefinition; + expect(check([venue]).map((i) => [i.path, i.code])).toEqual([ + ["collections/events/fields/0/collections/0", "reference_target_unknown"], + ]); + expect(check([venue], {}, ["halls"])).toEqual([]); + const sub = { ...SPEAKERS, fields: [{ ...SPEAKERS.fields[0], collections: ["crew"] }] }; + expect(check([sub as unknown as FieldDefinition]).map((i) => i.path)).toEqual([ + "collections/events/fields/0/fields/0/collections/0", + ]); + }); + + it("checks where: one target, its list fields, no now", () => { + const where = (w: object, collections = ["people"]) => + check([{ ...VENUE, collections, where: [w] } as unknown as FieldDefinition]); + expect(where({ field: "team", op: "eq", value: "core" })).toEqual([]); + expect(codes(where({ field: "team", op: "eq", value: "core" }, ["people", "places"]))).toEqual([ + "invalid_query", + ]); + expect(where({ field: "nope", op: "eq", value: "x" })[0]?.path).toBe( + "collections/events/fields/0/where/0/field", + ); + expect(codes(where({ field: "team", op: "eq", value: "now" }))).toContain("invalid_query"); + }); + + it("refuses maxItems without multiple, bad sub-fields and a bad itemLabel", () => { + expect(codes(check([{ ...VENUE, maxItems: 3 } as FieldDefinition]))).toEqual(["field_bounds"]); + const subs = (fields: object[], extra: object = {}) => + check([{ ...SPEAKERS, fields, ...extra } as unknown as FieldDefinition]); + const talk = SPEAKERS.fields[2]; + expect(codes(subs([talk, talk]))).toEqual(["field_key_duplicate"]); + expect(codes(subs([{ ...talk, key: "title" }]))).toEqual(["field_key_reserved"]); + expect( + codes(subs([{ key: "n", type: "number", labels: L("N"), minimum: 2, maximum: 1 }])), + ).toEqual(["field_bounds"]); + const role = SPEAKERS.fields[1]; + expect(codes(subs([{ ...role, options: [role.options[0], role.options[0]] }]))).toEqual([ + "field_option_duplicate", + ]); + expect(subs([talk], { itemLabel: "talk" })).toEqual([]); + expect(codes(subs([talk, SPEAKERS.fields[4]], { itemLabel: "day" }))).toEqual([ + "unknown_field", + ]); + }); + + it("counts sub-fields toward the leaf and site caps", () => { + const ten = Array.from({ length: 10 }, (_, i) => ({ + key: `s${i}`, + type: "text", + labels: L("S"), + })); + const repeaters = Array.from( + { length: 10 }, + (_, i) => ({ ...SPEAKERS, key: `r${i}`, fields: ten }) as unknown as FieldDefinition, + ); + // 10 fields + 100 sub-fields = 110 leaves + expect(codes(check(repeaters))).toEqual(["too_many_fields"]); + const nine = repeaters.slice(0, 9); + expect(check(nine)).toEqual([]); // 99 leaves + const sites: Collections = {}; + for (const c of ["a", "b"]) + sites[c] = { fields: nine as [FieldDefinition, ...FieldDefinition[]] }; + // 3 collections × 99 > 200 + expect(codes(check(nine, sites)).at(-1)).toBe("too_many_fields"); + }); +}); + +describe("values", () => { + it("compiles references and repeaters into the props subset", () => { + expect(fieldPropSchema(VENUE)).toMatchObject({ + type: "string", + pattern: "^entry_[A-Za-z0-9]{6,32}$", + }); + expect(fieldPropSchema(PARTNERS)).toMatchObject({ type: "array", maxItems: 3 }); + const repeater = fieldPropSchema(SPEAKERS); + expect(repeater).toMatchObject({ type: "array", maxItems: 4, items: { type: "object" } }); + expect(Object.keys((repeater as { items: { properties: object } }).items.properties)).toEqual( + SPEAKERS.fields.map((s) => s.key), + ); + expect( + validateProps( + { type: "object", properties: { v: repeater }, additionalProperties: false }, + { + v: [{ person: G1, role: "host" }], + }, + ), + ).toEqual([]); + }); + + it("checks reference values", () => { + expect(valueIssues(VENUE, G1, "fields/venue")).toEqual([]); + expect(valueIssues(VENUE, "asset_01HZX8K3Q9P1", "fields/venue")).toHaveLength(1); + expect(valueIssues(VENUE, [G1], "fields/venue")).toHaveLength(1); + expect(valueIssues(PARTNERS, [G1, G2], "fields/partners")).toEqual([]); + expect(valueIssues(PARTNERS, [G1, G1], "fields/partners")[0]?.message).toBe( + "each entry at most once", + ); + expect(valueIssues(PARTNERS, [G1, G2, G1, G2], "fields/partners")[0]?.message).toContain( + "at most 3", + ); + }); + + it("checks every repeater item against its sub-fields", () => { + const at = (v: unknown) => valueIssues(SPEAKERS, v, "fields/speakers").map((i) => i.path); + expect(at([{ person: G1, talk: "Hi", day: "2026-11-11", slides: "/s" }])).toEqual([]); + expect(at([{}])).toEqual(["fields/speakers/0"]); + expect(at([{ nope: 1 }])).toEqual(["fields/speakers/0/nope"]); + expect(at([{ talk: "a\u0007b" }])).toEqual(["fields/speakers/0/talk"]); + expect(at([{ day: "2026-02-30" }])).toEqual(["fields/speakers/0/day"]); + expect(at([{ slides: "asset:asset_01HZX8K3Q9P1" }])).toEqual(["fields/speakers/0/slides"]); + expect(at([{ role: "chair" }])).toEqual(["fields/speakers/0/role"]); + expect(at([{ person: null }])).toEqual(["fields/speakers/0/person"]); + expect(at(Array.from({ length: 5 }, () => ({ talk: "x" })))).toEqual(["fields/speakers"]); + // Rich text in an item has rich text's 50 000 limit, not the subset's 10 000. + expect(at([{ abstract: "a".repeat(20_000) }])).toEqual([]); + expect(at([{ abstract: "a".repeat(50_001) }])).toEqual(["fields/speakers/0/abstract"]); + }); + + it("caps references per entry across fields and items", () => { + const many = { + key: "many", + type: "reference", + labels: L("M"), + localized: true, + collections: ["places"], + multiple: true, + maxItems: 50, + } as const satisfies FieldDefinition; + const more = { ...many, key: "more" }; + const big = { ...SPEAKERS, maxItems: 50, fields: [SPEAKERS.fields[0]] } as FieldDefinition; + const ids = (n: number, p: string) => + Array.from({ length: n }, (_, i) => `entry_${p}${String(i).padStart(6, "0")}`); + const values = { many: ids(50, "a"), more: ids(50, "b"), speakers: [{ person: G1 }] }; + expect(codes(checkFieldValues([many, more, big], { values }))).toEqual(["too_many_references"]); + expect( + checkFieldValues([many, more, big], { values: { many: values.many, more: values.more } }), + ).toEqual([]); + expect(FIELD_LIMITS.referencesPerEntry).toBe(100); + }); + + it("needs required sub-fields in every item to publish", () => { + const site = siteWith([SPEAKERS]); + const entry = (status: "draft" | "published") => + ({ + id: "entry_01HZX8K3Q9E1", + collection: "events", + status, + fields: { speakers: [{ person: G1 }, { talk: "x" }] }, + }) as const; + expect(checkRequiredToPublish(site, entry("draft"))).toEqual([]); + expect(checkRequiredToPublish(site, entry("published")).map((i) => i.path)).toEqual([ + "fields/speakers/1/person", + ]); + }); +}); + +describe("resolveFields", () => { + it("resolves group ids and items, and skips types it does not know", () => { + const future = { + key: "geo", + type: "geopoint", + labels: L("Geo"), + localized: true, + } as unknown as FieldDefinition; + const site = siteWith([VENUE, SPEAKERS, future]); + const items = [{ person: G2, role: "host" }]; + const r = resolveFields(site, { + id: "entry_01HZX8K3Q9E1", + collection: "events", + fields: { venue: G1, speakers: items, geo: { lat: 1 } }, + }); + expect(Object.keys(r)).toEqual(["venue", "speakers"]); + expect([r.venue?.value, r.speakers?.value]).toEqual([G1, items]); + const bad = resolveFields(site, { + id: "entry_01HZX8K3Q9E1", + collection: "events", + fields: { speakers: [{}] }, + }); + expect(bad.speakers).toMatchObject({ + value: undefined, + issue: { code: "field_value_unreadable" }, + }); + }); +}); + +/** Every `[path, keyword]` of a schema. */ +function keywords(s: JsonSchema, at = ""): [string, string][] { + const out: [string, string][] = Object.keys(s).map((k) => [at, k]); + for (const [k, c] of Object.entries(s.properties ?? {})) out.push(...keywords(c, `${at}/${k}`)); + if (s.items) out.push(...keywords(s.items, `${at}/items`)); + return out; +} + +describe("fieldsJsonSchema", () => { + const ROLE = SPEAKERS.fields[1]; + const EXAMPLE: FieldDefinition[] = [ + STARTS, + VENUE, + { ...SPEAKERS, maxItems: 20, fields: [SPEAKERS.fields[0], ROLE, SPEAKERS.fields[2]] }, + ]; + const name = (key: string) => ({ places: "Places", people: "People" })[key]; + const open = fieldsJsonSchema(EXAMPLE, "en", "en", { collectionName: name }); + const strict = fieldsJsonSchema(EXAMPLE, "en", "en", { collectionName: name, strict: true }); + + it("emits references as group id strings and repeaters as closed item lists", () => { + const p = open.properties ?? {}; + expect(p.venue).toMatchObject({ type: "string", pattern: "^entry_[A-Za-z0-9]{6,32}$" }); + expect(p.venue?.description).toContain( + "An entry in Places (places) of this site, by the groupId", + ); + expect(p.venue?.description).toContain("every language version shows the targets"); + expect(p.speakers).toMatchObject({ + type: "array", + items: { type: "object", additionalProperties: false }, + }); + expect(p.speakers?.description).toContain("at most 20 items"); + expect(p.speakers?.items?.properties?.person?.description).toContain("People (people)"); + expect(p.speakers?.items?.properties?.person?.description).toContain("Needed to publish."); + expect(p.speakers?.items?.required).toBeUndefined(); + const partners = fieldsJsonSchema([PARTNERS], "en").properties?.partners; + expect(partners).toMatchObject({ type: "array", items: { type: "string" } }); + expect(partners?.description).toContain("at most 3 entries in places or people"); + }); + + it("keeps to the strict-mode keywords, a root object, no $ref and depth 4", () => { + for (const s of [open, strict]) { + for (const [, k] of keywords(s)) expect(JSON_SCHEMA_KEYWORDS).toContain(k); + expect(s.type).toBe("object"); + expect(JSON.stringify(s)).not.toMatch(/\$ref|\$defs|oneOf|anyOf/); + expect(strictSchemaCost(s).depth).toBe(4); + } + }); + + it("makes every property required and nullable in the strict profile", () => { + const p = strict.properties ?? {}; + expect(strict.required).toEqual(["startsAt", "venue", "speakers"]); + expect([p.startsAt?.type, p.venue?.type, p.speakers?.type]).toEqual([ + ["object", "null"], + ["string", "null"], + ["array", "null"], + ]); + // a datetime's own parts stay required and not nullable + expect(p.startsAt?.required).toEqual(["at", "timeZone"]); + expect(p.startsAt?.properties?.at?.type).toBe("string"); + const item = p.speakers?.items; + expect(item?.required).toEqual(["person", "role", "talk"]); + expect(item?.properties?.role?.enum).toEqual(["host", "speaker", "panelist", null]); + expect(item?.description).toContain("null unsets that value in this item"); + expect(strict.description).toContain("null leaves the stored value as it is"); + }); + + it("counts the RFC's example: 8 properties, depth 4, 4 enum values, 6 unions", () => { + expect(strictSchemaCost(strict)).toEqual({ properties: 8, depth: 4, enumValues: 4, unions: 6 }); + expect(strictSchemaCost(open)).toEqual({ properties: 8, depth: 4, enumValues: 3, unions: 0 }); + }); +}); + +type Node = Pick & { + translation?: { group: string }; +}; + +describe("pickMember", () => { + const venueEn: Node = { id: G1, collection: "places", locale: "en" }; + const venueTr: Node = { + id: "entry_01HZX8K3Q9T1", + collection: "places", + locale: "tr", + translation: { group: G1 }, + }; + const relinked: Node = { + id: G2, + collection: "places", + locale: "de", + translation: { group: "entry_01HZX8K3Q9ZZ" }, + }; + const opts = { defaultLocale: "en", collections: ["places"] }; + + it("takes the page's locale, then the default locale under default-with-notice", () => { + expect(pickMember(G1, [venueEn, venueTr], { ...opts, locale: "tr" })?.member).toBe(venueTr); + expect(pickMember(G1, [venueEn], { ...opts, locale: "tr" })).toBeUndefined(); + expect( + pickMember(G1, [venueEn], { ...opts, locale: "tr", fallback: "default-with-notice" }), + ).toEqual({ + member: venueEn, + fromFallbackLocale: true, + }); + }); + + it("counts members by COALESCE(translation.group, id) in allowed collections only", () => { + expect(pickMember(G1, [venueTr], { ...opts, locale: "tr" })?.member).toBe(venueTr); + // a relinked node is no longer a member of its own id's group + expect(pickMember(G2, [relinked], { ...opts, locale: "de" })).toBeUndefined(); + expect( + pickMember(G1, [venueEn], { ...opts, collections: ["halls"], locale: "en" }), + ).toBeUndefined(); + }); +}); + +describe("referenceIssues", () => { + const site = { ...siteWith([VENUE, SPEAKERS]), localization: { fallback: "none" } } as const; + const entry = (id: string, extra: Partial): ContentEntry => + ({ + schemaVersion: 1, + id, + siteId: "site_01HZX8K3Q9T1", + collection: "events", + slug: id.slice(-4).toLowerCase(), + locale: "en", + status: "published", + title: id, + body: { format: "markdown", markdown: "" }, + rev: 1, + createdAt: "2026-11-11T09:00:00Z", + updatedAt: "2026-11-11T09:00:00Z", + ...extra, + }) as ContentEntry; + const place = entry(G1, { collection: "places" }); + const NOW = Date.parse("2026-11-11T10:00:00Z"); + + it("is empty for a site without reference definitions", () => { + expect(referenceIssues({ site: siteWith([STARTS]), entries: [place] }, NOW)).toEqual([]); + }); + + it("names missing, hidden and locale-less targets of shown entries", () => { + const person = entry(G2, { collection: "people", status: "draft" }); + const event = entry("entry_01HZX8K3Q9E1", { + fields: { venue: G1, speakers: [{ person: G2 }, { person: "entry_01HZX8K3Q9NO" }] }, + }); + const turkish = entry("entry_01HZX8K3Q9E2", { + locale: "tr", + fields: {}, + translation: { + group: event.id, + method: "human", + source: { id: event.id, locale: "en", rev: 1, hash: "h".repeat(64) }, + }, + }); + const draft = entry("entry_01HZX8K3Q9E3", { + status: "draft", + fields: { venue: "entry_01HZX8K3Q9NO" }, + }); + const issues = referenceIssues({ site, entries: [place, person, event, turkish, draft] }, NOW); + expect(issues.map((i) => [i.entryId.slice(-2), i.path, i.reason])).toEqual([ + ["E1", "fields/speakers/0/person", "hidden"], + ["E1", "fields/speakers/1/person", "missing"], + // the Turkish version inherits the shared values and resolves them in Turkish + ["E2", "fields/venue", "locale"], + ["E2", "fields/speakers/0/person", "hidden"], + ["E2", "fields/speakers/1/person", "missing"], + ]); + const fallback = { ...site, localization: { fallback: "default-with-notice" as const } }; + expect( + referenceIssues({ site: fallback, entries: [place, event, turkish] }, NOW).some( + (i) => i.path === "fields/venue", + ), + ).toBe(false); + }); +}); diff --git a/packages/mcp-server/src/collection-fields.ts b/packages/mcp-server/src/collection-fields.ts index 0dee89c8..da0f6f69 100644 --- a/packages/mcp-server/src/collection-fields.ts +++ b/packages/mcp-server/src/collection-fields.ts @@ -6,19 +6,36 @@ */ import type { CollectionSettings, FieldDefinition, Site } from "@cloudhal/core-contracts"; import { SiteSchema } from "@cloudhal/core-contracts"; -import { fieldLabel, fieldsJsonSchema } from "@cloudhal/core-ts/fields"; +import { fieldLabel, fieldsJsonSchema, isReferenceFieldType } from "@cloudhal/core-ts/fields"; import { type JsonSchema, quote } from "./common.ts"; +import { defsClosure } from "./schema-transforms.ts"; const DEFS = SiteSchema.$defs as Record; const VARIANTS = (DEFS.FieldDefinition as JsonSchema).oneOf as JsonSchema[]; +/** A variant's `type` value (`{ enum: ["text"] }`). */ +const typeOf = (variant: JsonSchema): string => + ((variant.properties as Record).type?.enum ?? [])[0] ?? ""; + /** A definition variant without `migration`: only field operations write the marker. */ function withoutMarker(variant: JsonSchema): JsonSchema { const { migration: _m, ...properties } = variant.properties as Record; return { ...variant, properties }; } -/** `update_collection`'s `fields`: the contract's definitions, minus the operation marker. */ +/** + * The definitions `update_collection` takes: the contract's variants of the enabled types (RFC-0045's + * `reference` and `repeater` join at rollout step 7), each without the operation marker. + */ +const FIELD_DEFINITION: JsonSchema = { + ...DEFS.FieldDefinition, + oneOf: VARIANTS.filter((v) => !isReferenceFieldType(typeOf(v))).map(withoutMarker), +}; + +/** + * `update_collection`'s `fields`: the enabled definitions, minus the operation marker, with every + * `$defs` entry they reach (`defsClosure`), so a contract change cannot leave a `$ref` dangling. + */ export const FIELD_DEFINITIONS_INPUT: JsonSchema = { type: "array", maxItems: 30, @@ -26,16 +43,8 @@ export const FIELD_DEFINITIONS_INPUT: JsonSchema = { "The collection's typed custom fields, the whole list in form order ([] removes them all). Free edits only: add fields, reorder, change labels and help, add select options or change their labels, toggle multiline, searchable or requiredToPublish, change maxLength/minimum/maximum. Renaming a key, changing a type, localized or multiple, and dropping a key or option still in use need rename_field, change_field_type, set_field_localized or remove_field.", items: { $ref: "#/$defs/FieldDefinition" }, $defs: { - FieldDefinition: { - ...DEFS.FieldDefinition, - oneOf: VARIANTS.map(withoutMarker), - }, - FieldKey: DEFS.FieldKey, - FieldLabel: DEFS.FieldLabel, - FieldLabels: DEFS.FieldLabels, - FieldOption: DEFS.FieldOption, - Slug: DEFS.Slug, - Locale: DEFS.Locale, + ...defsClosure(SiteSchema as JsonSchema, FIELD_DEFINITION), + FieldDefinition: FIELD_DEFINITION, }, }; diff --git a/packages/mcp-server/src/field-hints.ts b/packages/mcp-server/src/field-hints.ts index c19d6470..8f7ad0a3 100644 --- a/packages/mcp-server/src/field-hints.ts +++ b/packages/mcp-server/src/field-hints.ts @@ -37,6 +37,13 @@ const HINTS: Partial> = { "fix the mappings at the paths in details.issues: list_collections shows a preset's paths, which are required and the sources each takes (structuredData.paths); field sources need a field of a fitting type, and required values come from the entry, never a constant", invalid_expiry: "expiry.field must be a date or datetime field of the collection (list_collections → fields)", + // RFC-0045 (references and repeaters; refused until they are enabled) + fields_unsupported: + "reference and repeater fields are not available on this server yet: use the nine field types list_collections shows (a text field for now), and leave the rest of the change as it is", + reference_target_unknown: + "a reference field's collections must be collections of this site (list_collections)", + too_many_references: + "an entry holds at most 100 references across its fields and repeater items; remove some", list_field_in_use: "contentList sections on the pages in details.pages still filter or sort by that field: change their where/orderBy first (update_page), then try again", }; diff --git a/packages/mcp-server/src/schema-transforms.ts b/packages/mcp-server/src/schema-transforms.ts index 49821f3c..ada965e1 100644 --- a/packages/mcp-server/src/schema-transforms.ts +++ b/packages/mcp-server/src/schema-transforms.ts @@ -30,6 +30,44 @@ export function hoistDefs(schema: JsonSchema): JsonSchema { : root; } +const LOCAL_REF = "#/$defs/"; + +/** The `$defs` names `node` refers to directly (`#/$defs/` only). */ +function refsOf(node: unknown, out: Set): Set { + if (Array.isArray(node)) for (const n of node) refsOf(n, out); + else if (node && typeof node === "object") + for (const [k, v] of Object.entries(node as Record)) { + if (k === "$ref" && typeof v === "string" && v.startsWith(LOCAL_REF)) + out.add(v.slice(LOCAL_REF.length)); + else refsOf(v, out); + } + return out; +} + +/** + * RFC-0045 §6: the `$defs` of `schema` that `from` reaches, transitively through `#/$defs/*` + * references: a `$defs` name (included itself) or a schema node of its own (not included). A tool + * that embeds part of a contract carries exactly these, so no `$ref` dangles when a contract + * change adds one (`hoistDefs` only hoists nested `$defs` and finds no missing ones). Throws on a + * reference `schema` does not define. + */ +export function defsClosure( + schema: JsonSchema, + from: string | JsonSchema, +): Record { + const defs = (schema.$defs ?? {}) as Record; + const out: Record = {}; + const queue = typeof from === "string" ? [from] : [...refsOf(from, new Set())]; + for (let name = queue.shift(); name !== undefined; name = queue.shift()) { + if (Object.hasOwn(out, name)) continue; + const def = defs[name]; + if (!def) throw new Error(`$defs has no ${name}`); + out[name] = def; + for (const next of refsOf(def, new Set())) if (!Object.hasOwn(out, next)) queue.push(next); + } + return out; +} + /** * Inputs may name an image by asset alone (`{ assetId, alt? }`, RFC-0004 §2.2): the store writes * `src`, `width` and `height`. So an input ImageRef needs `src` or `assetId`, and `alt` may be left diff --git a/packages/mcp-server/test/tool-schemas.test.ts b/packages/mcp-server/test/tool-schemas.test.ts new file mode 100644 index 00000000..2c461235 --- /dev/null +++ b/packages/mcp-server/test/tool-schemas.test.ts @@ -0,0 +1,146 @@ +/** + * RFC-0045 §6 and rollout step 1: every `$ref` in every tool's input and output schema resolves + * (`defsClosure` replaced `update_collection`'s hand-written `$defs`), `update_collection` lists + * only the enabled field types, and a reference or repeater definition is refused through it. + */ +import { AuditLog, PolicyEngine } from "@cloudhal/agent-runtime"; +import { SiteSchema } from "@cloudhal/core-contracts"; +import { SiteGraphStore, seedStarterSite } from "@cloudhal/core-ts"; +import { MemoryBlobStore, migrate, migrations } from "@cloudhal/storage"; +import { BetterSqlite3Driver } from "@cloudhal/storage/sqlite"; +import { describe, expect, it } from "vitest"; +import { FIELD_DEFINITIONS_INPUT } from "../src/collection-fields.ts"; +import type { JsonSchema } from "../src/common.ts"; +import { defsClosure } from "../src/schema-transforms.ts"; +import { defineTools, runTool, toolPolicies } from "../src/tools.ts"; + +const tools = defineTools(); + +/** Every `$ref` under `node`, with where it sits. */ +function refs(node: unknown, at = "", out: [string, string][] = []): [string, string][] { + if (Array.isArray(node)) + node.forEach((n, i) => { + refs(n, `${at}/${i}`, out); + }); + else if (node && typeof node === "object") + for (const [k, v] of Object.entries(node as Record)) + if (k === "$ref" && typeof v === "string") out.push([at, v]); + else refs(v, `${at}/${k}`, out); + return out; +} + +/** The node a local JSON pointer (`#/$defs/X`, `#/properties/a`) names in `root`, if any. */ +function resolve(root: unknown, ref: string): unknown { + if (!ref.startsWith("#")) return undefined; + let node: unknown = root; + for (const raw of ref.slice(2).split("/").filter(Boolean)) { + const key = raw.replaceAll("~1", "/").replaceAll("~0", "~"); + if (!node || typeof node !== "object" || !Object.hasOwn(node, key)) return undefined; + node = (node as Record)[key]; + } + return node; +} + +describe("tool schemas", () => { + it("resolve every $ref in every tool's input and output schema", () => { + const dangling: string[] = []; + for (const tool of tools) + for (const [which, schema] of [ + ["input", tool.inputSchema], + ["output", tool.outputSchema], + ] as const) + for (const [at, ref] of refs(schema)) + if (resolve(schema, ref) === undefined) + dangling.push(`${tool.name} ${which}${at}: ${ref}`); + expect(dangling).toEqual([]); + }); + + it("offers update_collection the enabled field types only", () => { + const defs = FIELD_DEFINITIONS_INPUT.$defs as Record; + const variants = (defs.FieldDefinition?.oneOf ?? []) as JsonSchema[]; + const types = variants.map( + (v) => (v.properties as Record).type?.enum[0], + ); + expect(types).toEqual([ + "text", + "richText", + "number", + "boolean", + "date", + "datetime", + "url", + "image", + "select", + ]); + // no marker, and nothing the stage-4 variants alone need + expect(variants.every((v) => !("migration" in (v.properties as object)))).toBe(true); + expect(Object.keys(defs).sort()).toEqual([ + "FieldDefinition", + "FieldKey", + "FieldLabel", + "FieldLabels", + "FieldOption", + "Locale", + "Slug", + ]); + }); +}); + +describe("defsClosure", () => { + const site = SiteSchema as unknown as JsonSchema; + + it("follows $refs transitively from a $defs name or a node", () => { + const closure = defsClosure(site, "FieldDefinition"); + for (const name of [ + "FieldDefinition", + "SubFieldDefinition", + "FieldCondition", + "FieldMigration", + "Id", + ]) + expect(Object.keys(closure)).toContain(name); + expect( + Object.keys(defsClosure(site, { items: { $ref: "#/$defs/FieldLabels" } })).sort(), + ).toEqual(["FieldLabel", "FieldLabels", "Locale"]); + }); + + it("throws on a reference the schema does not define", () => { + expect(() => defsClosure(site, { $ref: "#/$defs/Nope" })).toThrow(/Nope/); + }); +}); + +describe("update_collection and the RFC-0045 gate", () => { + it("refuses a reference or repeater definition with fields_unsupported", async () => { + const sql = await BetterSqlite3Driver.open(":memory:"); + await migrate(sql, migrations); + const store = new SiteGraphStore({ sql, blobs: new MemoryBlobStore() }); + const site = await seedStarterSite(store, { name: "Gate", slug: "gate" }); + await store.updateSite({ ...site, collections: { places: {}, events: {} } }); + const tool = tools.find((t) => t.name === "update_collection") as (typeof tools)[number]; + const agent = { kind: "agent" as const, id: "agent_gate0001", name: "Organiser" }; + const label = { en: { label: "Venue" } }; + for (const def of [ + { key: "venue", type: "reference", labels: label, localized: false, collections: ["places"] }, + { + key: "speakers", + type: "repeater", + labels: label, + localized: false, + fields: [{ key: "name", type: "text", labels: label }], + }, + ]) { + const call = runTool( + tool, + { siteId: site.id, collection: "events", fields: [def] }, + { store, principal: { actor: agent, scopes: ["*"], mode: "edits" } }, + { + policy: new PolicyEngine(toolPolicies(tools)), + audit: new AuditLog(sql), + confirmed: true, + }, + ); + await expect(call).rejects.toMatchObject({ code: "fields_unsupported" }); + } + expect((await store.getSite(site.id))?.collections?.events?.fields).toBeUndefined(); + }); +}); diff --git a/packages/ui/src/blocks/EntryFieldsForm.tsx b/packages/ui/src/blocks/EntryFieldsForm.tsx index 91c9d1e3..9481c3e6 100644 --- a/packages/ui/src/blocks/EntryFieldsForm.tsx +++ b/packages/ui/src/blocks/EntryFieldsForm.tsx @@ -326,6 +326,24 @@ function ImageControl(c: Ctl) { ); } +/** + * RFC-0045: reference and repeater fields until their blocks ship (rollout step 6). Both types are + * refused on every write path until then, so this only shows a stored value read-only, as JSON, + * and never writes. + */ +function StoredValueControl({ label, description, error, value }: Ctl) { + return ( + + ); +} + const CONTROLS: Record ReactNode> = { text: TextControl, url: TextControl, @@ -336,6 +354,8 @@ const CONTROLS: Record ReactNode> = { datetime: DateControl, image: ImageControl, select: SelectControl, + reference: StoredValueControl, + repeater: StoredValueControl, }; /** Placeholders only fit controls that show text; the rest say it under the control. */ diff --git a/packages/ui/test/entry-fields-form.test.tsx b/packages/ui/test/entry-fields-form.test.tsx index 4c9d579e..6022c2b6 100644 --- a/packages/ui/test/entry-fields-form.test.tsx +++ b/packages/ui/test/entry-fields-form.test.tsx @@ -208,6 +208,43 @@ describe("EntryFieldsForm", () => { "https://x.test", ); }); + + it("shows RFC-0045 reference and repeater values read-only until their blocks ship", () => { + const onChange = vi.fn(); + const defs: FieldDefinition[] = [ + { + key: "host", + type: "reference", + labels: { en: { label: "Host" } }, + localized: false, + collections: ["people"], + }, + { + key: "agenda", + type: "repeater", + labels: { en: { label: "Agenda" } }, + localized: false, + fields: [{ key: "talk", type: "text", labels: { en: { label: "Talk" } } }], + }, + ]; + render( + z} + labels={labels} + />, + ); + const host = screen.getByRole("textbox", { name: "Host" }); + expect(host).toBeDisabled(); + expect(host).toHaveValue('"entry_01HZX8K3Q9P1"'); + expect(screen.getByRole("textbox", { name: "Agenda" })).toHaveValue('[{"talk":"Hi"}]'); + expect(onChange).not.toHaveBeenCalled(); + }); }); describe("entry fields model", () => { From c3b0b6585ece2fb9d3133955378080f2028da562 Mon Sep 17 00:00:00 2001 From: Madraka <8898640+Madraka@users.noreply.github.com> Date: Wed, 11 Nov 2026 04:17:37 +0300 Subject: [PATCH 3/4] fix(core-ts): gate snapshot site bodies, keep push's 409, own-key props (PR #203 review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - assertFieldTypes(siteId, bundle, support) now checks the working site row and every site body a snapshot ref names in bundle.blobs, so a clean working copy cannot carry a reference or repeater definition (or a marker naming one) into a snapshot a publish or rollback could make live. Entry values need no own check: they mean nothing without a definition. - The admin bundle route keeps fields_unsupported (409 with details.issues) instead of turning it into 400 bad_request. - validateProps checks own keys (Object.hasOwn), so constructor, toString or __proto__ are unknown props; a repeater item counts as empty unless it holds a declared sub-field. - A reference's where refuses now only on date and datetime fields; on select, text and url it is a literal. - The fields_unsupported MCP hint names update_collection's input and the enabled types, interpolated from FIELD_TYPES. - The list fingerprint (RFC-0042 §4) leaves out RFC-0045's where and itemLabel, which never change a resolved value; collections, multiple, maxItems and fields count. Its schema-walking test covers the eleven types. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com> --- .changeset/reference-repeater-contract.md | 3 +- packages/api/src/routes/admin/sync.ts | 17 ++++-- packages/api/test/admin.test.ts | 56 ++++++++++++++++++- packages/core-ts/src/blocks.ts | 4 +- packages/core-ts/src/fields/value-checks.ts | 3 +- .../core-ts/src/list-values/fingerprint.ts | 15 ++++- .../core-ts/src/references/definitions.ts | 8 ++- packages/core-ts/src/sync-checks.ts | 46 ++++++++++++--- .../core-ts/src/testing-reference-gate.ts | 53 +++++++++++++++++- .../core-ts/test/list-values-rows.test.ts | 7 ++- packages/core-ts/test/references-core.test.ts | 22 +++++++- packages/mcp-server/src/field-hints.ts | 4 +- packages/mcp-server/test/tool-schemas.test.ts | 8 +++ 13 files changed, 215 insertions(+), 31 deletions(-) diff --git a/.changeset/reference-repeater-contract.md b/.changeset/reference-repeater-contract.md index 63e67108..e782b89d 100644 --- a/.changeset/reference-repeater-contract.md +++ b/.changeset/reference-repeater-contract.md @@ -13,7 +13,7 @@ The contract gains two field types for collections, `reference` and `repeater` ( - field operations; - every bundle `importSite` takes: import, pull, push, `cloudhal import --replace` and `--revert`, snapshot restores and edition copies. - `checkBundle` gained `assertFieldTypes`, which refuses the types when no capability is given. + `checkBundle` gained `assertFieldTypes`. It checks the working site row and every site body the bundle's snapshots carry, and refuses the types when no capability is given. A push answers `409 fields_unsupported` with its issues, not `400 bad_request`. - **Pure core** (`@cloudhal/core-ts/fields`), with no effect on stored data yet: - the definition rules: `reference_target_unknown`, `where`, `itemLabel`, and caps on sub-fields and leaves; - value checks for both types, the per-item `requiredToPublish`, and `too_many_references`; @@ -23,3 +23,4 @@ The contract gains two field types for collections, `reference` and `repeater` ( `FIELD_TYPES` keeps the nine enabled types (`EnabledFieldType`). `ALL_FIELD_TYPES` lists all eleven. - **MCP.** `update_collection`'s input now builds its `$defs` with `defsClosure` and offers only the enabled types. A test checks that every `$ref` in every tool's input and output schema resolves. Before this change, `pnpm gen` alone would have left the input pointing at `$defs` it did not carry. +- **Fix, existing sites:** block props and typed-field objects no longer accept keys named after `Object.prototype` members (`constructor`, `toString`, `__proto__`, …) as known props. The props interpreter (`validateProps`) now checks own keys only, so such keys are refused as unknown. diff --git a/packages/api/src/routes/admin/sync.ts b/packages/api/src/routes/admin/sync.ts index 5ca2d6c6..e6afcc01 100644 --- a/packages/api/src/routes/admin/sync.ts +++ b/packages/api/src/routes/admin/sync.ts @@ -24,6 +24,16 @@ import { OPERATOR_ACTOR } from "./shared.ts"; const MAX_MEDIA_PUT = 100 * 1024 * 1024; /** Bundle export/import, primary domain, media blobs. */ +/** + * Refusals a push answers with their own code and details instead of 400 bad_request (RFC-0008: + * 409 gates_unsupported, 400 invalid_availability; RFC-0045: 409 fields_unsupported with its issues). + */ +const KEPT_CODES: ReadonlySet = new Set([ + "gates_unsupported", + "invalid_availability", + "fields_unsupported", +]); + export function syncAdminRoutes(deps: AdminDeps): Hono { const { storage } = deps; return new Hono() @@ -144,12 +154,7 @@ export function syncAdminRoutes(deps: AdminDeps): Hono { { error: "conflict", message: e.message, details: { differing: e.differing } }, 409, ); - // RFC-0008: refused gates keep their code (409 gates_unsupported, 400 invalid_availability). - if ( - e instanceof InvariantError && - (e.code === "gates_unsupported" || e.code === "invalid_availability") - ) - throw e; + if (e instanceof InvariantError && KEPT_CODES.has(e.code)) throw e; if (e instanceof Error) throw badRequest(e.message); throw e; } diff --git a/packages/api/test/admin.test.ts b/packages/api/test/admin.test.ts index 2979a89a..9cbedb2a 100644 --- a/packages/api/test/admin.test.ts +++ b/packages/api/test/admin.test.ts @@ -1,5 +1,12 @@ /** `cloudhal push`/`pull` endpoints and the /mcp origin guard. */ -import { SiteGraphStore, seedStarterSite } from "@cloudhal/core-ts"; +import type { Site } from "@cloudhal/core-contracts"; +import { + canonicalJson, + exportSite, + nodeHash, + SiteGraphStore, + seedStarterSite, +} from "@cloudhal/core-ts"; import { makePng } from "@cloudhal/core-ts/media/fixtures"; import { MemoryBlobStore } from "@cloudhal/storage"; import { BetterSqlite3Driver } from "@cloudhal/storage/sqlite"; @@ -269,6 +276,53 @@ describe("admin bundle endpoints", () => { expect((await put(bundle.replace(site.id, "site_other0000001"))).status).toBe(400); expect((await put(bundle, "?mode=yolo")).status).toBe(400); }); + + it("refuses a pushed reference or repeater definition with 409 fields_unsupported (RFC-0045)", async () => { + const a = await backend(); + const b = await backend(); + const site = await seedStarterSite(a.store, { name: "Pushed", slug: "pushed" }); + const app = createApi({ store: b.store, admin: { deployToken: TOKEN, storage: b } }); + const bundle = await exportSite(a, site.id); + const row = bundle.nodes.find((n) => n.kind === "site"); + if (!row) throw new Error("no site row"); + const label = { en: { label: "Venue" } }; + const body = { + ...(JSON.parse(row.body) as Site), + collections: { + places: {}, + events: { + fields: [ + { + key: "venue", + type: "reference", + labels: label, + localized: false, + collections: ["places"], + }, + ], + }, + }, + } as Site; + row.body = canonicalJson(body); + row.hash = await nodeHash(body); + for (const query of ["", "?mode=replace"]) { + const res = await app.request(`/api/admin/sites/${site.id}/bundle${query}`, { + method: "PUT", + headers: { authorization: `Bearer ${TOKEN}`, "content-type": "application/json" }, + body: JSON.stringify(bundle), + }); + expect(res.status, query).toBe(409); + const answer = (await res.json()) as { + error: string; + details: { issues: { path: string }[] }; + }; + expect(answer.error).toBe("fields_unsupported"); + expect(answer.details.issues.map((i) => i.path)).toEqual([ + "collections/events/fields/0/type", + ]); + } + expect(await b.store.getSite(site.id)).toBeUndefined(); + }); }); describe("short deploy token", () => { diff --git a/packages/core-ts/src/blocks.ts b/packages/core-ts/src/blocks.ts index c0f7efd4..c50a310f 100644 --- a/packages/core-ts/src/blocks.ts +++ b/packages/core-ts/src/blocks.ts @@ -454,8 +454,10 @@ function validateObject( return; } const at = (key: string) => (path ? `${path}/${key}` : key); + // Own keys only: `constructor`, `toString` or `__proto__` are not props of a block. for (const key of Object.keys(v)) - if (!(key in s.properties)) out.push({ path: at(key), message: "not a prop of this block" }); + if (!Object.hasOwn(s.properties, key)) + out.push({ path: at(key), message: "not a prop of this block" }); for (const key of s.required ?? []) if (v[key] === undefined) out.push({ path: at(key), message: "required" }); for (const [key, child] of Object.entries(s.properties)) { diff --git a/packages/core-ts/src/fields/value-checks.ts b/packages/core-ts/src/fields/value-checks.ts index 72647ce1..d1c9d42a 100644 --- a/packages/core-ts/src/fields/value-checks.ts +++ b/packages/core-ts/src/fields/value-checks.ts @@ -136,7 +136,8 @@ const EXTRA: Partial> = { if (def.type !== "repeater" || !Array.isArray(value)) return []; const out: FieldIssue[] = []; (value as Record[]).forEach((item, i) => { - if (Object.keys(item).length === 0) out.push(issue(`${path}/${i}`, "an item needs a value")); + if (!def.fields.some((sub) => Object.hasOwn(item, sub.key) && item[sub.key] !== undefined)) + out.push(issue(`${path}/${i}`, "an item needs a value")); for (const sub of def.fields) { const v = item[sub.key]; const extra = EXTRA[sub.type]; diff --git a/packages/core-ts/src/list-values/fingerprint.ts b/packages/core-ts/src/list-values/fingerprint.ts index 99efcfb1..4b94d668 100644 --- a/packages/core-ts/src/list-values/fingerprint.ts +++ b/packages/core-ts/src/list-values/fingerprint.ts @@ -19,10 +19,19 @@ export const NO_LIST = "none"; /** * Definition properties left out of the fingerprint: they never change a resolved value (labels, - * help, publish rules, site search). Every other property counts, including future ones (a unit - * test walks the schema and fails on a property that is neither here nor fingerprinted). + * help, publish rules, site search; RFC-0045's `where`, which narrows the picker and is never + * re-checked, and `itemLabel`, which only names items in Studio). Every other property counts, + * including future ones (a unit test walks the schema and fails on a property that is neither here + * nor fingerprinted). A reference's `collections`, `multiple` and `maxItems` and a repeater's + * `fields` count: they decide which stored values still read. */ -export const EXCLUDED: readonly string[] = ["labels", "requiredToPublish", "searchable"]; +export const EXCLUDED: readonly string[] = [ + "labels", + "requiredToPublish", + "searchable", + "where", + "itemLabel", +]; /** One listed definition as fingerprinted: options keep their values, not their labels. */ function shape(def: FieldDefinition): Record { diff --git a/packages/core-ts/src/references/definitions.ts b/packages/core-ts/src/references/definitions.ts index b804cd82..aa7fed94 100644 --- a/packages/core-ts/src/references/definitions.ts +++ b/packages/core-ts/src/references/definitions.ts @@ -118,14 +118,18 @@ function whereIssues( const [target, ...more] = def.collections; if (more.length > 0 || target === undefined) return [issue(`${at}/where`, "where needs exactly one target collection")]; + const settings = collections[target]; + const types = new Map((settings?.fields ?? []).map((d) => [d.key, d.type])); const out: FieldIssue[] = []; where.forEach((c, i) => { - if (c.value === "now" || c.values?.includes("now")) + // `now` is the clock only on date and datetime fields; elsewhere it is a literal value. + const type = types.get(c.field); + const clock = type === "date" || type === "datetime"; + if (clock && (c.value === "now" || c.values?.includes("now"))) out.push( issue(`${at}/where/${i}`, "where cannot use now: it is checked when a target is chosen"), ); }); - const settings = collections[target]; for (const q of checkFieldQuery(settings?.fields ?? [], settings?.listFields, { where })) out.push(issue(`${at}/${q.path}`, q.message)); return out; diff --git a/packages/core-ts/src/sync-checks.ts b/packages/core-ts/src/sync-checks.ts index d7e9cc92..df83ac1d 100644 --- a/packages/core-ts/src/sync-checks.ts +++ b/packages/core-ts/src/sync-checks.ts @@ -254,18 +254,48 @@ export async function assertIdsFree(to: StoragePair, bundle: SiteBundle): Promis } /** - * RFC-0045 §1: a bundle whose site defines a reference or repeater field (or a marker converting - * one) is refused (`fields_unsupported`) unless `support` says the site's renderer reads them. - * Without a capability the types are refused, unlike `assertGates`. One check for `importSite`, - * so for pull, push, `cloudhal import --replace` and `--revert`, and snapshot restores. + * The site bodies a bundle would write: its working site row, and every blob a snapshot ref of + * kind `site` names (a publish or rollback can make any of them live). Blobs that do not parse are + * left to `bodiesToWrite`, which refuses them. + */ +function bundleSiteBodies(bundle: SiteBundle, site: NodeRow): { at: string; body: unknown }[] { + const out: { at: string; body: unknown }[] = [{ at: "site", body: JSON.parse(site.body) }]; + const hashes = new Set(); + for (const row of bundle.snapshots) + for (const ref of snapshotRefs(row)) if (ref.kind === "site") hashes.add(ref.hash); + for (const hash of [...hashes].sort()) { + const text = bundle.blobs[hash]; + if (text === undefined) continue; + try { + const body: unknown = JSON.parse(text); + if (typeof body === "object" && body !== null) out.push({ at: `blobs/${hash}`, body }); + } catch { + // not JSON: bodiesToWrite refuses it without quoting it + } + } + return out; +} + +/** + * RFC-0045 §1: a bundle whose site, working copy or any snapshot's, defines a reference or + * repeater field (or a marker converting one) is refused (`fields_unsupported`) unless `support` + * says the site's renderer reads them. Without a capability the types are refused, unlike + * `assertGates`. One check for `importSite`, so for pull, push, `cloudhal import --replace` and + * `--revert`, and snapshot restores. Entry values need no check of their own: they mean nothing + * without a definition, and every site body that could define one is checked here. */ export async function assertFieldTypes( siteId: string, - site: NodeRow, + bundle: SiteBundle, support: FieldTypeSupport | undefined, ): Promise { - const body = JSON.parse(site.body) as Parameters[0]; - const issues = unsupportedFieldTypeIssues(body); + const site = bundle.nodes.find((n) => n.kind === "site" && n.id === siteId); + if (!site) return; + const issues = bundleSiteBodies(bundle, site).flatMap(({ at, body }) => + unsupportedFieldTypeIssues(body as Parameters[0]).map((i) => + at === "site" ? i : { ...i, path: `${at}/${i.path}` }, + ), + ); if (issues.length === 0 || (await referenceTypesSupported(siteId, support))) return; refuseFieldIssues(issues, { siteId }); } @@ -294,7 +324,7 @@ export async function checkBundle( await assertValidRows(siteId, bundle); assertDistinctRows(bundle); await assertGates(siteId, bundle, gates); - await assertFieldTypes(siteId, siteRow, fieldTypes); + await assertFieldTypes(siteId, bundle, fieldTypes); return siteRow; } diff --git a/packages/core-ts/src/testing-reference-gate.ts b/packages/core-ts/src/testing-reference-gate.ts index bdf3c07e..ec557181 100644 --- a/packages/core-ts/src/testing-reference-gate.ts +++ b/packages/core-ts/src/testing-reference-gate.ts @@ -5,7 +5,7 @@ * push, `cloudhal import --replace` and `--revert`, snapshot restores and edition copies all go * through its `checkBundle`). Called by `describeSiteGraphStore`, so it runs on every backend. */ -import type { Actor, FieldDefinition, Site } from "@cloudhal/core-contracts"; +import type { Actor, FieldDefinition, NodeRef, Site } from "@cloudhal/core-contracts"; import { describe, expect, it } from "vitest"; import { InvariantError } from "./errors.ts"; import { runFieldOperation } from "./fields-operations/index.ts"; @@ -59,6 +59,30 @@ async function withCollections(bundle: SiteBundle, collections: Site["collection return copy; } +/** + * `bundle` with a clean working copy, whose snapshot `snapshotId` names a site body of the same id + * and rev with `collections` replaced: what a hand-made archive or a newer store could send. + */ +async function withSnapshotSite( + bundle: SiteBundle, + snapshotId: string, + collections: Site["collections"], +) { + const copy = structuredClone(bundle); + const row = copy.snapshots.find((s) => s.id === snapshotId); + if (!row) throw new Error("no snapshot row"); + const snapshot = JSON.parse(row.body) as { nodes: NodeRef[] }; + const ref = snapshot.nodes.find((r) => r.kind === "site"); + const text = ref ? copy.blobs[ref.hash] : undefined; + if (!ref || text === undefined) throw new Error("no site blob"); + const body = { ...(JSON.parse(text) as Site), collections }; + const hash = await nodeHash(body); + copy.blobs[hash] = canonicalJson(body); + ref.hash = hash; + row.body = canonicalJson(snapshot); + return copy; +} + export function referenceGateSuite(ctx: SuiteContext): void { describe("RFC-0045 step 1: reference and repeater types are refused on every write path", () => { let n = 0; @@ -118,7 +142,7 @@ export function referenceGateSuite(ctx: SuiteContext): void { expect((await store().getSite(site.id))?.collections).toEqual(site.collections); }); - it("refuses bundles that define them: merge and replace imports alike", async () => { + it("refuses bundles that define them: bootstrap and replace imports alike", async () => { const site = await setup(); const bundle = await exportSite(storage(), site.id); for (const def of [VENUE, SPEAKERS]) { @@ -140,5 +164,30 @@ export function referenceGateSuite(ctx: SuiteContext): void { // the untouched bundle still imports expect((await importSite(storage(), bundle)).nodes.unchanged).toBe(true); }); + + it("refuses bundles whose snapshots' site bodies define them, the working copy clean", async () => { + const site = await setup(); + const snap = await store().createSnapshot(site.id, { actor: person, reason: "manual" }); + const bundle = await exportSite(storage(), site.id); + for (const collections of [ + events([TEXT, VENUE]), + events([TEXT, SPEAKERS]), + events([{ ...TEXT, migration: { id: snap.id, fromType: "reference" } }]), + events([{ ...TEXT, migration: { id: snap.id, sub: "name" } }]), + ]) { + const tainted = await withSnapshotSite(bundle, snap.id, collections); + for (const mode of ["bootstrap", "replace"] as const) + expect(await code(importSite(storage(), tainted, { mode })), mode).toBe( + "fields_unsupported", + ); + } + // the same rewrite with clean definitions passes the gate (and fails later, elsewhere) + const clean = await withSnapshotSite(bundle, snap.id, events([TEXT])); + const later = await importSite(storage(), clean, { mode: "replace" }).then( + () => "", + (e: unknown) => (e instanceof InvariantError ? e.code : "other"), + ); + expect(later).not.toBe("fields_unsupported"); + }); }); } diff --git a/packages/core-ts/test/list-values-rows.test.ts b/packages/core-ts/test/list-values-rows.test.ts index 339678b7..c2d8d56e 100644 --- a/packages/core-ts/test/list-values-rows.test.ts +++ b/packages/core-ts/test/list-values-rows.test.ts @@ -207,7 +207,8 @@ describe("list fingerprint (RFC-0042 §4)", () => { $defs: { FieldDefinition: { oneOf: { properties: Record }[] } }; }; const variants = schema.$defs.FieldDefinition.oneOf; - expect(variants.length).toBe(9); + // the nine RFC-0041 types and RFC-0045's reference and repeater + expect(variants.length).toBe(11); const sentinel = (name: string, type: string | undefined): unknown => { if (name === "migration") return { id: "op_1", fromLocalized: true }; if (name === "options") return [{ value: "z", labels: { en: "Z" } }]; @@ -222,6 +223,10 @@ describe("list fingerprint (RFC-0042 §4)", () => { key: "a", type, ...(type === "select" ? { options: [{ value: "q", labels: { en: "Q" } }] } : {}), + ...(type === "reference" ? { collections: ["places"] } : {}), + ...(type === "repeater" + ? { fields: [{ key: "s", type: "text", labels: { en: { label: "S" } } }] } + : {}), } as never); const before = await listFingerprint(site([base], ["a"]), "c"); for (const [name, p] of Object.entries(props)) { diff --git a/packages/core-ts/test/references-core.test.ts b/packages/core-ts/test/references-core.test.ts index 6eb60c8c..77b58a9d 100644 --- a/packages/core-ts/test/references-core.test.ts +++ b/packages/core-ts/test/references-core.test.ts @@ -94,10 +94,14 @@ const siteWith = (events: FieldDefinition[], more: Collections = {}) => ({ type: "select", labels: L("Team"), localized: false, - options: [{ value: "core", labels: { en: "Core" } }], + options: [ + { value: "core", labels: { en: "Core" } }, + { value: "now", labels: { en: "Now" } }, + ], }, + { key: "joined", type: "date", labels: L("Joined"), localized: false }, ], - listFields: ["team"], + listFields: ["team", "joined"], }, ...more, } as unknown as Collections, @@ -169,7 +173,13 @@ describe("definition rules (with the capability)", () => { expect(where({ field: "nope", op: "eq", value: "x" })[0]?.path).toBe( "collections/events/fields/0/where/0/field", ); - expect(codes(where({ field: "team", op: "eq", value: "now" }))).toContain("invalid_query"); + // `now` is refused as the clock on a date field, and is a plain option value on a select + expect(where({ field: "joined", op: "lt", value: "now" }).map((i) => [i.path, i.code])).toEqual( + [["collections/events/fields/0/where/0", "invalid_query"]], + ); + expect(where({ field: "team", op: "eq", value: "now" })).toEqual([]); + expect(where({ field: "team", op: "in", values: ["core", "now"] })).toEqual([]); + expect(where({ field: "joined", op: "lt", value: "2026-01-01" })).toEqual([]); }); it("refuses maxItems without multiple, bad sub-fields and a bad itemLabel", () => { @@ -254,6 +264,12 @@ describe("values", () => { expect(at([{ person: G1, talk: "Hi", day: "2026-11-11", slides: "/s" }])).toEqual([]); expect(at([{}])).toEqual(["fields/speakers/0"]); expect(at([{ nope: 1 }])).toEqual(["fields/speakers/0/nope"]); + // prototype names are not sub-fields, and do not make an item non-empty + expect(at([{ constructor: { deep: [1, { x: "y" }] } }])).toEqual([ + "fields/speakers/0/constructor", + ]); + expect(at([{ toString: 1, talk: "x" }])).toEqual(["fields/speakers/0/toString"]); + expect(at([JSON.parse('{"__proto__": {"a": 1}}')])).toEqual(["fields/speakers/0/__proto__"]); expect(at([{ talk: "a\u0007b" }])).toEqual(["fields/speakers/0/talk"]); expect(at([{ day: "2026-02-30" }])).toEqual(["fields/speakers/0/day"]); expect(at([{ slides: "asset:asset_01HZX8K3Q9P1" }])).toEqual(["fields/speakers/0/slides"]); diff --git a/packages/mcp-server/src/field-hints.ts b/packages/mcp-server/src/field-hints.ts index 8f7ad0a3..fab8f971 100644 --- a/packages/mcp-server/src/field-hints.ts +++ b/packages/mcp-server/src/field-hints.ts @@ -4,6 +4,7 @@ * `details.issues`, each at its slash path (`fields/startsAt/at`), so the hint only names the tool. */ import type { InvariantCode } from "@cloudhal/core-ts"; +import { FIELD_TYPES } from "@cloudhal/core-ts/fields"; const OPERATIONS = "rename_field, change_field_type, set_field_localized or remove_field"; @@ -38,8 +39,7 @@ const HINTS: Partial> = { invalid_expiry: "expiry.field must be a date or datetime field of the collection (list_collections → fields)", // RFC-0045 (references and repeaters; refused until they are enabled) - fields_unsupported: - "reference and repeater fields are not available on this server yet: use the nine field types list_collections shows (a text field for now), and leave the rest of the change as it is", + fields_unsupported: `reference and repeater fields are not available on this server yet: give the field one of the types update_collection's fields input takes (${FIELD_TYPES.join(", ")}; text for now), and keep the rest of the change as it is`, reference_target_unknown: "a reference field's collections must be collections of this site (list_collections)", too_many_references: diff --git a/packages/mcp-server/test/tool-schemas.test.ts b/packages/mcp-server/test/tool-schemas.test.ts index 2c461235..8aea655b 100644 --- a/packages/mcp-server/test/tool-schemas.test.ts +++ b/packages/mcp-server/test/tool-schemas.test.ts @@ -11,6 +11,7 @@ import { BetterSqlite3Driver } from "@cloudhal/storage/sqlite"; import { describe, expect, it } from "vitest"; import { FIELD_DEFINITIONS_INPUT } from "../src/collection-fields.ts"; import type { JsonSchema } from "../src/common.ts"; +import { fieldHint } from "../src/field-hints.ts"; import { defsClosure } from "../src/schema-transforms.ts"; import { defineTools, runTool, toolPolicies } from "../src/tools.ts"; @@ -110,6 +111,13 @@ describe("defsClosure", () => { }); describe("update_collection and the RFC-0045 gate", () => { + it("hints at the types update_collection takes, from the enabled list", () => { + const hint = fieldHint("fields_unsupported", {}) ?? ""; + expect(hint).toContain("update_collection's fields input"); + expect(hint).toContain("text, richText, number, boolean, date, datetime, url, image, select"); + expect(hint).not.toContain("list_collections"); + }); + it("refuses a reference or repeater definition with fields_unsupported", async () => { const sql = await BetterSqlite3Driver.open(":memory:"); await migrate(sql, migrations); From a7bf5d4d9ef916c07f75284c7910e602183cb440 Mon Sep 17 00:00:00 2001 From: Madraka <8898640+Madraka@users.noreply.github.com> Date: Wed, 11 Nov 2026 04:27:34 +0300 Subject: [PATCH 4/4] fix(core-ts): gate verified snapshot site bodies only, read own values (PR #203 verify) - The snapshot part of the RFC-0045 gate moves to prepareImport as assertSnapshotFieldTypes, over the bodies bodiesToWrite has hash-checked and validated as sites. An invalid blob is refused by its schema again ("is not a valid site", never a TypeError), a forged text under a hash the target holds is not read, and the refusal's details.issues is capped at 50 with total. - validateProps reads own values only (also for required), so a prop named isPrototypeOf the value leaves out is absent; repeater value checks, referenceUses and per-item requiredToPublish read item values the same way (ownValue). toLocaleString, isPrototypeOf and propertyIsEnumerable join RESERVED_FIELD_KEYS. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com> --- .changeset/reference-repeater-contract.md | 4 +- packages/core-ts/src/blocks.ts | 8 +- packages/core-ts/src/fields/types.ts | 7 ++ packages/core-ts/src/fields/value-checks.ts | 6 +- packages/core-ts/src/references/values.ts | 6 +- packages/core-ts/src/sync-checks.ts | 79 +++++++++++-------- packages/core-ts/src/sync-plan.ts | 7 ++ .../core-ts/src/testing-reference-gate.ts | 21 +++++ .../core-ts/test/fields-definitions.test.ts | 3 +- packages/core-ts/test/references-core.test.ts | 39 +++++++++ 10 files changed, 134 insertions(+), 46 deletions(-) diff --git a/.changeset/reference-repeater-contract.md b/.changeset/reference-repeater-contract.md index e782b89d..e0ce785c 100644 --- a/.changeset/reference-repeater-contract.md +++ b/.changeset/reference-repeater-contract.md @@ -13,7 +13,7 @@ The contract gains two field types for collections, `reference` and `repeater` ( - field operations; - every bundle `importSite` takes: import, pull, push, `cloudhal import --replace` and `--revert`, snapshot restores and edition copies. - `checkBundle` gained `assertFieldTypes`. It checks the working site row and every site body the bundle's snapshots carry, and refuses the types when no capability is given. A push answers `409 fields_unsupported` with its issues, not `400 bad_request`. + `checkBundle` gained `assertFieldTypes`. It checks the working site row, and `prepareImport` checks every snapshot site body the import writes once it has been verified (bodies the target already holds are its own), and refuses the types when no capability is given. A push answers `409 fields_unsupported` with its issues, not `400 bad_request`. - **Pure core** (`@cloudhal/core-ts/fields`), with no effect on stored data yet: - the definition rules: `reference_target_unknown`, `where`, `itemLabel`, and caps on sub-fields and leaves; - value checks for both types, the per-item `requiredToPublish`, and `too_many_references`; @@ -23,4 +23,4 @@ The contract gains two field types for collections, `reference` and `repeater` ( `FIELD_TYPES` keeps the nine enabled types (`EnabledFieldType`). `ALL_FIELD_TYPES` lists all eleven. - **MCP.** `update_collection`'s input now builds its `$defs` with `defsClosure` and offers only the enabled types. A test checks that every `$ref` in every tool's input and output schema resolves. Before this change, `pnpm gen` alone would have left the input pointing at `$defs` it did not carry. -- **Fix, existing sites:** block props and typed-field objects no longer accept keys named after `Object.prototype` members (`constructor`, `toString`, `__proto__`, …) as known props. The props interpreter (`validateProps`) now checks own keys only, so such keys are refused as unknown. +- **Fix, existing sites:** block props and typed-field objects no longer accept keys named after `Object.prototype` members (`constructor`, `toString`, `__proto__`, …) as known props. The props interpreter (`validateProps`) now checks and reads own keys only. Such keys are refused as unknown, and a prop the value leaves out is absent, not the prototype's function. `toLocaleString`, `isPrototypeOf` and `propertyIsEnumerable` join the reserved field keys, beside `toString`, `valueOf` and `hasOwnProperty`. diff --git a/packages/core-ts/src/blocks.ts b/packages/core-ts/src/blocks.ts index c50a310f..c16f3892 100644 --- a/packages/core-ts/src/blocks.ts +++ b/packages/core-ts/src/blocks.ts @@ -454,14 +454,16 @@ function validateObject( return; } const at = (key: string) => (path ? `${path}/${key}` : key); - // Own keys only: `constructor`, `toString` or `__proto__` are not props of a block. + // Own keys and values only: `constructor`, `toString` or `__proto__` are not props of a block, + // and a prop named `isPrototypeOf` the value leaves out is absent, not Object.prototype's. + const own = (key: string) => (Object.hasOwn(v, key) ? v[key] : undefined); for (const key of Object.keys(v)) if (!Object.hasOwn(s.properties, key)) out.push({ path: at(key), message: "not a prop of this block" }); for (const key of s.required ?? []) - if (v[key] === undefined) out.push({ path: at(key), message: "required" }); + if (own(key) === undefined) out.push({ path: at(key), message: "required" }); for (const [key, child] of Object.entries(s.properties)) { - const value = v[key]; + const value = own(key); if (value === undefined) continue; if (child.type === "array") { if (!Array.isArray(value)) { diff --git a/packages/core-ts/src/fields/types.ts b/packages/core-ts/src/fields/types.ts index 9e731e13..14a6fb03 100644 --- a/packages/core-ts/src/fields/types.ts +++ b/packages/core-ts/src/fields/types.ts @@ -114,6 +114,9 @@ export const RESERVED_FIELD_KEYS: ReadonlySet = new Set([ "toString", "valueOf", "hasOwnProperty", + "toLocaleString", + "isPrototypeOf", + "propertyIsEnumerable", ]); export type FieldIssueCode = @@ -165,6 +168,10 @@ export function fieldDefinitions(site: SiteFields, collection: string): readonly export const isRecord = (v: unknown): v is Record => typeof v === "object" && v !== null && !Array.isArray(v); +/** `record[key]` when it is the record's own property; never a prototype member (`isPrototypeOf`). */ +export const ownValue = (record: Readonly>, key: string): unknown => + Object.hasOwn(record, key) ? record[key] : undefined; + /** Structural equality of JSON values (key order ignored). */ export function sameJson(a: unknown, b: unknown): boolean { if (a === b) return true; diff --git a/packages/core-ts/src/fields/value-checks.ts b/packages/core-ts/src/fields/value-checks.ts index d1c9d42a..ae9470cb 100644 --- a/packages/core-ts/src/fields/value-checks.ts +++ b/packages/core-ts/src/fields/value-checks.ts @@ -9,7 +9,7 @@ import { bodyDirectives } from "../body-blocks.ts"; import { checkDirective } from "../directives.ts"; import { checkBodySurfaces, createCatalog } from "../surfaces.ts"; import { fieldPropSchema } from "./props.ts"; -import { FIELD_LIMITS, type FieldIssue } from "./types.ts"; +import { FIELD_LIMITS, type FieldIssue, ownValue } from "./types.ts"; export interface ValueCheckOptions { /** Directives and surface fences in rich text are checked against it (absent: shape only). */ @@ -136,10 +136,10 @@ const EXTRA: Partial> = { if (def.type !== "repeater" || !Array.isArray(value)) return []; const out: FieldIssue[] = []; (value as Record[]).forEach((item, i) => { - if (!def.fields.some((sub) => Object.hasOwn(item, sub.key) && item[sub.key] !== undefined)) + if (!def.fields.some((sub) => ownValue(item, sub.key) !== undefined)) out.push(issue(`${path}/${i}`, "an item needs a value")); for (const sub of def.fields) { - const v = item[sub.key]; + const v = ownValue(item, sub.key); const extra = EXTRA[sub.type]; if (v !== undefined && extra) out.push( diff --git a/packages/core-ts/src/references/values.ts b/packages/core-ts/src/references/values.ts index 1e130ddb..f87a1af1 100644 --- a/packages/core-ts/src/references/values.ts +++ b/packages/core-ts/src/references/values.ts @@ -4,7 +4,7 @@ * sub-fields in every item. Pure; the store's target guard (rollout step 2) checks the targets. */ import type { FieldDefinition } from "@cloudhal/core-contracts"; -import { FIELD_LIMITS, type FieldIssue, isRecord } from "../fields/types.ts"; +import { FIELD_LIMITS, type FieldIssue, isRecord, ownValue } from "../fields/types.ts"; /** "Has a value" for one sub-value: not absent, not an empty string. */ const hasSubValue = (v: unknown): boolean => @@ -34,7 +34,7 @@ export function referenceUses(def: FieldDefinition, value: unknown, path: string value.forEach((item, i) => { if (!isRecord(item)) return; for (const sub of subs) { - const v = item[sub.key]; + const v = ownValue(item, sub.key); if (typeof v === "string" && sub.type === "reference") out.push({ path: `${path}/${i}/${sub.key}`, groupId: v, collections: sub.collections }); } @@ -76,7 +76,7 @@ export function requiredSubFieldIssues( const out: FieldIssue[] = []; value.forEach((item, i) => { for (const sub of required) - if (!isRecord(item) || !hasSubValue(item[sub.key])) + if (!isRecord(item) || !hasSubValue(ownValue(item, sub.key))) out.push({ path: `${path}/${i}/${sub.key}`, code: "required_field", diff --git a/packages/core-ts/src/sync-checks.ts b/packages/core-ts/src/sync-checks.ts index df83ac1d..a8836361 100644 --- a/packages/core-ts/src/sync-checks.ts +++ b/packages/core-ts/src/sync-checks.ts @@ -15,6 +15,7 @@ import { } from "./availability.ts"; import { InvariantError } from "./errors.ts"; import { refuseFieldIssues } from "./fields/refusal.ts"; +import type { FieldIssue } from "./fields/types.ts"; import { nodeHash } from "./hash.ts"; import { validateMenuTree } from "./menu.ts"; import { isNodeKind, NODE_SPEC, type NodeKind, type NodeTypes } from "./nodes.ts"; @@ -253,36 +254,28 @@ export async function assertIdsFree(to: StoragePair, bundle: SiteBundle): Promis } } -/** - * The site bodies a bundle would write: its working site row, and every blob a snapshot ref of - * kind `site` names (a publish or rollback can make any of them live). Blobs that do not parse are - * left to `bodiesToWrite`, which refuses them. - */ -function bundleSiteBodies(bundle: SiteBundle, site: NodeRow): { at: string; body: unknown }[] { - const out: { at: string; body: unknown }[] = [{ at: "site", body: JSON.parse(site.body) }]; - const hashes = new Set(); - for (const row of bundle.snapshots) - for (const ref of snapshotRefs(row)) if (ref.kind === "site") hashes.add(ref.hash); - for (const hash of [...hashes].sort()) { - const text = bundle.blobs[hash]; - if (text === undefined) continue; - try { - const body: unknown = JSON.parse(text); - if (typeof body === "object" && body !== null) out.push({ at: `blobs/${hash}`, body }); - } catch { - // not JSON: bodiesToWrite refuses it without quoting it - } - } - return out; +/** At most this many issues travel in a field-type refusal's `details.issues` (with `total`). */ +const MAX_TYPE_ISSUES = 50; + +type SiteBody = Parameters[0]; + +/** The gate's refusal over `issues`, unless `support` allows the types for the site. */ +async function refuseUnsupportedTypes( + siteId: string, + issues: readonly FieldIssue[], + support: FieldTypeSupport | undefined, +): Promise { + if (issues.length === 0 || (await referenceTypesSupported(siteId, support))) return; + refuseFieldIssues(issues.slice(0, MAX_TYPE_ISSUES), { siteId, total: issues.length }); } /** - * RFC-0045 §1: a bundle whose site, working copy or any snapshot's, defines a reference or - * repeater field (or a marker converting one) is refused (`fields_unsupported`) unless `support` - * says the site's renderer reads them. Without a capability the types are refused, unlike - * `assertGates`. One check for `importSite`, so for pull, push, `cloudhal import --replace` and - * `--revert`, and snapshot restores. Entry values need no check of their own: they mean nothing - * without a definition, and every site body that could define one is checked here. + * RFC-0045 §1: a bundle whose working site defines a reference or repeater field (or a marker + * converting one) is refused (`fields_unsupported`) unless `support` says the site's renderer + * reads them. Without a capability the types are refused, unlike `assertGates`. One check for + * `importSite`, so for pull, push, `cloudhal import --replace` and `--revert`, and snapshot + * restores. The site bodies of the bundle's snapshots are checked by + * `assertSnapshotFieldTypes`, once `bodiesToWrite` has verified them. */ export async function assertFieldTypes( siteId: string, @@ -291,13 +284,31 @@ export async function assertFieldTypes( ): Promise { const site = bundle.nodes.find((n) => n.kind === "site" && n.id === siteId); if (!site) return; - const issues = bundleSiteBodies(bundle, site).flatMap(({ at, body }) => - unsupportedFieldTypeIssues(body as Parameters[0]).map((i) => - at === "site" ? i : { ...i, path: `${at}/${i.path}` }, - ), - ); - if (issues.length === 0 || (await referenceTypesSupported(siteId, support))) return; - refuseFieldIssues(issues, { siteId }); + const issues = unsupportedFieldTypeIssues(JSON.parse(site.body) as SiteBody); + await refuseUnsupportedTypes(siteId, issues, support); +} + +/** + * RFC-0045 §1 for snapshots: the site bodies an import writes (hash-checked and validated as the + * site of every snapshot ref to them, `bodiesToWrite`), since a publish or rollback can make any + * of them live. Bodies the target already holds are its own and are not written, so not checked. + * Entry bodies need no check: a value means nothing without a definition. + */ +export async function assertSnapshotFieldTypes( + siteId: string, + written: readonly { hash: string; body: unknown }[], + siteHashes: ReadonlySet, + support: FieldTypeSupport | undefined, +): Promise { + const issues = written + .filter((b) => siteHashes.has(b.hash)) + .flatMap((b) => + unsupportedFieldTypeIssues(b.body as SiteBody).map((i) => ({ + ...i, + path: `blobs/${b.hash}/${i.path}`, + })), + ); + await refuseUnsupportedTypes(siteId, issues, support); } /** diff --git a/packages/core-ts/src/sync-plan.ts b/packages/core-ts/src/sync-plan.ts index e6b3f680..6196dd1a 100644 --- a/packages/core-ts/src/sync-plan.ts +++ b/packages/core-ts/src/sync-plan.ts @@ -27,6 +27,7 @@ import { import { assertIdsFree, assertNodeBody, + assertSnapshotFieldTypes, assertTimeZone, checkBundle, listedMedia, @@ -349,6 +350,12 @@ export async function prepareImport( await assertIdsFree(to, bundle); const refs = bundle.snapshots.map((s) => ({ id: s.id, refs: snapshotRefs(s) })); const blobs = await bodiesToWrite(to, bundle, refs); + // RFC-0045 §1: the site bodies of snapshots too, once verified (rollout step 2 passes the + // capability, CopySiteOptions.fieldTypes, here and to checkBundle). + const siteHashes = new Set( + refs.flatMap((s) => s.refs.filter((r) => r.kind === "site").map((r) => r.hash)), + ); + await assertSnapshotFieldTypes(siteId, blobs, siteHashes, undefined); await assertSnapshotsBacked(to, bundle, refs); const snapshots = await snapshotsToAdd(to, bundle); const project = await projectToAdd(to, bundle); diff --git a/packages/core-ts/src/testing-reference-gate.ts b/packages/core-ts/src/testing-reference-gate.ts index ec557181..76370d47 100644 --- a/packages/core-ts/src/testing-reference-gate.ts +++ b/packages/core-ts/src/testing-reference-gate.ts @@ -181,6 +181,27 @@ export function referenceGateSuite(ctx: SuiteContext): void { "fields_unsupported", ); } + // an invalid site body is refused as before, by its schema, never by the gate's walk + for (const broken of [{ events: { fields: {} } }, { events: { fields: [null] } }]) { + const bad = await withSnapshotSite(bundle, snap.id, broken as never); + await expect(importSite(storage(), bad, { mode: "replace" })).rejects.toThrow( + /is not a valid site/, + ); + } + // a body the target already holds is its own: a forged text under its hash is not read + const forged = structuredClone(bundle); + const ref = ( + JSON.parse(forged.snapshots.find((r) => r.id === snap.id)?.body ?? "{}") as { + nodes: NodeRef[]; + } + ).nodes.find((r) => r.kind === "site"); + if (!ref) throw new Error("no site ref"); + const text = forged.blobs[ref.hash] ?? "{}"; + forged.blobs[ref.hash] = canonicalJson({ + ...(JSON.parse(text) as Site), + collections: events([TEXT, VENUE]), + }); + expect((await importSite(storage(), forged)).nodes.unchanged).toBe(true); // the same rewrite with clean definitions passes the gate (and fails later, elsewhere) const clean = await withSnapshotSite(bundle, snap.id, events([TEXT])); const later = await importSite(storage(), clean, { mode: "replace" }).then( diff --git a/packages/core-ts/test/fields-definitions.test.ts b/packages/core-ts/test/fields-definitions.test.ts index 86720ed6..6d016b9f 100644 --- a/packages/core-ts/test/fields-definitions.test.ts +++ b/packages/core-ts/test/fields-definitions.test.ts @@ -59,7 +59,8 @@ describe("checkFieldDefinitions", () => { }), ]); } - expect(RESERVED_FIELD_KEYS.size).toBe(32); + // RFC-0045 review: three more Object.prototype names (toLocaleString, isPrototypeOf, propertyIsEnumerable) + expect(RESERVED_FIELD_KEYS.size).toBe(35); }); it("reserves every ContentEntry property of the contract", async () => { diff --git a/packages/core-ts/test/references-core.test.ts b/packages/core-ts/test/references-core.test.ts index 77b58a9d..6fafa5d9 100644 --- a/packages/core-ts/test/references-core.test.ts +++ b/packages/core-ts/test/references-core.test.ts @@ -281,6 +281,45 @@ describe("values", () => { expect(at([{ abstract: "a".repeat(50_001) }])).toEqual(["fields/speakers/0/abstract"]); }); + it("reads only an item's own values, so prototype-named sub-fields are simply absent", () => { + const odd = { + ...SPEAKERS, + fields: ["isPrototypeOf", "toLocaleString", "propertyIsEnumerable"].map((key) => ({ + key, + type: "text", + labels: L(key), + })), + } as unknown as FieldDefinition; + expect(valueIssues(odd, [{ isPrototypeOf: "x" }], "fields/odd")).toEqual([]); + expect( + valueIssues(odd, [{ toLocaleString: "y" }, { propertyIsEnumerable: "z" }], "fields/odd"), + ).toEqual([]); + // a required prop the value leaves out is missing, not Object.prototype's function + expect( + validateProps( + { + type: "object", + properties: { isPrototypeOf: { type: "string" as const } }, + required: ["isPrototypeOf"], + additionalProperties: false, + }, + {}, + ), + ).toEqual([{ path: "isPrototypeOf", message: "required" }]); + // and the names are reserved as keys, top-level and in a repeater + const reserved = checkFieldDefinitions( + siteWith([odd, { ...STARTS, key: "isPrototypeOf" }]), + undefined, + ENABLED, + ); + expect(reserved.map((i) => i.path)).toEqual([ + "collections/events/fields/0/fields/0/key", + "collections/events/fields/0/fields/1/key", + "collections/events/fields/0/fields/2/key", + "collections/events/fields/1/key", + ]); + }); + it("caps references per entry across fields and items", () => { const many = { key: "many",