From 672b993b3aa327690280aac185a01ee8c650ccf0 Mon Sep 17 00:00:00 2001 From: Madraka <8898640+Madraka@users.noreply.github.com> Date: Fri, 23 Oct 2026 02:30:35 +0300 Subject: [PATCH] feat(contracts,core-ts): typed custom fields contract and checker (RFC-0041 step 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - site.schema.json: optional CollectionSettings.fields (1–30), the FieldDefinition union of nine types with discriminator `type` (ADR-0043), labels, options and the migration marker; 2 valid and 28 invalid fixtures; TS and Rust regenerated. - @cloudhal/core-ts/fields (Worker-safe, eval-free): definition checks and caps, compiling to ADR-0030's props subset for validateProps, per-type value checks, the requiredToPublish check, a JSON Schema for agents within OpenAI and Anthropic strict modes, and resolveFields for localization. Nothing reads the module yet: no behaviour change. 82 new tests; cargo test and clippy pass. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com> --- .changeset/typed-fields-contract.md | 6 + .../src/generated/content_entry.rs | 2 +- crates/cloudhal-schema/src/generated/site.rs | 519 ++ .../invalid/site/field-boolean-required.json | 37 + .../invalid/site/field-date-searchable.json | 37 + .../invalid/site/field-help-too-long.json | 37 + .../invalid/site/field-key-kebab-case.json | 36 + .../invalid/site/field-key-too-long.json | 36 + .../invalid/site/field-key-upper-first.json | 36 + .../invalid/site/field-label-bad-locale.json | 36 + .../invalid/site/field-label-empty.json | 36 + .../invalid/site/field-label-too-long.json | 36 + .../invalid/site/field-labels-empty.json | 32 + .../site/field-migration-unknown-type.json | 40 + .../site/field-migration-without-id.json | 39 + .../site/field-number-with-max-length.json | 37 + .../site/field-option-value-not-slug.json | 44 + .../site/field-option-without-labels.json | 41 + .../site/field-select-empty-options.json | 37 + .../site/field-select-too-many-options.json | 344 ++ .../site/field-select-without-options.json | 36 + .../site/field-text-max-length-too-big.json | 37 + .../site/field-text-max-length-zero.json | 37 + .../invalid/site/field-text-with-options.json | 44 + .../invalid/site/field-unknown-property.json | 37 + .../invalid/site/field-unknown-type.json | 36 + .../invalid/site/field-without-labels.json | 31 + .../invalid/site/field-without-localized.json | 35 + .../fixtures/invalid/site/fields-empty.json | 25 + .../invalid/site/fields-too-many.json | 336 ++ .../valid/site/collection-fields-minimal.json | 121 + .../valid/site/collection-fields.json | 223 + .../schemas/content-entry.schema.json | 2 +- .../core-contracts/schemas/site.schema.json | 408 ++ .../src/generated/content-entry.ts | 2 +- .../core-contracts/src/generated/index.ts | 2 +- .../core-contracts/src/generated/schemas.ts | 481 +- packages/core-contracts/src/generated/site.ts | 258 + .../src/generated/validators.js | 4549 ++++++++++++----- packages/core-ts/package.json | 5 + packages/core-ts/src/fields/definitions.ts | 203 + packages/core-ts/src/fields/index.ts | 57 + packages/core-ts/src/fields/json-schema.ts | 159 + packages/core-ts/src/fields/props.ts | 111 + packages/core-ts/src/fields/resolve.ts | 107 + packages/core-ts/src/fields/types.ts | 180 + packages/core-ts/src/fields/value-checks.ts | 147 + packages/core-ts/src/fields/values.ts | 115 + .../core-ts/test/fields-definitions.test.ts | 237 + packages/core-ts/test/fields-fixture.ts | 86 + packages/core-ts/test/fields-resolve.test.ts | 258 + packages/core-ts/test/fields-values.test.ts | 237 + .../test/golden/fields-json-schema.snap | 96 + .../core-ts/test/worker-time-free.test.ts | 3 +- 54 files changed, 9007 insertions(+), 1162 deletions(-) create mode 100644 .changeset/typed-fields-contract.md create mode 100644 packages/core-contracts/fixtures/invalid/site/field-boolean-required.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-date-searchable.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-help-too-long.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-key-kebab-case.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-key-too-long.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-key-upper-first.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-label-bad-locale.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-label-empty.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-label-too-long.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-labels-empty.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-migration-unknown-type.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-migration-without-id.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-number-with-max-length.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-option-value-not-slug.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-option-without-labels.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-select-empty-options.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-select-too-many-options.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-select-without-options.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-text-max-length-too-big.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-text-max-length-zero.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-text-with-options.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-unknown-property.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-unknown-type.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-without-labels.json create mode 100644 packages/core-contracts/fixtures/invalid/site/field-without-localized.json create mode 100644 packages/core-contracts/fixtures/invalid/site/fields-empty.json create mode 100644 packages/core-contracts/fixtures/invalid/site/fields-too-many.json create mode 100644 packages/core-contracts/fixtures/valid/site/collection-fields-minimal.json create mode 100644 packages/core-contracts/fixtures/valid/site/collection-fields.json create mode 100644 packages/core-ts/src/fields/definitions.ts create mode 100644 packages/core-ts/src/fields/index.ts create mode 100644 packages/core-ts/src/fields/json-schema.ts create mode 100644 packages/core-ts/src/fields/props.ts create mode 100644 packages/core-ts/src/fields/resolve.ts create mode 100644 packages/core-ts/src/fields/types.ts create mode 100644 packages/core-ts/src/fields/value-checks.ts create mode 100644 packages/core-ts/src/fields/values.ts create mode 100644 packages/core-ts/test/fields-definitions.test.ts create mode 100644 packages/core-ts/test/fields-fixture.ts create mode 100644 packages/core-ts/test/fields-resolve.test.ts create mode 100644 packages/core-ts/test/fields-values.test.ts create mode 100644 packages/core-ts/test/golden/fields-json-schema.snap diff --git a/.changeset/typed-fields-contract.md b/.changeset/typed-fields-contract.md new file mode 100644 index 00000000..7f324071 --- /dev/null +++ b/.changeset/typed-fields-contract.md @@ -0,0 +1,6 @@ +--- +"@cloudhal/core-contracts": minor +"@cloudhal/core-ts": minor +--- + +Typed custom fields, step 1 (RFC-0041 / ADR-0043). `Site.collections..fields` takes an ordered list of 1–30 field definitions in nine types (`text`, `richText`, `number`, `boolean`, `date`, `datetime`, `url`, `image`, `select`), discriminated by `type`, each with per-locale labels, `localized` and (except `boolean`) `requiredToPublish`. The new `@cloudhal/core-ts/fields` subpath (Worker-safe, eval-free) checks definitions (reserved keys, unique keys and options, 30 per collection, 200 per site, 256 KB of collection settings, the plain-edit guard), checks values through ADR-0030's `validateProps` (only changed values; unknown keys refused once a collection has definitions; 256 KB per entry), checks `requiredToPublish`, emits the JSON Schema agents receive (strict-mode safe), and resolves values across translations (`resolveFields`). Nothing reads it yet: the store, API, MCP, Studio and renderer adopt it in later steps, so nothing changes for existing sites. diff --git a/crates/cloudhal-schema/src/generated/content_entry.rs b/crates/cloudhal-schema/src/generated/content_entry.rs index 1ac0121d..cd081298 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; any JSON."] + #[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."] 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 5dd108cb..08d0a829 100644 --- a/crates/cloudhal-schema/src/generated/site.rs +++ b/crates/cloudhal-schema/src/generated/site.rs @@ -1829,6 +1829,9 @@ pub struct CollectionSettings { #[doc = "RFC-0040: entries appear in the site's RSS feeds. Absent: true."] #[serde(skip_serializing_if = "::std::option::Option::is_none")] pub feed: ::std::option::Option, + #[doc = "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)."] + #[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")] + pub fields: ::std::vec::Vec, #[doc = "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."] #[serde(skip_serializing_if = "::std::option::Option::is_none")] pub icon: ::std::option::Option, @@ -2201,6 +2204,522 @@ impl<'de> ::serde::Deserialize<'de> for DomainHostname { }) } } +#[doc = "RFC-0041: one typed custom field of a collection, discriminated by `type`."] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, PartialEq)] +#[serde(tag = "type", deny_unknown_fields)] +pub enum FieldDefinition { + #[doc = "TextField\n\nPlain text: a string of at most maxLength characters."] + #[serde(rename = "text")] + Text { + key: FieldKey, + labels: FieldLabels, + #[doc = "true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."] + localized: bool, + #[doc = "Absent: 5000."] + #[serde( + rename = "maxLength", + skip_serializing_if = "::std::option::Option::is_none" + )] + max_length: ::std::option::Option<::std::num::NonZeroU64>, + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + migration: ::std::option::Option, + #[doc = "Line breaks allowed. Absent: false."] + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + multiline: ::std::option::Option, + #[doc = "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 = "Its words go into site search. Absent: false."] + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + searchable: ::std::option::Option, + }, + #[doc = "RichTextField\n\nCloudhal Markdown v1 (ADR-0028), at most 50 000 characters."] + #[serde(rename = "richText")] + RichText { + key: FieldKey, + labels: FieldLabels, + #[doc = "true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."] + localized: bool, + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + migration: ::std::option::Option, + #[doc = "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 = "Its words go into site search. Absent: false."] + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + searchable: ::std::option::Option, + }, + #[doc = "NumberField\n\nA finite number."] + #[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, + #[doc = "true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."] + localized: bool, + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + maximum: ::std::option::Option, + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + migration: ::std::option::Option, + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + minimum: ::std::option::Option, + #[doc = "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 = "BooleanField\n\ntrue or false; absent reads as false, so it cannot be required to publish."] + #[serde(rename = "boolean")] + Boolean { + key: FieldKey, + labels: FieldLabels, + #[doc = "true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."] + localized: bool, + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + migration: ::std::option::Option, + }, + #[doc = "DateField\n\nA calendar day without a zone (YYYY-MM-DD)."] + #[serde(rename = "date")] + Date { + key: FieldKey, + labels: FieldLabels, + #[doc = "true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."] + localized: bool, + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + migration: ::std::option::Option, + #[doc = "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 = "DateTimeField\n\nAn instant with the zone it was entered in: {at, timeZone} (RFC-0008 §9.1)."] + #[serde(rename = "datetime")] + Datetime { + key: FieldKey, + labels: FieldLabels, + #[doc = "true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."] + localized: bool, + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + migration: ::std::option::Option, + #[doc = "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 = "UrlField\n\nA link: https, http, mailto, tel, a site-relative path or a #fragment; at most 2048 characters."] + #[serde(rename = "url")] + Url { + key: FieldKey, + labels: FieldLabels, + #[doc = "true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."] + localized: bool, + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + migration: ::std::option::Option, + #[doc = "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 = "ImageField\n\nAn image Asset of the same site, by id (asset_…)."] + #[serde(rename = "image")] + Image { + key: FieldKey, + labels: FieldLabels, + #[doc = "true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."] + localized: bool, + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + migration: ::std::option::Option, + #[doc = "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 = "SelectField\n\nOne of a closed list of options, or several when multiple."] + #[serde(rename = "select")] + Select { + key: FieldKey, + labels: FieldLabels, + #[doc = "true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."] + localized: bool, + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + migration: ::std::option::Option, + #[doc = "The value is a list of option values. Absent: false."] + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + multiple: ::std::option::Option, + options: ::std::vec::Vec, + #[doc = "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 = "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)] +#[serde(transparent)] +pub struct FieldKey(::std::string::String); +impl ::std::ops::Deref for FieldKey { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: FieldKey) -> Self { + value.0 + } +} +impl ::std::str::FromStr for FieldKey { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> ::std::result::Result { + static PATTERN: ::std::sync::LazyLock<::regress::Regex> = + ::std::sync::LazyLock::new(|| { + ::regress::Regex::new("^[a-z][A-Za-z0-9]{0,39}$").unwrap() + }); + if PATTERN.find(value).is_none() { + return Err("doesn't match pattern \"^[a-z][A-Za-z0-9]{0,39}$\"".into()); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for FieldKey { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for FieldKey { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for FieldKey { + fn deserialize(deserializer: D) -> ::std::result::Result + where + D: ::serde::Deserializer<'de>, + { + ::std::string::String::deserialize(deserializer)? + .parse() + .map_err(|e: self::error::ConversionError| { + ::custom(e.to_string()) + }) + } +} +#[doc = "`FieldLabel`"] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct FieldLabel { + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + pub help: ::std::option::Option, + pub label: FieldLabelLabel, +} +#[doc = "`FieldLabelHelp`"] +#[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct FieldLabelHelp(::std::string::String); +impl ::std::ops::Deref for FieldLabelHelp { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: FieldLabelHelp) -> Self { + value.0 + } +} +impl ::std::str::FromStr for FieldLabelHelp { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> ::std::result::Result { + if value.chars().count() > 300usize { + return Err("longer than 300 characters".into()); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for FieldLabelHelp { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for FieldLabelHelp { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for FieldLabelHelp { + fn deserialize(deserializer: D) -> ::std::result::Result + where + D: ::serde::Deserializer<'de>, + { + ::std::string::String::deserialize(deserializer)? + .parse() + .map_err(|e: self::error::ConversionError| { + ::custom(e.to_string()) + }) + } +} +#[doc = "`FieldLabelLabel`"] +#[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct FieldLabelLabel(::std::string::String); +impl ::std::ops::Deref for FieldLabelLabel { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: FieldLabelLabel) -> Self { + value.0 + } +} +impl ::std::str::FromStr for FieldLabelLabel { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> ::std::result::Result { + if value.chars().count() > 100usize { + return Err("longer than 100 characters".into()); + } + if value.chars().count() < 1usize { + return Err("shorter than 1 characters".into()); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for FieldLabelLabel { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for FieldLabelLabel { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for FieldLabelLabel { + fn deserialize(deserializer: D) -> ::std::result::Result + where + D: ::serde::Deserializer<'de>, + { + ::std::string::String::deserialize(deserializer)? + .parse() + .map_err(|e: self::error::ConversionError| { + ::custom(e.to_string()) + }) + } +} +#[doc = "The field's label and help per locale. Missing a locale: the default locale's, then the key."] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, PartialEq)] +#[serde(transparent)] +pub struct FieldLabels(pub ::std::collections::HashMap); +impl ::std::ops::Deref for FieldLabels { + type Target = ::std::collections::HashMap; + fn deref(&self) -> &::std::collections::HashMap { + &self.0 + } +} +impl ::std::convert::From for ::std::collections::HashMap { + fn from(value: FieldLabels) -> Self { + value.0 + } +} +impl ::std::convert::From<::std::collections::HashMap> for FieldLabels { + fn from(value: ::std::collections::HashMap) -> Self { + Self(value) + } +} +#[doc = "RFC-0041 §6: present only while a field operation rewrites entries."] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct FieldMigration { + #[serde( + rename = "fromKey", + skip_serializing_if = "::std::option::Option::is_none" + )] + pub from_key: ::std::option::Option, + #[serde( + rename = "fromLocalized", + skip_serializing_if = "::std::option::Option::is_none" + )] + pub from_localized: ::std::option::Option, + #[serde( + rename = "fromType", + skip_serializing_if = "::std::option::Option::is_none" + )] + pub from_type: ::std::option::Option, + #[doc = "The operation's \"before\" snapshot."] + pub id: Id, +} +#[doc = "`FieldMigrationFromType`"] +#[derive( + :: serde :: Deserialize, + :: serde :: Serialize, + Clone, + Copy, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, +)] +pub enum FieldMigrationFromType { + #[serde(rename = "text")] + Text, + #[serde(rename = "richText")] + RichText, + #[serde(rename = "number")] + Number, + #[serde(rename = "boolean")] + Boolean, + #[serde(rename = "date")] + Date, + #[serde(rename = "datetime")] + Datetime, + #[serde(rename = "url")] + Url, + #[serde(rename = "image")] + Image, + #[serde(rename = "select")] + Select, +} +impl ::std::fmt::Display for FieldMigrationFromType { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Self::Text => f.write_str("text"), + Self::RichText => f.write_str("richText"), + Self::Number => f.write_str("number"), + Self::Boolean => f.write_str("boolean"), + Self::Date => f.write_str("date"), + Self::Datetime => f.write_str("datetime"), + Self::Url => f.write_str("url"), + Self::Image => f.write_str("image"), + Self::Select => f.write_str("select"), + } + } +} +impl ::std::str::FromStr for FieldMigrationFromType { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> ::std::result::Result { + match value { + "text" => Ok(Self::Text), + "richText" => Ok(Self::RichText), + "number" => Ok(Self::Number), + "boolean" => Ok(Self::Boolean), + "date" => Ok(Self::Date), + "datetime" => Ok(Self::Datetime), + "url" => Ok(Self::Url), + "image" => Ok(Self::Image), + "select" => Ok(Self::Select), + _ => Err("invalid value".into()), + } + } +} +impl ::std::convert::TryFrom<&str> for FieldMigrationFromType { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for FieldMigrationFromType { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +#[doc = "One option of a select field. Values are unique in the field (checked in code)."] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct FieldOption { + pub labels: ::std::collections::HashMap, + #[doc = "Stored in entries, never shown."] + pub value: Slug, +} +#[doc = "`FieldOptionLabelsValue`"] +#[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct FieldOptionLabelsValue(::std::string::String); +impl ::std::ops::Deref for FieldOptionLabelsValue { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: FieldOptionLabelsValue) -> Self { + value.0 + } +} +impl ::std::str::FromStr for FieldOptionLabelsValue { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> ::std::result::Result { + if value.chars().count() > 100usize { + return Err("longer than 100 characters".into()); + } + if value.chars().count() < 1usize { + return Err("shorter than 1 characters".into()); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for FieldOptionLabelsValue { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for FieldOptionLabelsValue { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for FieldOptionLabelsValue { + fn deserialize(deserializer: D) -> ::std::result::Result + where + D: ::serde::Deserializer<'de>, + { + ::std::string::String::deserialize(deserializer)? + .parse() + .map_err(|e: self::error::ConversionError| { + ::custom(e.to_string()) + }) + } +} #[doc = "`FooterColumn`"] #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, PartialEq)] #[serde(deny_unknown_fields)] diff --git a/packages/core-contracts/fixtures/invalid/site/field-boolean-required.json b/packages/core-contracts/fixtures/invalid/site/field-boolean-required.json new file mode 100644 index 00000000..9af76ba8 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-boolean-required.json @@ -0,0 +1,37 @@ +{ + "reason": "a boolean field cannot be required to publish (absent reads as false)", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "online", + "type": "boolean", + "labels": { + "en": { + "label": "Online" + } + }, + "localized": false, + "requiredToPublish": true + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-date-searchable.json b/packages/core-contracts/fixtures/invalid/site/field-date-searchable.json new file mode 100644 index 00000000..7ad75147 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-date-searchable.json @@ -0,0 +1,37 @@ +{ + "reason": "only text and rich text fields are searchable", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "deadline", + "type": "date", + "labels": { + "en": { + "label": "Deadline" + } + }, + "localized": false, + "searchable": true + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-help-too-long.json b/packages/core-contracts/fixtures/invalid/site/field-help-too-long.json new file mode 100644 index 00000000..97c6d2f1 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-help-too-long.json @@ -0,0 +1,37 @@ +{ + "reason": "help has at most 300 characters", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "Venue", + "help": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + }, + "localized": true + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-key-kebab-case.json b/packages/core-contracts/fixtures/invalid/site/field-key-kebab-case.json new file mode 100644 index 00000000..5deeba2d --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-key-kebab-case.json @@ -0,0 +1,36 @@ +{ + "reason": "a field key is lower camelCase", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "start-date", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-key-too-long.json b/packages/core-contracts/fixtures/invalid/site/field-key-too-long.json new file mode 100644 index 00000000..6061e233 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-key-too-long.json @@ -0,0 +1,36 @@ +{ + "reason": "a field key has at most 40 characters", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-key-upper-first.json b/packages/core-contracts/fixtures/invalid/site/field-key-upper-first.json new file mode 100644 index 00000000..3b4e7de5 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-key-upper-first.json @@ -0,0 +1,36 @@ +{ + "reason": "a field key starts with a lower-case letter", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "Venue", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-label-bad-locale.json b/packages/core-contracts/fixtures/invalid/site/field-label-bad-locale.json new file mode 100644 index 00000000..55bf8bdb --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-label-bad-locale.json @@ -0,0 +1,36 @@ +{ + "reason": "labels are keyed by BCP 47 locale", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "venue", + "type": "text", + "labels": { + "English": { + "label": "Venue" + } + }, + "localized": true + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-label-empty.json b/packages/core-contracts/fixtures/invalid/site/field-label-empty.json new file mode 100644 index 00000000..bcb84b95 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-label-empty.json @@ -0,0 +1,36 @@ +{ + "reason": "a label is not empty", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "" + } + }, + "localized": true + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-label-too-long.json b/packages/core-contracts/fixtures/invalid/site/field-label-too-long.json new file mode 100644 index 00000000..e7d6910c --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-label-too-long.json @@ -0,0 +1,36 @@ +{ + "reason": "a label has at most 100 characters", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + }, + "localized": true + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-labels-empty.json b/packages/core-contracts/fixtures/invalid/site/field-labels-empty.json new file mode 100644 index 00000000..d8130b93 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-labels-empty.json @@ -0,0 +1,32 @@ +{ + "reason": "labels name at least one locale", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "venue", + "type": "text", + "labels": {}, + "localized": true + } + ] + } + } + } +} 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 new file mode 100644 index 00000000..be54012c --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-migration-unknown-type.json @@ -0,0 +1,40 @@ +{ + "reason": "a migration's fromType is one of the nine types", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true, + "migration": { + "id": "snap_01HZX8K3Q9MG", + "fromType": "color" + } + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-migration-without-id.json b/packages/core-contracts/fixtures/invalid/site/field-migration-without-id.json new file mode 100644 index 00000000..f4654db1 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-migration-without-id.json @@ -0,0 +1,39 @@ +{ + "reason": "a migration marker names its operation", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true, + "migration": { + "fromKey": "place" + } + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-number-with-max-length.json b/packages/core-contracts/fixtures/invalid/site/field-number-with-max-length.json new file mode 100644 index 00000000..8bb41080 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-number-with-max-length.json @@ -0,0 +1,37 @@ +{ + "reason": "maxLength belongs to text fields only", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "Price" + } + }, + "localized": false, + "maxLength": 10 + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-option-value-not-slug.json b/packages/core-contracts/fixtures/invalid/site/field-option-value-not-slug.json new file mode 100644 index 00000000..f5157cd8 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-option-value-not-slug.json @@ -0,0 +1,44 @@ +{ + "reason": "an option value is a slug", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "format", + "type": "select", + "labels": { + "en": { + "label": "Format" + } + }, + "localized": false, + "options": [ + { + "value": "Talk Show", + "labels": { + "en": "Talk" + } + } + ] + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-option-without-labels.json b/packages/core-contracts/fixtures/invalid/site/field-option-without-labels.json new file mode 100644 index 00000000..6f028cce --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-option-without-labels.json @@ -0,0 +1,41 @@ +{ + "reason": "an option has labels", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "format", + "type": "select", + "labels": { + "en": { + "label": "Format" + } + }, + "localized": false, + "options": [ + { + "value": "talk" + } + ] + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-select-empty-options.json b/packages/core-contracts/fixtures/invalid/site/field-select-empty-options.json new file mode 100644 index 00000000..39fb3dfb --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-select-empty-options.json @@ -0,0 +1,37 @@ +{ + "reason": "a select field has at least one option", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "format", + "type": "select", + "labels": { + "en": { + "label": "Format" + } + }, + "localized": false, + "options": [] + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-select-too-many-options.json b/packages/core-contracts/fixtures/invalid/site/field-select-too-many-options.json new file mode 100644 index 00000000..d3095db2 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-select-too-many-options.json @@ -0,0 +1,344 @@ +{ + "reason": "a select field has at most 50 options", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "format", + "type": "select", + "labels": { + "en": { + "label": "Format" + } + }, + "localized": false, + "options": [ + { + "value": "o0", + "labels": { + "en": "O0" + } + }, + { + "value": "o1", + "labels": { + "en": "O1" + } + }, + { + "value": "o2", + "labels": { + "en": "O2" + } + }, + { + "value": "o3", + "labels": { + "en": "O3" + } + }, + { + "value": "o4", + "labels": { + "en": "O4" + } + }, + { + "value": "o5", + "labels": { + "en": "O5" + } + }, + { + "value": "o6", + "labels": { + "en": "O6" + } + }, + { + "value": "o7", + "labels": { + "en": "O7" + } + }, + { + "value": "o8", + "labels": { + "en": "O8" + } + }, + { + "value": "o9", + "labels": { + "en": "O9" + } + }, + { + "value": "o10", + "labels": { + "en": "O10" + } + }, + { + "value": "o11", + "labels": { + "en": "O11" + } + }, + { + "value": "o12", + "labels": { + "en": "O12" + } + }, + { + "value": "o13", + "labels": { + "en": "O13" + } + }, + { + "value": "o14", + "labels": { + "en": "O14" + } + }, + { + "value": "o15", + "labels": { + "en": "O15" + } + }, + { + "value": "o16", + "labels": { + "en": "O16" + } + }, + { + "value": "o17", + "labels": { + "en": "O17" + } + }, + { + "value": "o18", + "labels": { + "en": "O18" + } + }, + { + "value": "o19", + "labels": { + "en": "O19" + } + }, + { + "value": "o20", + "labels": { + "en": "O20" + } + }, + { + "value": "o21", + "labels": { + "en": "O21" + } + }, + { + "value": "o22", + "labels": { + "en": "O22" + } + }, + { + "value": "o23", + "labels": { + "en": "O23" + } + }, + { + "value": "o24", + "labels": { + "en": "O24" + } + }, + { + "value": "o25", + "labels": { + "en": "O25" + } + }, + { + "value": "o26", + "labels": { + "en": "O26" + } + }, + { + "value": "o27", + "labels": { + "en": "O27" + } + }, + { + "value": "o28", + "labels": { + "en": "O28" + } + }, + { + "value": "o29", + "labels": { + "en": "O29" + } + }, + { + "value": "o30", + "labels": { + "en": "O30" + } + }, + { + "value": "o31", + "labels": { + "en": "O31" + } + }, + { + "value": "o32", + "labels": { + "en": "O32" + } + }, + { + "value": "o33", + "labels": { + "en": "O33" + } + }, + { + "value": "o34", + "labels": { + "en": "O34" + } + }, + { + "value": "o35", + "labels": { + "en": "O35" + } + }, + { + "value": "o36", + "labels": { + "en": "O36" + } + }, + { + "value": "o37", + "labels": { + "en": "O37" + } + }, + { + "value": "o38", + "labels": { + "en": "O38" + } + }, + { + "value": "o39", + "labels": { + "en": "O39" + } + }, + { + "value": "o40", + "labels": { + "en": "O40" + } + }, + { + "value": "o41", + "labels": { + "en": "O41" + } + }, + { + "value": "o42", + "labels": { + "en": "O42" + } + }, + { + "value": "o43", + "labels": { + "en": "O43" + } + }, + { + "value": "o44", + "labels": { + "en": "O44" + } + }, + { + "value": "o45", + "labels": { + "en": "O45" + } + }, + { + "value": "o46", + "labels": { + "en": "O46" + } + }, + { + "value": "o47", + "labels": { + "en": "O47" + } + }, + { + "value": "o48", + "labels": { + "en": "O48" + } + }, + { + "value": "o49", + "labels": { + "en": "O49" + } + }, + { + "value": "o50", + "labels": { + "en": "O50" + } + } + ] + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-select-without-options.json b/packages/core-contracts/fixtures/invalid/site/field-select-without-options.json new file mode 100644 index 00000000..88c576d8 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-select-without-options.json @@ -0,0 +1,36 @@ +{ + "reason": "a select field declares its options", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "format", + "type": "select", + "labels": { + "en": { + "label": "Format" + } + }, + "localized": false + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-text-max-length-too-big.json b/packages/core-contracts/fixtures/invalid/site/field-text-max-length-too-big.json new file mode 100644 index 00000000..2aa9c908 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-text-max-length-too-big.json @@ -0,0 +1,37 @@ +{ + "reason": "a text field holds at most 5000 characters", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true, + "maxLength": 10000 + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-text-max-length-zero.json b/packages/core-contracts/fixtures/invalid/site/field-text-max-length-zero.json new file mode 100644 index 00000000..7ca6d856 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-text-max-length-zero.json @@ -0,0 +1,37 @@ +{ + "reason": "a text field's maxLength is at least 1", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true, + "maxLength": 0 + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-text-with-options.json b/packages/core-contracts/fixtures/invalid/site/field-text-with-options.json new file mode 100644 index 00000000..e75ecf96 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-text-with-options.json @@ -0,0 +1,44 @@ +{ + "reason": "options belong to select fields only", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true, + "options": [ + { + "value": "a", + "labels": { + "en": "A" + } + } + ] + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-unknown-property.json b/packages/core-contracts/fixtures/invalid/site/field-unknown-property.json new file mode 100644 index 00000000..3555970f --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-unknown-property.json @@ -0,0 +1,37 @@ +{ + "reason": "field definitions take no extra properties", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true, + "pattern": "^a+$" + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-unknown-type.json b/packages/core-contracts/fixtures/invalid/site/field-unknown-type.json new file mode 100644 index 00000000..541285b0 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-unknown-type.json @@ -0,0 +1,36 @@ +{ + "reason": "a field type is one of the nine closed types", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "venue", + "type": "color", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-without-labels.json b/packages/core-contracts/fixtures/invalid/site/field-without-labels.json new file mode 100644 index 00000000..9ebd51ed --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-without-labels.json @@ -0,0 +1,31 @@ +{ + "reason": "every field has labels", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "venue", + "type": "text", + "localized": true + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/field-without-localized.json b/packages/core-contracts/fixtures/invalid/site/field-without-localized.json new file mode 100644 index 00000000..a075f3ec --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/field-without-localized.json @@ -0,0 +1,35 @@ +{ + "reason": "every field decides whether it is localized", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + } + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/fields-empty.json b/packages/core-contracts/fixtures/invalid/site/fields-empty.json new file mode 100644 index 00000000..b51c1778 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/fields-empty.json @@ -0,0 +1,25 @@ +{ + "reason": "an empty fields list is written as no fields", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [] + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/fields-too-many.json b/packages/core-contracts/fixtures/invalid/site/fields-too-many.json new file mode 100644 index 00000000..34ff73c2 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/fields-too-many.json @@ -0,0 +1,336 @@ +{ + "reason": "a collection has at most 30 fields", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "fields": [ + { + "key": "field0", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field1", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field2", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field3", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field4", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field5", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field6", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field7", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field8", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field9", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field10", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field11", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field12", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field13", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field14", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field15", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field16", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field17", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field18", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field19", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field20", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field21", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field22", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field23", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field24", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field25", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field26", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field27", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field28", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field29", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + }, + { + "key": "field30", + "type": "text", + "labels": { + "en": { + "label": "Venue" + } + }, + "localized": true + } + ] + } + } + } +} diff --git a/packages/core-contracts/fixtures/valid/site/collection-fields-minimal.json b/packages/core-contracts/fixtures/valid/site/collection-fields-minimal.json new file mode 100644 index 00000000..cfae2c0d --- /dev/null +++ b/packages/core-contracts/fixtures/valid/site/collection-fields-minimal.json @@ -0,0 +1,121 @@ +{ + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "jobs": { + "fields": [ + { + "key": "field0", + "type": "text", + "labels": { + "en": { + "label": "Field 0" + } + }, + "localized": true + }, + { + "key": "field1", + "type": "richText", + "labels": { + "en": { + "label": "Field 1" + } + }, + "localized": false + }, + { + "key": "field2", + "type": "number", + "labels": { + "en": { + "label": "Field 2" + } + }, + "localized": true + }, + { + "key": "field3", + "type": "boolean", + "labels": { + "en": { + "label": "Field 3" + } + }, + "localized": false + }, + { + "key": "field4", + "type": "date", + "labels": { + "en": { + "label": "Field 4" + } + }, + "localized": true + }, + { + "key": "field5", + "type": "datetime", + "labels": { + "en": { + "label": "Field 5" + } + }, + "localized": false + }, + { + "key": "field6", + "type": "url", + "labels": { + "en": { + "label": "Field 6" + } + }, + "localized": true + }, + { + "key": "field7", + "type": "image", + "labels": { + "en": { + "label": "Field 7" + } + }, + "localized": false + }, + { + "key": "field8", + "type": "select", + "labels": { + "en": { + "label": "Field 8" + } + }, + "localized": false, + "options": [ + { + "value": "a", + "labels": { + "en": "A" + } + } + ] + } + ] + } + } +} diff --git a/packages/core-contracts/fixtures/valid/site/collection-fields.json b/packages/core-contracts/fixtures/valid/site/collection-fields.json new file mode 100644 index 00000000..8aa06f8d --- /dev/null +++ b/packages/core-contracts/fixtures/valid/site/collection-fields.json @@ -0,0 +1,223 @@ +{ + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Minimal", + "slug": "minimal", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "posts": {}, + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "Venue", + "help": "Where it happens." + }, + "tr": { + "label": "Mekan" + } + }, + "localized": true, + "requiredToPublish": true, + "maxLength": 200, + "multiline": false, + "searchable": true + }, + { + "key": "notes", + "type": "text", + "labels": { + "en": { + "label": "Internal notes" + } + }, + "localized": false, + "multiline": true + }, + { + "key": "agenda", + "type": "richText", + "labels": { + "en": { + "label": "Agenda" + } + }, + "localized": true, + "requiredToPublish": false, + "searchable": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "Price", + "help": "In euros." + } + }, + "localized": false, + "integer": false, + "minimum": 0.5, + "maximum": 999.5 + }, + { + "key": "seats", + "type": "number", + "labels": { + "en": { + "label": "Seats" + } + }, + "localized": false, + "integer": true + }, + { + "key": "online", + "type": "boolean", + "labels": { + "en": { + "label": "Online" + } + }, + "localized": false + }, + { + "key": "deadline", + "type": "date", + "labels": { + "en": { + "label": "Sign-up deadline" + } + }, + "localized": false, + "requiredToPublish": true + }, + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "Starts" + } + }, + "localized": false, + "requiredToPublish": true + }, + { + "key": "ticketUrl", + "type": "url", + "labels": { + "en": { + "label": "Tickets" + } + }, + "localized": true + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "Poster" + } + }, + "localized": false + }, + { + "key": "format", + "type": "select", + "labels": { + "en": { + "label": "Format" + } + }, + "localized": false, + "requiredToPublish": true, + "multiple": true, + "options": [ + { + "value": "talk", + "labels": { + "en": "Talk", + "tr": "Konuşma" + } + }, + { + "value": "workshop", + "labels": { + "en": "Workshop" + } + } + ] + }, + { + "key": "level", + "type": "select", + "labels": { + "en": { + "label": "Level" + } + }, + "localized": false, + "multiple": false, + "options": [ + { + "value": "beginner", + "labels": { + "en": "Beginner" + } + } + ] + }, + { + "key": "speaker", + "type": "text", + "labels": { + "en": { + "label": "Speaker" + } + }, + "localized": false, + "migration": { + "id": "snap_01HZX8K3Q9MG", + "fromKey": "presenter", + "fromType": "richText", + "fromLocalized": true + } + }, + { + "key": "room", + "type": "text", + "labels": { + "en": { + "label": "Room" + } + }, + "localized": false, + "migration": { + "id": "snap_01HZX8K3Q9MH" + } + } + ] + } + } +} diff --git a/packages/core-contracts/schemas/content-entry.schema.json b/packages/core-contracts/schemas/content-entry.schema.json index 6194cc8b..09035ea4 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; any JSON.", + "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.", "additionalProperties": true }, "tags": { diff --git a/packages/core-contracts/schemas/site.schema.json b/packages/core-contracts/schemas/site.schema.json index 0b901926..2eaafb58 100644 --- a/packages/core-contracts/schemas/site.schema.json +++ b/packages/core-contracts/schemas/site.schema.json @@ -104,6 +104,15 @@ "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" + } } } }, @@ -117,6 +126,405 @@ "description": { "type": "string", "maxLength": 1000 } } }, + "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}$" + }, + "FieldLabels": { + "type": "object", + "description": "The field's label and help per locale. Missing a locale: the default locale's, then the key.", + "propertyNames": { + "$ref": "common.schema.json#/$defs/Locale" + }, + "minProperties": 1, + "additionalProperties": { + "$ref": "#/$defs/FieldLabel" + } + }, + "FieldLabel": { + "type": "object", + "additionalProperties": false, + "required": ["label"], + "properties": { + "label": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "help": { + "type": "string", + "maxLength": 300 + } + } + }, + "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": "common.schema.json#/$defs/Slug", + "description": "Stored in entries, never shown." + }, + "labels": { + "type": "object", + "propertyNames": { + "$ref": "common.schema.json#/$defs/Locale" + }, + "minProperties": 1, + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 100 + } + } + } + }, + "FieldMigration": { + "type": "object", + "description": "RFC-0041 §6: present only while a field operation rewrites entries.", + "additionalProperties": false, + "required": ["id"], + "properties": { + "id": { + "$ref": "common.schema.json#/$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" + } + } + }, "Brand": { "type": "object", "description": "The site's logo and social links (RFC-0025 \u00a71), shared by the header, the footer and SEO.", diff --git a/packages/core-contracts/src/generated/content-entry.ts b/packages/core-contracts/src/generated/content-entry.ts index b6d8d553..bae213b8 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; any JSON. + * 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. */ fields: { [k: string]: unknown | undefined; diff --git a/packages/core-contracts/src/generated/index.ts b/packages/core-contracts/src/generated/index.ts index f3c23f6f..d6f81151 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, Section, BodyProfile, ActorKind, BodyBlocks, SurfaceComponentId, Path, Section1, TimeZone, Site, Domain, Localization, CollectionSettings, CollectionLabel, ThemeRef, Design, Brand, ImageRef, SocialLink, Chrome, ChromeHeader, HeaderAction, ChromeFooter, FooterColumn, HeroSection, Link, RichTextSection, BodyBlock, Actor, ImageSection, ContentListSection, ComponentSection, SurfaceSection, SurfaceComponent, Announcement, Consent } from "./site.ts"; +export type { Locale, FieldDefinition, FieldKey, 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, ThemeRef, Design, Brand, ImageRef, SocialLink, Chrome, ChromeHeader, HeaderAction, ChromeFooter, FooterColumn, HeroSection, Link, RichTextSection, BodyBlock, Actor, ImageSection, ContentListSection, 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 16acf4b6..51cd9585 100644 --- a/packages/core-contracts/src/generated/schemas.ts +++ b/packages/core-contracts/src/generated/schemas.ts @@ -625,6 +625,15 @@ export const SiteSchema = { "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" + } } } }, @@ -681,6 +690,476 @@ export const SiteSchema = { } } }, + "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 + } + } + } + }, "FooterColumn": { "type": "object", "additionalProperties": false, @@ -2468,7 +2947,7 @@ export const ContentEntrySchema = { }, "fields": { "type": "object", - "description": "Collection-specific values; any JSON.", + "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.", "additionalProperties": true }, "tags": { diff --git a/packages/core-contracts/src/generated/site.ts b/packages/core-contracts/src/generated/site.ts index 14bbad78..91c90970 100644 --- a/packages/core-contracts/src/generated/site.ts +++ b/packages/core-contracts/src/generated/site.ts @@ -33,6 +33,29 @@ export type Slug = string; * via the `definition` "Locale". */ export type Locale = string; +/** + * RFC-0041: one typed custom field of a collection, discriminated by `type`. + * + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "FieldDefinition". + */ +export type FieldDefinition = + | TextField + | RichTextField + | NumberField + | BooleanField + | DateField + | DateTimeField + | UrlField + | ImageField + | SelectField; +/** + * 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). + * + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "FieldKey". + */ +export type FieldKey = string; /** * 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. */ @@ -230,6 +253,13 @@ export interface CollectionSettings { * RFC-0040: default order where none is given (Studio's list, entry queries without sort, MCP list_entries). Absent: newest. */ sort?: "newest" | "oldest" | "title"; + /** + * 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 + */ + fields?: [FieldDefinition, ...FieldDefinition[]]; } /** * This interface was referenced by `Site`'s JSON-Schema @@ -240,6 +270,234 @@ export interface CollectionLabel { plural: string; description?: string; } +/** + * Plain text: a string of at most maxLength characters. + */ +export interface TextField { + key: FieldKey; + type: "text"; + labels: FieldLabels; + /** + * true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it. + */ + localized: boolean; + /** + * An entry in review or published needs a value. Absent: false. + */ + requiredToPublish?: boolean; + migration?: FieldMigration; + /** + * Absent: 5000. + */ + maxLength?: number; + /** + * Line breaks allowed. Absent: false. + */ + multiline?: boolean; + /** + * Its words go into site search. Absent: false. + */ + searchable?: boolean; +} +/** + * The field's label and help per locale. Missing a locale: the default locale's, then the key. + * + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "FieldLabels". + */ +export interface FieldLabels { + [k: string]: FieldLabel | undefined; +} +/** + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "FieldLabel". + */ +export interface FieldLabel { + label: string; + help?: string; +} +/** + * RFC-0041 §6: present only while a field operation rewrites entries. + * + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "FieldMigration". + */ +export interface 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. + */ + id: string; + fromKey?: FieldKey; + fromType?: + "text" | "richText" | "number" | "boolean" | "date" | "datetime" | "url" | "image" | "select"; + fromLocalized?: boolean; +} +/** + * Cloudhal Markdown v1 (ADR-0028), at most 50 000 characters. + */ +export interface RichTextField { + key: FieldKey; + type: "richText"; + labels: FieldLabels; + /** + * true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it. + */ + localized: boolean; + /** + * An entry in review or published needs a value. Absent: false. + */ + requiredToPublish?: boolean; + migration?: FieldMigration; + /** + * Its words go into site search. Absent: false. + */ + searchable?: boolean; +} +/** + * A finite number. + */ +export interface NumberField { + key: FieldKey; + type: "number"; + labels: FieldLabels; + /** + * true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it. + */ + localized: boolean; + /** + * An entry in review or published needs a value. Absent: false. + */ + requiredToPublish?: boolean; + migration?: FieldMigration; + /** + * Whole numbers only. Absent: false. + */ + integer?: boolean; + minimum?: number; + maximum?: number; +} +/** + * true or false; absent reads as false, so it cannot be required to publish. + */ +export interface BooleanField { + key: FieldKey; + type: "boolean"; + labels: FieldLabels; + /** + * true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it. + */ + localized: boolean; + migration?: FieldMigration; +} +/** + * A calendar day without a zone (YYYY-MM-DD). + */ +export interface DateField { + key: FieldKey; + type: "date"; + labels: FieldLabels; + /** + * true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it. + */ + localized: boolean; + /** + * An entry in review or published needs a value. Absent: false. + */ + requiredToPublish?: boolean; + migration?: FieldMigration; +} +/** + * An instant with the zone it was entered in: {at, timeZone} (RFC-0008 §9.1). + */ +export interface DateTimeField { + key: FieldKey; + type: "datetime"; + labels: FieldLabels; + /** + * true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it. + */ + localized: boolean; + /** + * An entry in review or published needs a value. Absent: false. + */ + requiredToPublish?: boolean; + migration?: FieldMigration; +} +/** + * A link: https, http, mailto, tel, a site-relative path or a #fragment; at most 2048 characters. + */ +export interface UrlField { + key: FieldKey; + type: "url"; + labels: FieldLabels; + /** + * true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it. + */ + localized: boolean; + /** + * An entry in review or published needs a value. Absent: false. + */ + requiredToPublish?: boolean; + migration?: FieldMigration; +} +/** + * An image Asset of the same site, by id (asset_…). + */ +export interface ImageField { + key: FieldKey; + type: "image"; + labels: FieldLabels; + /** + * true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it. + */ + localized: boolean; + /** + * An entry in review or published needs a value. Absent: false. + */ + requiredToPublish?: boolean; + migration?: FieldMigration; +} +/** + * One of a closed list of options, or several when multiple. + */ +export interface SelectField { + key: FieldKey; + type: "select"; + labels: FieldLabels; + /** + * true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it. + */ + localized: boolean; + /** + * An entry in review or published needs a value. Absent: false. + */ + requiredToPublish?: boolean; + migration?: FieldMigration; + /** + * @minItems 1 + * @maxItems 50 + */ + options: [FieldOption, ...FieldOption[]]; + /** + * The value is a list of option values. Absent: false. + */ + multiple?: boolean; +} +/** + * One option of a select field. Values are unique in the field (checked in code). + * + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "FieldOption". + */ +export interface FieldOption { + /** + * Lower-case URL segment. + */ + value: string; + labels: { + [k: string]: string | undefined; + }; +} /** * Theme (a.k.a. Experience) applied by the renderer. Config is theme-defined and validated by the theme's own schema, not here. * diff --git a/packages/core-contracts/src/generated/validators.js b/packages/core-contracts/src/generated/validators.js index 5ece1a23..3474c469 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"}}}},"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"]}}},"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"}}},"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}}},{"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}}}]},"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}}},"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"}}}},"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"}}}},"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"}}},"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}}}},"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}}},{"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}}}]},"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}}},"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 schema59 = {"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 schema95 = {"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 schema97 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; -const schema83 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; -const schema100 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const schema78 = {"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 schema114 = {"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 schema116 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema102 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema119 = {"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 pattern45 = new RegExp("^(?:UTC|[A-Z][A-Za-z_-]+(?:/[A-Za-z0-9_+-]+){1,2})$", "u"); +const pattern59 = 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={}}={}){ @@ -842,10 +842,2205 @@ return errors === 0; } validate22.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const schema54 = {"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"]}}}; +const schema54 = {"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"}}}}; const schema56 = {"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 pattern19 = new RegExp("^(/[a-z0-9]+(-[a-z0-9]+)*)+/\\{slug\\}$", "u"); const pattern21 = new RegExp("^[a-z0-9]+(-[a-z0-9]+)*$", "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 schema58 = {"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}$"}; +const pattern22 = new RegExp("^[a-z][A-Za-z0-9]{0,39}$", "u"); +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}}}; + +function validate26(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +let vErrors = null; +let errors = 0; +const evaluated0 = validate26.evaluated; +if(evaluated0.dynamicProps){ +evaluated0.props = undefined; +} +if(evaluated0.dynamicItems){ +evaluated0.items = undefined; +} +if(data && typeof data == "object" && !Array.isArray(data)){ +if(Object.keys(data).length < 1){ +const err0 = {instancePath,schemaPath:"#/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"}; +if(vErrors === null){ +vErrors = [err0]; +} +else { +vErrors.push(err0); +} +errors++; +} +for(const key0 in data){ +const _errs1 = errors; +if(typeof key0 === "string"){ +if(!pattern11.test(key0)){ +const err1 = {instancePath,schemaPath:"#/$defs/Locale/pattern",keyword:"pattern",params:{pattern: "^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},message:"must match pattern \""+"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"+"\"",propertyName:key0}; +if(vErrors === null){ +vErrors = [err1]; +} +else { +vErrors.push(err1); +} +errors++; +} +} +else { +const err2 = {instancePath,schemaPath:"#/$defs/Locale/type",keyword:"type",params:{type: "string"},message:"must be string",propertyName:key0}; +if(vErrors === null){ +vErrors = [err2]; +} +else { +vErrors.push(err2); +} +errors++; +} +var valid0 = _errs1 === errors; +if(!valid0){ +const err3 = {instancePath,schemaPath:"#/propertyNames",keyword:"propertyNames",params:{propertyName: key0},message:"property name must be valid"}; +if(vErrors === null){ +vErrors = [err3]; +} +else { +vErrors.push(err3); +} +errors++; +} +} +for(const key1 in data){ +let data0 = data[key1]; +if(data0 && typeof data0 == "object" && !Array.isArray(data0)){ +if(data0.label === undefined){ +const err4 = {instancePath:instancePath+"/" + key1.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/$defs/FieldLabel/required",keyword:"required",params:{missingProperty: "label"},message:"must have required property '"+"label"+"'"}; +if(vErrors === null){ +vErrors = [err4]; +} +else { +vErrors.push(err4); +} +errors++; +} +for(const key2 in data0){ +if(!((key2 === "label") || (key2 === "help"))){ +const err5 = {instancePath:instancePath+"/" + key1.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/$defs/FieldLabel/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key2},message:"must NOT have additional properties"}; +if(vErrors === null){ +vErrors = [err5]; +} +else { +vErrors.push(err5); +} +errors++; +} +} +if(data0.label !== undefined){ +let data1 = data0.label; +if(typeof data1 === "string"){ +if(func2(data1) > 100){ +const err6 = {instancePath:instancePath+"/" + key1.replace(/~/g, "~0").replace(/\//g, "~1")+"/label",schemaPath:"#/$defs/FieldLabel/properties/label/maxLength",keyword:"maxLength",params:{limit: 100},message:"must NOT have more than 100 characters"}; +if(vErrors === null){ +vErrors = [err6]; +} +else { +vErrors.push(err6); +} +errors++; +} +if(func2(data1) < 1){ +const err7 = {instancePath:instancePath+"/" + key1.replace(/~/g, "~0").replace(/\//g, "~1")+"/label",schemaPath:"#/$defs/FieldLabel/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}; +if(vErrors === null){ +vErrors = [err7]; +} +else { +vErrors.push(err7); +} +errors++; +} +} +else { +const err8 = {instancePath:instancePath+"/" + key1.replace(/~/g, "~0").replace(/\//g, "~1")+"/label",schemaPath:"#/$defs/FieldLabel/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err8]; +} +else { +vErrors.push(err8); +} +errors++; +} +} +if(data0.help !== undefined){ +let data2 = data0.help; +if(typeof data2 === "string"){ +if(func2(data2) > 300){ +const err9 = {instancePath:instancePath+"/" + key1.replace(/~/g, "~0").replace(/\//g, "~1")+"/help",schemaPath:"#/$defs/FieldLabel/properties/help/maxLength",keyword:"maxLength",params:{limit: 300},message:"must NOT have more than 300 characters"}; +if(vErrors === null){ +vErrors = [err9]; +} +else { +vErrors.push(err9); +} +errors++; +} +} +else { +const err10 = {instancePath:instancePath+"/" + key1.replace(/~/g, "~0").replace(/\//g, "~1")+"/help",schemaPath:"#/$defs/FieldLabel/properties/help/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err10]; +} +else { +vErrors.push(err10); +} +errors++; +} +} +} +else { +const err11 = {instancePath:instancePath+"/" + key1.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/$defs/FieldLabel/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err11]; +} +else { +vErrors.push(err11); +} +errors++; +} +} +} +else { +const err12 = {instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err12]; +} +else { +vErrors.push(err12); +} +errors++; +} +validate26.errors = vErrors; +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"}}}; + +function validate28(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +let vErrors = null; +let errors = 0; +const evaluated0 = validate28.evaluated; +if(evaluated0.dynamicProps){ +evaluated0.props = undefined; +} +if(evaluated0.dynamicItems){ +evaluated0.items = undefined; +} +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.id === undefined){ +const err0 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "id"},message:"must have required property '"+"id"+"'"}; +if(vErrors === null){ +vErrors = [err0]; +} +else { +vErrors.push(err0); +} +errors++; +} +for(const key0 in data){ +if(!((((key0 === "id") || (key0 === "fromKey")) || (key0 === "fromType")) || (key0 === "fromLocalized"))){ +const err1 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; +if(vErrors === null){ +vErrors = [err1]; +} +else { +vErrors.push(err1); +} +errors++; +} +} +if(data.id !== undefined){ +let data0 = data.id; +if(typeof data0 === "string"){ +if(func2(data0) > 48){ +const err2 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/maxLength",keyword:"maxLength",params:{limit: 48},message:"must NOT have more than 48 characters"}; +if(vErrors === null){ +vErrors = [err2]; +} +else { +vErrors.push(err2); +} +errors++; +} +if(func2(data0) < 8){ +const err3 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/minLength",keyword:"minLength",params:{limit: 8},message:"must NOT have fewer than 8 characters"}; +if(vErrors === null){ +vErrors = [err3]; +} +else { +vErrors.push(err3); +} +errors++; +} +if(!pattern4.test(data0)){ +const err4 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/pattern",keyword:"pattern",params:{pattern: "^[a-z]+_[A-Za-z0-9]{6,32}$"},message:"must match pattern \""+"^[a-z]+_[A-Za-z0-9]{6,32}$"+"\""}; +if(vErrors === null){ +vErrors = [err4]; +} +else { +vErrors.push(err4); +} +errors++; +} +} +else { +const err5 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err5]; +} +else { +vErrors.push(err5); +} +errors++; +} +} +if(data.fromKey !== undefined){ +let data1 = data.fromKey; +if(typeof data1 === "string"){ +if(!pattern22.test(data1)){ +const err6 = {instancePath:instancePath+"/fromKey",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 = [err6]; +} +else { +vErrors.push(err6); +} +errors++; +} +} +else { +const err7 = {instancePath:instancePath+"/fromKey",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err7]; +} +else { +vErrors.push(err7); +} +errors++; +} +} +if(data.fromType !== undefined){ +let data2 = data.fromType; +if(typeof data2 !== "string"){ +const err8 = {instancePath:instancePath+"/fromType",schemaPath:"#/properties/fromType/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err8]; +} +else { +vErrors.push(err8); +} +errors++; +} +if(!(((((((((data2 === "text") || (data2 === "richText")) || (data2 === "number")) || (data2 === "boolean")) || (data2 === "date")) || (data2 === "datetime")) || (data2 === "url")) || (data2 === "image")) || (data2 === "select"))){ +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]; +} +else { +vErrors.push(err9); +} +errors++; +} +} +if(data.fromLocalized !== undefined){ +if(typeof data.fromLocalized !== "boolean"){ +const err10 = {instancePath:instancePath+"/fromLocalized",schemaPath:"#/properties/fromLocalized/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}; +if(vErrors === null){ +vErrors = [err10]; +} +else { +vErrors.push(err10); +} +errors++; +} +} +} +else { +const err11 = {instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err11]; +} +else { +vErrors.push(err11); +} +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}}}}; + +function validate46(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +let vErrors = null; +let errors = 0; +const evaluated0 = validate46.evaluated; +if(evaluated0.dynamicProps){ +evaluated0.props = undefined; +} +if(evaluated0.dynamicItems){ +evaluated0.items = undefined; +} +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.value === undefined){ +const err0 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "value"},message:"must have required property '"+"value"+"'"}; +if(vErrors === null){ +vErrors = [err0]; +} +else { +vErrors.push(err0); +} +errors++; +} +if(data.labels === undefined){ +const err1 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "labels"},message:"must have required property '"+"labels"+"'"}; +if(vErrors === null){ +vErrors = [err1]; +} +else { +vErrors.push(err1); +} +errors++; +} +for(const key0 in data){ +if(!((key0 === "value") || (key0 === "labels"))){ +const err2 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; +if(vErrors === null){ +vErrors = [err2]; +} +else { +vErrors.push(err2); +} +errors++; +} +} +if(data.value !== undefined){ +let data0 = data.value; +if(typeof data0 === "string"){ +if(func2(data0) > 128){ +const err3 = {instancePath:instancePath+"/value",schemaPath:"#/$defs/Slug/maxLength",keyword:"maxLength",params:{limit: 128},message:"must NOT have more than 128 characters"}; +if(vErrors === null){ +vErrors = [err3]; +} +else { +vErrors.push(err3); +} +errors++; +} +if(func2(data0) < 1){ +const err4 = {instancePath:instancePath+"/value",schemaPath:"#/$defs/Slug/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}; +if(vErrors === null){ +vErrors = [err4]; +} +else { +vErrors.push(err4); +} +errors++; +} +if(!pattern5.test(data0)){ +const err5 = {instancePath:instancePath+"/value",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 = [err5]; +} +else { +vErrors.push(err5); +} +errors++; +} +} +else { +const err6 = {instancePath:instancePath+"/value",schemaPath:"#/$defs/Slug/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err6]; +} +else { +vErrors.push(err6); +} +errors++; +} +} +if(data.labels !== undefined){ +let data1 = data.labels; +if(data1 && typeof data1 == "object" && !Array.isArray(data1)){ +if(Object.keys(data1).length < 1){ +const err7 = {instancePath:instancePath+"/labels",schemaPath:"#/properties/labels/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"}; +if(vErrors === null){ +vErrors = [err7]; +} +else { +vErrors.push(err7); +} +errors++; +} +for(const key1 in data1){ +const _errs7 = errors; +if(typeof key1 === "string"){ +if(!pattern11.test(key1)){ +const err8 = {instancePath:instancePath+"/labels",schemaPath:"#/$defs/Locale/pattern",keyword:"pattern",params:{pattern: "^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},message:"must match pattern \""+"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"+"\"",propertyName:key1}; +if(vErrors === null){ +vErrors = [err8]; +} +else { +vErrors.push(err8); +} +errors++; +} +} +else { +const err9 = {instancePath:instancePath+"/labels",schemaPath:"#/$defs/Locale/type",keyword:"type",params:{type: "string"},message:"must be string",propertyName:key1}; +if(vErrors === null){ +vErrors = [err9]; +} +else { +vErrors.push(err9); +} +errors++; +} +var valid2 = _errs7 === errors; +if(!valid2){ +const err10 = {instancePath:instancePath+"/labels",schemaPath:"#/properties/labels/propertyNames",keyword:"propertyNames",params:{propertyName: key1},message:"property name must be valid"}; +if(vErrors === null){ +vErrors = [err10]; +} +else { +vErrors.push(err10); +} +errors++; +} +} +for(const key2 in data1){ +let data2 = data1[key2]; +if(typeof data2 === "string"){ +if(func2(data2) > 100){ +const err11 = {instancePath:instancePath+"/labels/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/labels/additionalProperties/maxLength",keyword:"maxLength",params:{limit: 100},message:"must NOT have more than 100 characters"}; +if(vErrors === null){ +vErrors = [err11]; +} +else { +vErrors.push(err11); +} +errors++; +} +if(func2(data2) < 1){ +const err12 = {instancePath:instancePath+"/labels/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/labels/additionalProperties/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}; +if(vErrors === null){ +vErrors = [err12]; +} +else { +vErrors.push(err12); +} +errors++; +} +} +else { +const err13 = {instancePath:instancePath+"/labels/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/labels/additionalProperties/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err13]; +} +else { +vErrors.push(err13); +} +errors++; +} +} +} +else { +const err14 = {instancePath:instancePath+"/labels",schemaPath:"#/properties/labels/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err14]; +} +else { +vErrors.push(err14); +} +errors++; +} +} +} +else { +const err15 = {instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err15]; +} +else { +vErrors.push(err15); +} +errors++; +} +validate46.errors = vErrors; +return errors === 0; +} +validate46.evaluated = {"props":true,"dynamicProps":false,"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 30){ +const err27 = {instancePath:instancePath+"/fields",schemaPath:"#/properties/fields/maxItems",keyword:"maxItems",params:{limit: 30},message:"must NOT have more than 30 items"}; if(vErrors === null){ vErrors = [err27]; } @@ -1191,17 +3388,57 @@ vErrors.push(err27); } errors++; } +if(data10.length < 1){ +const err28 = {instancePath:instancePath+"/fields",schemaPath:"#/properties/fields/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}; +if(vErrors === null){ +vErrors = [err28]; +} +else { +vErrors.push(err28); +} +errors++; +} +const len0 = data10.length; +for(let i0=0; i0*'.","minLength":1,"maxLength":512}}},{"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 schema75 = {"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 schema77 = {"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 pattern37 = new RegExp("^[a-z][a-z0-9-]{1,40}$", "u"); -const pattern39 = new RegExp("^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$", "u"); -const schema78 = {"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 schema79 = {"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 pattern30 = new RegExp("^blk_[0-9A-Za-z]{8,24}$", "u"); -const pattern31 = new RegExp("^[0-9a-f]{64}$", "u"); -const schema80 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; -const schema81 = {"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 schema92 = {"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}}},{"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 schema94 = {"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 schema96 = {"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 pattern51 = new RegExp("^[a-z][a-z0-9-]{1,40}$", "u"); +const pattern53 = new RegExp("^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$", "u"); +const schema97 = {"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 schema98 = {"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 pattern44 = new RegExp("^blk_[0-9A-Za-z]{8,24}$", "u"); +const pattern45 = new RegExp("^[0-9a-f]{64}$", "u"); +const schema99 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; +const schema100 = {"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 validate43(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate67(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate43.evaluated; +const evaluated0 = validate67.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -2452,7 +4689,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: schema81.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema100.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -2543,16 +4780,16 @@ vErrors.push(err11); } errors++; } -validate43.errors = vErrors; +validate67.errors = vErrors; return errors === 0; } -validate43.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate67.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate42(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate66(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate42.evaluated; +const evaluated0 = validate66.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -2595,7 +4832,7 @@ errors++; if(data.id !== undefined){ let data0 = data.id; if(typeof data0 === "string"){ -if(!pattern30.test(data0)){ +if(!pattern44.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]; @@ -2620,7 +4857,7 @@ errors++; if(data.hash !== undefined){ let data1 = data.hash; if(typeof data1 === "string"){ -if(!pattern31.test(data1)){ +if(!pattern45.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]; @@ -2643,8 +4880,8 @@ errors++; } } if(data.by !== undefined){ -if(!(validate43(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate43.errors : vErrors.concat(validate43.errors); +if(!(validate67(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate67.errors : vErrors.concat(validate67.errors); errors = vErrors.length; } } @@ -2661,7 +4898,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: schema79.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: schema98.properties.via.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err8]; } @@ -2707,16 +4944,16 @@ vErrors.push(err11); } errors++; } -validate42.errors = vErrors; +validate66.errors = vErrors; return errors === 0; } -validate42.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate66.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate41(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate65(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate41.evaluated; +const evaluated0 = validate65.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -2736,8 +4973,8 @@ errors++; } const len0 = data.length; for(let i0=0; i0*'.","minLength":1,"maxLength":512}}},{"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 schema102 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; -const schema103 = {"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 schema105 = {"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 schema106 = {"type":"string","description":"Editorial lifecycle of a page or content entry.","enum":["draft","review","published","archived"]}; -const schema132 = {"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 schema133 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; -const schema119 = {"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 schema107 = {"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}}},{"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 schema111 = {"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 schema113 = {"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 schema122 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; -const schema109 = {"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 = validate79; +const schema120 = {"$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."},"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}}},{"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 schema121 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; +const schema122 = {"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 schema124 = {"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 schema125 = {"type":"string","description":"Editorial lifecycle of a page or content entry.","enum":["draft","review","published","archived"]}; +const schema151 = {"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 schema152 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema138 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema165 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const schema126 = {"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}}},{"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 schema130 = {"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 schema132 = {"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 schema141 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; +const schema128 = {"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 validate57(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate81(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate57.evaluated; +const evaluated0 = validate81.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -6544,20 +8781,20 @@ vErrors.push(err16); } errors++; } -validate57.errors = vErrors; +validate81.errors = vErrors; return errors === 0; } -validate57.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate81.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const schema114 = {"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 schema115 = {"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 schema116 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; -const schema117 = {"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 schema133 = {"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 schema134 = {"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 schema135 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; +const schema136 = {"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 validate61(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate85(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate61.evaluated; +const evaluated0 = validate85.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -6610,7 +8847,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: schema117.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema136.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -6701,16 +8938,16 @@ vErrors.push(err11); } errors++; } -validate61.errors = vErrors; +validate85.errors = vErrors; return errors === 0; } -validate61.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate85.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate60(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate84(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate60.evaluated; +const evaluated0 = validate84.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -6753,7 +8990,7 @@ errors++; if(data.id !== undefined){ let data0 = data.id; if(typeof data0 === "string"){ -if(!pattern30.test(data0)){ +if(!pattern44.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]; @@ -6778,7 +9015,7 @@ errors++; if(data.hash !== undefined){ let data1 = data.hash; if(typeof data1 === "string"){ -if(!pattern31.test(data1)){ +if(!pattern45.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]; @@ -6801,8 +9038,8 @@ errors++; } } if(data.by !== undefined){ -if(!(validate61(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate61.errors : vErrors.concat(validate61.errors); +if(!(validate85(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate85.errors : vErrors.concat(validate85.errors); errors = vErrors.length; } } @@ -6819,7 +9056,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: schema115.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: schema134.properties.via.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err8]; } @@ -6865,16 +9102,16 @@ vErrors.push(err11); } errors++; } -validate60.errors = vErrors; +validate84.errors = vErrors; return errors === 0; } -validate60.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate84.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate59(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate83(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate59.evaluated; +const evaluated0 = validate83.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -6894,8 +9131,8 @@ errors++; } const len0 = data.length; for(let i0=0; i0*'.","minLength":1,"maxLength":512}}},{"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."},"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 schema148 = {"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":"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 schema153 = {"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 schema162 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; -const schema149 = {"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 = validate101; +const schema166 = {"$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}}},{"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."},"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 schema167 = {"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 schema170 = {"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 schema172 = {"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 schema181 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; +const schema168 = {"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 validate78(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate102(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate78.evaluated; +const evaluated0 = validate102.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -10354,21 +12591,21 @@ vErrors.push(err16); } errors++; } -validate78.errors = vErrors; +validate102.errors = vErrors; return errors === 0; } -validate78.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate102.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const schema154 = {"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 schema155 = {"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 schema159 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; -const schema156 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; -const schema157 = {"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 schema173 = {"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 schema174 = {"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 schema178 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema175 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; +const schema176 = {"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 validate82(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate106(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate82.evaluated; +const evaluated0 = validate106.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -10421,7 +12658,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: schema157.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema176.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -10512,16 +12749,16 @@ vErrors.push(err11); } errors++; } -validate82.errors = vErrors; +validate106.errors = vErrors; return errors === 0; } -validate82.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate106.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate81(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate105(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate81.evaluated; +const evaluated0 = validate105.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -10564,7 +12801,7 @@ errors++; if(data.id !== undefined){ let data0 = data.id; if(typeof data0 === "string"){ -if(!pattern30.test(data0)){ +if(!pattern44.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]; @@ -10589,7 +12826,7 @@ errors++; if(data.hash !== undefined){ let data1 = data.hash; if(typeof data1 === "string"){ -if(!pattern31.test(data1)){ +if(!pattern45.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]; @@ -10612,8 +12849,8 @@ errors++; } } if(data.by !== undefined){ -if(!(validate82(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate82.errors : vErrors.concat(validate82.errors); +if(!(validate106(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate106.errors : vErrors.concat(validate106.errors); errors = vErrors.length; } } @@ -10630,7 +12867,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: schema155.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: schema174.properties.via.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err8]; } @@ -10676,16 +12913,16 @@ vErrors.push(err11); } errors++; } -validate81.errors = vErrors; +validate105.errors = vErrors; return errors === 0; } -validate81.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate105.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate80(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate104(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate80.evaluated; +const evaluated0 = validate104.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -10705,8 +12942,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 schema188 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; +const schema189 = {"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 schema191 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; +const schema193 = {"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 schema194 = {"type":"string","description":"Editorial lifecycle of a page or content entry.","enum":["draft","review","published","archived"]}; +const schema202 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema214 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema227 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const schema195 = {"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 schema196 = {"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 schema197 = {"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 schema198 = {"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 schema199 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; +const schema200 = {"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 validate93(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate117(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate93.evaluated; +const evaluated0 = validate117.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -12476,7 +14713,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: schema181.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema200.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -12567,16 +14804,16 @@ vErrors.push(err11); } errors++; } -validate93.errors = vErrors; +validate117.errors = vErrors; return errors === 0; } -validate93.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate117.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate92(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate116(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate92.evaluated; +const evaluated0 = validate116.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -12619,7 +14856,7 @@ errors++; if(data.id !== undefined){ let data0 = data.id; if(typeof data0 === "string"){ -if(!pattern30.test(data0)){ +if(!pattern44.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]; @@ -12644,7 +14881,7 @@ errors++; if(data.hash !== undefined){ let data1 = data.hash; if(typeof data1 === "string"){ -if(!pattern31.test(data1)){ +if(!pattern45.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]; @@ -12667,8 +14904,8 @@ errors++; } } if(data.by !== undefined){ -if(!(validate93(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate93.errors : vErrors.concat(validate93.errors); +if(!(validate117(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate117.errors : vErrors.concat(validate117.errors); errors = vErrors.length; } } @@ -12685,7 +14922,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: schema179.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: schema198.properties.via.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err8]; } @@ -12731,16 +14968,16 @@ vErrors.push(err11); } errors++; } -validate92.errors = vErrors; +validate116.errors = vErrors; return errors === 0; } -validate92.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate116.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate91(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate115(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate91.evaluated; +const evaluated0 = validate115.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -12760,8 +14997,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 schema231 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; -const schema232 = {"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 schema234 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; -const schema235 = {"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 schema236 = {"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 schema240 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; -const schema241 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; -const schema243 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; -const schema238 = {"type":"object","description":"Public archive pages: / and //page/.","additionalProperties":false,"required":["enabled","basePath"],"properties":{"enabled":{"type":"boolean"},"basePath":{"$ref":"#/$defs/Path"}}}; -const schema239 = {"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 = validate140; +const schema249 = {"$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 schema250 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; +const schema251 = {"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 schema253 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; +const schema254 = {"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 schema255 = {"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 schema259 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema260 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema262 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const schema257 = {"type":"object","description":"Public archive pages: / and //page/.","additionalProperties":false,"required":["enabled","basePath"],"properties":{"enabled":{"type":"boolean"},"basePath":{"$ref":"#/$defs/Path"}}}; +const schema258 = {"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 validate117(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate141(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate117.evaluated; +const evaluated0 = validate141.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -16534,7 +18771,7 @@ vErrors.push(err4); } errors++; } -if(!pattern44.test(data1)){ +if(!pattern58.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]; @@ -16567,17 +18804,17 @@ vErrors.push(err7); } errors++; } -validate117.errors = vErrors; +validate141.errors = vErrors; return errors === 0; } -validate117.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate141.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate116(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate140(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ /*# sourceURL="https://cloudhal.dev/schemas/v1/taxonomy.schema.json" */; let vErrors = null; let errors = 0; -const evaluated0 = validate116.evaluated; +const evaluated0 = validate140.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -16726,7 +18963,7 @@ vErrors.push(err13); errors++; } for(const key0 in data){ -if(!(func1.call(schema230.properties, key0))){ +if(!(func1.call(schema249.properties, key0))){ const err14 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err14]; @@ -16750,7 +18987,7 @@ vErrors.push(err15); errors++; } if(!(data0 === 1)){ -const err16 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema231.enum},message:"must be equal to one of the allowed values"}; +const err16 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema250.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err16]; } @@ -17123,7 +19360,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: schema230.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: schema249.properties.allowNewTerms.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err48]; } @@ -17282,8 +19519,8 @@ errors++; } } if(data.archive !== undefined){ -if(!(validate117(data.archive, {instancePath:instancePath+"/archive",parentData:data,parentDataProperty:"archive",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate117.errors : vErrors.concat(validate117.errors); +if(!(validate141(data.archive, {instancePath:instancePath+"/archive",parentData:data,parentDataProperty:"archive",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate141.errors : vErrors.concat(validate141.errors); errors = vErrors.length; } } @@ -17447,29 +19684,29 @@ vErrors.push(err73); } errors++; } -validate116.errors = vErrors; +validate140.errors = vErrors; return errors === 0; } -validate116.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate140.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -export const validateTerm = validate119; -const schema244 = {"$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 schema245 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; -const schema246 = {"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 schema249 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; -const schema250 = {"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 schema257 = {"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 schema263 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; -const schema264 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; -const schema266 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; -const schema251 = {"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 schema253 = {"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 schema254 = {"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 = validate143; +const schema263 = {"$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 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 schema268 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; +const schema269 = {"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 schema276 = {"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 schema282 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema283 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema285 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const schema270 = {"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 schema272 = {"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 schema273 = {"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 validate122(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate146(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate122.evaluated; +const evaluated0 = validate146.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -17675,16 +19912,16 @@ vErrors.push(err16); } errors++; } -validate122.errors = vErrors; +validate146.errors = vErrors; return errors === 0; } -validate122.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate146.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate121(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate145(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate121.evaluated; +const evaluated0 = validate145.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -17792,8 +20029,8 @@ errors++; } } if(data.ogImage !== undefined){ -if(!(validate122(data.ogImage, {instancePath:instancePath+"/ogImage",parentData:data,parentDataProperty:"ogImage",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate122.errors : vErrors.concat(validate122.errors); +if(!(validate146(data.ogImage, {instancePath:instancePath+"/ogImage",parentData:data,parentDataProperty:"ogImage",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate146.errors : vErrors.concat(validate146.errors); errors = vErrors.length; } } @@ -17808,16 +20045,16 @@ vErrors.push(err8); } errors++; } -validate121.errors = vErrors; +validate145.errors = vErrors; return errors === 0; } -validate121.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate145.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate120(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate144(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate120.evaluated; +const evaluated0 = validate144.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -18083,8 +20320,8 @@ errors++; } } if(data.seo !== undefined){ -if(!(validate121(data.seo, {instancePath:instancePath+"/seo",parentData:data,parentDataProperty:"seo",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate121.errors : vErrors.concat(validate121.errors); +if(!(validate145(data.seo, {instancePath:instancePath+"/seo",parentData:data,parentDataProperty:"seo",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate145.errors : vErrors.concat(validate145.errors); errors = vErrors.length; } } @@ -18099,18 +20336,18 @@ vErrors.push(err21); } errors++; } -validate120.errors = vErrors; +validate144.errors = vErrors; return errors === 0; } -validate120.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate144.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const schema260 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; -const schema261 = {"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 schema279 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; +const schema280 = {"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 validate127(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate151(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate127.evaluated; +const evaluated0 = validate151.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -18163,7 +20400,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: schema261.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema280.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -18254,17 +20491,17 @@ vErrors.push(err11); } errors++; } -validate127.errors = vErrors; +validate151.errors = vErrors; return errors === 0; } -validate127.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate151.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate119(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate143(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ /*# sourceURL="https://cloudhal.dev/schemas/v1/term.schema.json" */; let vErrors = null; let errors = 0; -const evaluated0 = validate119.evaluated; +const evaluated0 = validate143.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -18383,7 +20620,7 @@ vErrors.push(err10); errors++; } for(const key0 in data){ -if(!(func1.call(schema244.properties, key0))){ +if(!(func1.call(schema263.properties, key0))){ const err11 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err11]; @@ -18407,7 +20644,7 @@ vErrors.push(err12); errors++; } if(!(data0 === 1)){ -const err13 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema245.enum},message:"must be equal to one of the allowed values"}; +const err13 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema264.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err13]; } @@ -18610,7 +20847,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: schema244.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: schema263.properties.status.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err31]; } @@ -18633,7 +20870,7 @@ vErrors.push(err32); errors++; } if(!((data6 === "public") || (data6 === "internal"))){ -const err33 = {instancePath:instancePath+"/visibility",schemaPath:"#/properties/visibility/enum",keyword:"enum",params:{allowedValues: schema244.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: schema263.properties.visibility.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err33]; } @@ -18693,8 +20930,8 @@ errors++; } } for(const key2 in data7){ -if(!(validate120(data7[key2], {instancePath:instancePath+"/labels/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),parentData:data7,parentDataProperty:key2,rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate120.errors : vErrors.concat(validate120.errors); +if(!(validate144(data7[key2], {instancePath:instancePath+"/labels/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),parentData:data7,parentDataProperty:key2,rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate144.errors : vErrors.concat(validate144.errors); errors = vErrors.length; } } @@ -18791,8 +21028,8 @@ errors++; } } if(data.image !== undefined){ -if(!(validate122(data.image, {instancePath:instancePath+"/image",parentData:data,parentDataProperty:"image",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate122.errors : vErrors.concat(validate122.errors); +if(!(validate146(data.image, {instancePath:instancePath+"/image",parentData:data,parentDataProperty:"image",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate146.errors : vErrors.concat(validate146.errors); errors = vErrors.length; } } @@ -19098,8 +21335,8 @@ errors++; } } if(data.proposedBy !== undefined){ -if(!(validate127(data.proposedBy, {instancePath:instancePath+"/proposedBy",parentData:data,parentDataProperty:"proposedBy",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate127.errors : vErrors.concat(validate127.errors); +if(!(validate151(data.proposedBy, {instancePath:instancePath+"/proposedBy",parentData:data,parentDataProperty:"proposedBy",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate151.errors : vErrors.concat(validate151.errors); errors = vErrors.length; } } @@ -19216,27 +21453,27 @@ vErrors.push(err79); } errors++; } -validate119.errors = vErrors; +validate143.errors = vErrors; return errors === 0; } -validate119.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate143.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -export const validateRedirect = validate129; -const schema267 = {"$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 schema268 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; -const schema269 = {"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 schema271 = {"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 schema274 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; -const schema275 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; -const schema277 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; -const pattern134 = new RegExp("^/(?:[^\\s?#/\\\\]+(?:/[^\\s?#/\\\\]+)*)?$", "u"); -const pattern136 = new RegExp("^(?:/(?![/\\\\])[^\\s\\\\]*|https://[^\\s\\\\/?#]+(?:[/?#][^\\s\\\\]*)?)$", "u"); -const schema272 = {"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 = validate153; +const schema286 = {"$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 schema287 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; +const schema288 = {"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 schema290 = {"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 schema293 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema294 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema296 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const pattern148 = new RegExp("^/(?:[^\\s?#/\\\\]+(?:/[^\\s?#/\\\\]+)*)?$", "u"); +const pattern150 = new RegExp("^(?:/(?![/\\\\])[^\\s\\\\]*|https://[^\\s\\\\/?#]+(?:[/?#][^\\s\\\\]*)?)$", "u"); +const schema291 = {"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 validate130(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate154(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate130.evaluated; +const evaluated0 = validate154.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -19289,7 +21526,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: schema272.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: schema291.properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -19355,17 +21592,17 @@ vErrors.push(err9); } errors++; } -validate130.errors = vErrors; +validate154.errors = vErrors; return errors === 0; } -validate130.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate154.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate129(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate153(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ /*# sourceURL="https://cloudhal.dev/schemas/v1/redirect.schema.json" */; let vErrors = null; let errors = 0; -const evaluated0 = validate129.evaluated; +const evaluated0 = validate153.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -19484,7 +21721,7 @@ vErrors.push(err10); errors++; } for(const key0 in data){ -if(!(func1.call(schema267.properties, key0))){ +if(!(func1.call(schema286.properties, key0))){ const err11 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err11]; @@ -19508,7 +21745,7 @@ vErrors.push(err12); errors++; } if(!(data0 === 1)){ -const err13 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema268.enum},message:"must be equal to one of the allowed values"}; +const err13 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema287.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err13]; } @@ -19621,7 +21858,7 @@ vErrors.push(err22); } errors++; } -if(!pattern134.test(data3)){ +if(!pattern148.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]; @@ -19681,7 +21918,7 @@ vErrors.push(err27); } errors++; } -if(!pattern136.test(data5)){ +if(!pattern150.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]; @@ -19716,7 +21953,7 @@ vErrors.push(err30); errors++; } if(!((data6 === 301) || (data6 === 302))){ -const err31 = {instancePath:instancePath+"/status",schemaPath:"#/properties/status/enum",keyword:"enum",params:{allowedValues: schema267.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: schema286.properties.status.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err31]; } @@ -19739,7 +21976,7 @@ vErrors.push(err32); errors++; } if(!((data7 === "exact") || (data7 === "prefix"))){ -const err33 = {instancePath:instancePath+"/match",schemaPath:"#/properties/match/enum",keyword:"enum",params:{allowedValues: schema267.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: schema286.properties.match.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err33]; } @@ -19762,7 +21999,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: schema267.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: schema286.properties.reason.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err35]; } @@ -19773,8 +22010,8 @@ errors++; } } if(data.source !== undefined){ -if(!(validate130(data.source, {instancePath:instancePath+"/source",parentData:data,parentDataProperty:"source",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate130.errors : vErrors.concat(validate130.errors); +if(!(validate154(data.source, {instancePath:instancePath+"/source",parentData:data,parentDataProperty:"source",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate154.errors : vErrors.concat(validate154.errors); errors = vErrors.length; } } @@ -19916,34 +22153,34 @@ vErrors.push(err46); } errors++; } -validate129.errors = vErrors; +validate153.errors = vErrors; return errors === 0; } -validate129.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate153.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -export const validateAsset = validate132; -const schema278 = {"$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 schema279 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; -const schema280 = {"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 hex SHA-256 digest.","pattern":"^[0-9a-f]{64}$"}; -const schema284 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; -const schema289 = {"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 schema290 = {"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 schema302 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; -const schema288 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; -const schema305 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; -const pattern140 = new RegExp("^[a-z0-9][a-z0-9!#$&^_.+-]{0,126}/[a-z0-9][a-z0-9!#$&^_.+-]{0,126}$", "u"); -const pattern146 = new RegExp("^[0-9A-Za-z#$%*+,.:;=?@^_{|}~\\[\\]-]{6,100}$", "u"); -const pattern147 = new RegExp("^data:image/(?:webp|png|jpeg);base64,[A-Za-z0-9+/]+={0,2}$", "u"); -const pattern148 = new RegExp("^#[0-9a-f]{6}$", "u"); -const schema285 = {"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 schema286 = {"type":"string","description":"Absolute https URL without credentials, whitespace or backslashes.","pattern":"^https://[^\\s\\\\/?#@]+(?:[/?#][^\\s\\\\]*)?$","maxLength":2048}; -const pattern144 = new RegExp("^https://[^\\s\\\\/?#@]+(?:[/?#][^\\s\\\\]*)?$", "u"); +export const validateAsset = validate156; +const schema297 = {"$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 schema298 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; +const schema299 = {"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 schema301 = {"type":"string","description":"Lower-case hex SHA-256 digest.","pattern":"^[0-9a-f]{64}$"}; +const schema303 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; +const schema308 = {"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 schema309 = {"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 schema321 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema307 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema324 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const pattern154 = new RegExp("^[a-z0-9][a-z0-9!#$&^_.+-]{0,126}/[a-z0-9][a-z0-9!#$&^_.+-]{0,126}$", "u"); +const pattern160 = new RegExp("^[0-9A-Za-z#$%*+,.:;=?@^_{|}~\\[\\]-]{6,100}$", "u"); +const pattern161 = new RegExp("^data:image/(?:webp|png|jpeg);base64,[A-Za-z0-9+/]+={0,2}$", "u"); +const pattern162 = new RegExp("^#[0-9a-f]{6}$", "u"); +const schema304 = {"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 schema305 = {"type":"string","description":"Absolute https URL without credentials, whitespace or backslashes.","pattern":"^https://[^\\s\\\\/?#@]+(?:[/?#][^\\s\\\\]*)?$","maxLength":2048}; +const pattern158 = new RegExp("^https://[^\\s\\\\/?#@]+(?:[/?#][^\\s\\\\]*)?$", "u"); -function validate133(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate157(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate133.evaluated; +const evaluated0 = validate157.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -19990,7 +22227,7 @@ vErrors.push(err2); errors++; } if(!(data0 === "blob")){ -const err3 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema285.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: schema304.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err3]; } @@ -20074,7 +22311,7 @@ vErrors.push(err9); errors++; } if(!(data1 === "external")){ -const err10 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema285.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: schema304.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err10]; } @@ -20097,7 +22334,7 @@ vErrors.push(err11); } errors++; } -if(!pattern144.test(data2)){ +if(!pattern158.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]; @@ -20132,7 +22369,7 @@ vErrors.push(err14); } errors++; } -if(!pattern144.test(data3)){ +if(!pattern158.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]; @@ -20167,7 +22404,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: schema285.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: schema304.oneOf[1].properties.checksum.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err18]; } @@ -20248,19 +22485,19 @@ vErrors = null; } } } -validate133.errors = vErrors; +validate157.errors = vErrors; evaluated0.props = props0; return errors === 0; } -validate133.evaluated = {"dynamicProps":true,"dynamicItems":false}; +validate157.evaluated = {"dynamicProps":true,"dynamicItems":false}; -const schema293 = {"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 pattern152 = new RegExp("^[A-Za-z0-9][A-Za-z0-9.+-]*(?: (?:AND|OR|WITH) [A-Za-z0-9][A-Za-z0-9.+-]*)*$", "u"); +const schema312 = {"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 pattern166 = new RegExp("^[A-Za-z0-9][A-Za-z0-9.+-]*(?: (?:AND|OR|WITH) [A-Za-z0-9][A-Za-z0-9.+-]*)*$", "u"); -function validate135(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate159(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate135.evaluated; +const evaluated0 = validate159.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -20317,7 +22554,7 @@ vErrors.push(err3); errors++; } if(!(data0 === "spdx")){ -const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema293.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: schema312.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -20340,7 +22577,7 @@ vErrors.push(err5); } errors++; } -if(!pattern152.test(data1)){ +if(!pattern166.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]; @@ -20426,7 +22663,7 @@ vErrors.push(err12); errors++; } if(!(data2 === "url")){ -const err13 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema293.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: schema312.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err13]; } @@ -20449,7 +22686,7 @@ vErrors.push(err14); } errors++; } -if(!pattern144.test(data3)){ +if(!pattern158.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]; @@ -20552,19 +22789,19 @@ vErrors = null; } } } -validate135.errors = vErrors; +validate159.errors = vErrors; evaluated0.props = props0; return errors === 0; } -validate135.evaluated = {"dynamicProps":true,"dynamicItems":false}; +validate159.evaluated = {"dynamicProps":true,"dynamicItems":false}; -const schema295 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; -const schema296 = {"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 schema314 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; +const schema315 = {"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 validate137(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate161(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate137.evaluated; +const evaluated0 = validate161.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -20617,7 +22854,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: schema296.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema315.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -20708,18 +22945,18 @@ vErrors.push(err11); } errors++; } -validate137.errors = vErrors; +validate161.errors = vErrors; return errors === 0; } -validate137.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate161.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const schema298 = {"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 pattern155 = new RegExp("^[^/\\\\\\u0000-\\u001f\\u007f]+$", "u"); +const schema317 = {"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 pattern169 = new RegExp("^[^/\\\\\\u0000-\\u001f\\u007f]+$", "u"); -function validate139(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate163(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate139.evaluated; +const evaluated0 = validate163.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -20766,7 +23003,7 @@ vErrors.push(err2); errors++; } if(!(data0 === "upload")){ -const err3 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema298.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: schema317.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err3]; } @@ -20799,7 +23036,7 @@ vErrors.push(err5); } errors++; } -if(!pattern155.test(data1)){ +if(!pattern169.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]; @@ -20895,7 +23132,7 @@ vErrors.push(err13); errors++; } if(!(data2 === "fetch")){ -const err14 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema298.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: schema317.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err14]; } @@ -20918,7 +23155,7 @@ vErrors.push(err15); } errors++; } -if(!pattern144.test(data3)){ +if(!pattern158.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]; @@ -21036,7 +23273,7 @@ vErrors.push(err24); errors++; } if(!(data5 === "import")){ -const err25 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/2/properties/kind/enum",keyword:"enum",params:{allowedValues: schema298.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: schema317.oneOf[2].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err25]; } @@ -21187,7 +23424,7 @@ vErrors.push(err36); errors++; } if(!(data8 === "link")){ -const err37 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/3/properties/kind/enum",keyword:"enum",params:{allowedValues: schema298.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: schema317.oneOf[3].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err37]; } @@ -21245,18 +23482,18 @@ vErrors = null; } } } -validate139.errors = vErrors; +validate163.errors = vErrors; evaluated0.props = props0; return errors === 0; } -validate139.evaluated = {"dynamicProps":true,"dynamicItems":false}; +validate163.evaluated = {"dynamicProps":true,"dynamicItems":false}; -function validate132(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate156(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ /*# sourceURL="https://cloudhal.dev/schemas/v1/asset.schema.json" */; let vErrors = null; let errors = 0; -const evaluated0 = validate132.evaluated; +const evaluated0 = validate156.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -21415,7 +23652,7 @@ vErrors.push(err14); errors++; } for(const key0 in data){ -if(!(func1.call(schema278.properties, key0))){ +if(!(func1.call(schema297.properties, key0))){ const err15 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err15]; @@ -21439,7 +23676,7 @@ vErrors.push(err16); errors++; } if(!(data0 === 1)){ -const err17 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema279.enum},message:"must be equal to one of the allowed values"}; +const err17 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema298.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err17]; } @@ -21552,7 +23789,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: schema278.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: schema297.properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err27]; } @@ -21575,7 +23812,7 @@ vErrors.push(err28); } errors++; } -if(!pattern140.test(data4)){ +if(!pattern154.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]; @@ -21625,7 +23862,7 @@ errors++; if(data.sha256 !== undefined){ let data6 = data.sha256; if(typeof data6 === "string"){ -if(!pattern31.test(data6)){ +if(!pattern45.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]; @@ -21650,7 +23887,7 @@ errors++; if(data.originalSha256 !== undefined){ let data7 = data.originalSha256; if(typeof data7 === "string"){ -if(!pattern31.test(data7)){ +if(!pattern45.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]; @@ -21718,8 +23955,8 @@ errors++; } } if(data.storage !== undefined){ -if(!(validate133(data.storage, {instancePath:instancePath+"/storage",parentData:data,parentDataProperty:"storage",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate133.errors : vErrors.concat(validate133.errors); +if(!(validate157(data.storage, {instancePath:instancePath+"/storage",parentData:data,parentDataProperty:"storage",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate157.errors : vErrors.concat(validate157.errors); errors = vErrors.length; } } @@ -21843,7 +24080,7 @@ errors++; if(data10.blurhash !== undefined){ let data14 = data10.blurhash; if(typeof data14 === "string"){ -if(!pattern146.test(data14)){ +if(!pattern160.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]; @@ -21878,7 +24115,7 @@ vErrors.push(err53); } errors++; } -if(!pattern147.test(data15)){ +if(!pattern161.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]; @@ -21903,7 +24140,7 @@ errors++; if(data10.color !== undefined){ let data16 = data10.color; if(typeof data16 === "string"){ -if(!pattern148.test(data16)){ +if(!pattern162.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]; @@ -21938,7 +24175,7 @@ vErrors.push(err58); errors++; } if(!(data17 === true)){ -const err59 = {instancePath:instancePath+"/image/alpha",schemaPath:"#/$defs/AssetImage/properties/alpha/enum",keyword:"enum",params:{allowedValues: schema289.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: schema308.properties.alpha.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err59]; } @@ -22413,8 +24650,8 @@ errors++; } } if(data.license !== undefined){ -if(!(validate135(data.license, {instancePath:instancePath+"/license",parentData:data,parentDataProperty:"license",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate135.errors : vErrors.concat(validate135.errors); +if(!(validate159(data.license, {instancePath:instancePath+"/license",parentData:data,parentDataProperty:"license",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate159.errors : vErrors.concat(validate159.errors); errors = vErrors.length; } } @@ -22431,7 +24668,7 @@ vErrors.push(err99); errors++; } if(!((data30 === "active") || (data30 === "retired"))){ -const err100 = {instancePath:instancePath+"/status",schemaPath:"#/properties/status/enum",keyword:"enum",params:{allowedValues: schema278.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: schema297.properties.status.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err100]; } @@ -22442,14 +24679,14 @@ errors++; } } if(data.uploadedBy !== undefined){ -if(!(validate137(data.uploadedBy, {instancePath:instancePath+"/uploadedBy",parentData:data,parentDataProperty:"uploadedBy",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate137.errors : vErrors.concat(validate137.errors); +if(!(validate161(data.uploadedBy, {instancePath:instancePath+"/uploadedBy",parentData:data,parentDataProperty:"uploadedBy",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate161.errors : vErrors.concat(validate161.errors); errors = vErrors.length; } } if(data.source !== undefined){ -if(!(validate139(data.source, {instancePath:instancePath+"/source",parentData:data,parentDataProperty:"source",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate139.errors : vErrors.concat(validate139.errors); +if(!(validate163(data.source, {instancePath:instancePath+"/source",parentData:data,parentDataProperty:"source",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate163.errors : vErrors.concat(validate163.errors); errors = vErrors.length; } } @@ -22566,30 +24803,30 @@ vErrors.push(err109); } errors++; } -validate132.errors = vErrors; +validate156.errors = vErrors; return errors === 0; } -validate132.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate156.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -export const validateMenu = validate141; -const schema306 = {"$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 schema307 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; -const schema308 = {"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 schema310 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; -const schema330 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; -const schema331 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; -const schema333 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; -const schema311 = {"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 schema316 = {"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 pattern172 = 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 schema314 = {"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 schema315 = {"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 pattern164 = new RegExp("^[^\\s\\x00-\\x1F\\x7F](?:[^\\x00-\\x1F\\x7F]*[^\\s\\x00-\\x1F\\x7F])?$", "u"); +export const validateMenu = validate165; +const schema325 = {"$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 schema326 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; +const schema327 = {"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 schema329 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; +const schema349 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema350 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema352 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const schema330 = {"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 schema335 = {"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 pattern186 = 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 schema333 = {"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 schema334 = {"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 pattern178 = new RegExp("^[^\\s\\x00-\\x1F\\x7F](?:[^\\x00-\\x1F\\x7F]*[^\\s\\x00-\\x1F\\x7F])?$", "u"); -function validate143(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate167(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate143.evaluated; +const evaluated0 = validate167.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -22666,7 +24903,7 @@ vErrors.push(err5); } errors++; } -if(!pattern164.test(data0)){ +if(!pattern178.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]; @@ -22699,17 +24936,17 @@ vErrors.push(err8); } errors++; } -validate143.errors = vErrors; +validate167.errors = vErrors; return errors === 0; } -validate143.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate167.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const wrapper0 = {validate: validate142}; +const wrapper0 = {validate: validate166}; -function validate142(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate166(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate142.evaluated; +const evaluated0 = validate166.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -22821,7 +25058,7 @@ vErrors.push(err8); errors++; } if(!(data1 === "page")){ -const err9 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema311.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: schema330.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err9]; } @@ -22877,8 +25114,8 @@ errors++; } } if(data.labels !== undefined){ -if(!(validate143(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate143.errors : vErrors.concat(validate143.errors); +if(!(validate167(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate167.errors : vErrors.concat(validate167.errors); errors = vErrors.length; } } @@ -22895,7 +25132,7 @@ vErrors.push(err14); errors++; } if(!(data4 === "mega")){ -const err15 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema316.enum},message:"must be equal to one of the allowed values"}; +const err15 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema335.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err15]; } @@ -23066,7 +25303,7 @@ vErrors.push(err28); errors++; } if(!(data8 === "contentEntry")){ -const err29 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema311.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: schema330.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err29]; } @@ -23122,8 +25359,8 @@ errors++; } } if(data.labels !== undefined){ -if(!(validate143(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate143.errors : vErrors.concat(validate143.errors); +if(!(validate167(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate167.errors : vErrors.concat(validate167.errors); errors = vErrors.length; } } @@ -23140,7 +25377,7 @@ vErrors.push(err34); errors++; } if(!(data11 === "mega")){ -const err35 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema316.enum},message:"must be equal to one of the allowed values"}; +const err35 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema335.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err35]; } @@ -23318,7 +25555,7 @@ vErrors.push(err48); errors++; } if(!(data15 === "term")){ -const err49 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/2/properties/kind/enum",keyword:"enum",params:{allowedValues: schema311.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: schema330.oneOf[2].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err49]; } @@ -23374,8 +25611,8 @@ errors++; } } if(data.labels !== undefined){ -if(!(validate143(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate143.errors : vErrors.concat(validate143.errors); +if(!(validate167(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate167.errors : vErrors.concat(validate167.errors); errors = vErrors.length; } } @@ -23392,7 +25629,7 @@ vErrors.push(err54); errors++; } if(!(data18 === "mega")){ -const err55 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema316.enum},message:"must be equal to one of the allowed values"}; +const err55 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema335.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err55]; } @@ -23580,7 +25817,7 @@ vErrors.push(err69); errors++; } if(!(data22 === "collection")){ -const err70 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/3/properties/kind/enum",keyword:"enum",params:{allowedValues: schema311.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: schema330.oneOf[3].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err70]; } @@ -23636,8 +25873,8 @@ errors++; } } if(data.labels !== undefined){ -if(!(validate143(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate143.errors : vErrors.concat(validate143.errors); +if(!(validate167(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate167.errors : vErrors.concat(validate167.errors); errors = vErrors.length; } } @@ -23654,7 +25891,7 @@ vErrors.push(err75); errors++; } if(!(data25 === "mega")){ -const err76 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema316.enum},message:"must be equal to one of the allowed values"}; +const err76 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema335.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err76]; } @@ -23842,7 +26079,7 @@ vErrors.push(err90); errors++; } if(!(data29 === "url")){ -const err91 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/4/properties/kind/enum",keyword:"enum",params:{allowedValues: schema311.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: schema330.oneOf[4].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err91]; } @@ -23865,7 +26102,7 @@ vErrors.push(err92); } errors++; } -if(!pattern172.test(data30)){ +if(!pattern186.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]; @@ -23900,8 +26137,8 @@ errors++; } } if(data.labels !== undefined){ -if(!(validate143(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate143.errors : vErrors.concat(validate143.errors); +if(!(validate167(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate167.errors : vErrors.concat(validate167.errors); errors = vErrors.length; } } @@ -23918,7 +26155,7 @@ vErrors.push(err96); errors++; } if(!(data33 === "mega")){ -const err97 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema316.enum},message:"must be equal to one of the allowed values"}; +const err97 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema335.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err97]; } @@ -24106,7 +26343,7 @@ vErrors.push(err111); errors++; } if(!(data37 === "group")){ -const err112 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/5/properties/kind/enum",keyword:"enum",params:{allowedValues: schema311.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: schema330.oneOf[5].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err112]; } @@ -24117,8 +26354,8 @@ errors++; } } if(data.labels !== undefined){ -if(!(validate143(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate143.errors : vErrors.concat(validate143.errors); +if(!(validate167(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate167.errors : vErrors.concat(validate167.errors); errors = vErrors.length; } } @@ -24135,7 +26372,7 @@ vErrors.push(err113); errors++; } if(!(data39 === "mega")){ -const err114 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema316.enum},message:"must be equal to one of the allowed values"}; +const err114 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema335.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err114]; } @@ -24237,18 +26474,18 @@ vErrors = null; } } } -validate142.errors = vErrors; +validate166.errors = vErrors; evaluated0.props = props1; return errors === 0; } -validate142.evaluated = {"dynamicProps":true,"dynamicItems":false}; +validate166.evaluated = {"dynamicProps":true,"dynamicItems":false}; -function validate141(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate165(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ /*# sourceURL="https://cloudhal.dev/schemas/v1/menu.schema.json" */; let vErrors = null; let errors = 0; -const evaluated0 = validate141.evaluated; +const evaluated0 = validate165.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -24347,7 +26584,7 @@ vErrors.push(err8); errors++; } for(const key0 in data){ -if(!(func1.call(schema306.properties, key0))){ +if(!(func1.call(schema325.properties, key0))){ const err9 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err9]; @@ -24371,7 +26608,7 @@ vErrors.push(err10); errors++; } if(!(data0 === 1)){ -const err11 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema307.enum},message:"must be equal to one of the allowed values"}; +const err11 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema326.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err11]; } @@ -24566,8 +26803,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 schema432 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; -const schema433 = {"type":"string","description":"A release id: `rel_` and 6 to 32 letters or digits.","pattern":"^rel_[A-Za-z0-9]{6,32}$"}; -const schema434 = {"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 schema446 = {"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 schema447 = {"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 schema443 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; -const schema439 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; -const schema475 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; -const pattern221 = new RegExp("^rel_[A-Za-z0-9]{6,32}$", "u"); -const pattern223 = new RegExp("^[^\\x00-\\x1F\\x7F\\u202A-\\u202E\\u2066-\\u2069]+$", "u"); -const pattern229 = new RegExp("^rl1:[0-9a-f]{64}$", "u"); -const schema435 = {"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 schema437 = {"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 schema438 = {"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 = validate189; +const schema450 = {"$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 schema451 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; +const schema452 = {"type":"string","description":"A release id: `rel_` and 6 to 32 letters or digits.","pattern":"^rel_[A-Za-z0-9]{6,32}$"}; +const schema453 = {"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 schema465 = {"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 schema466 = {"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 schema462 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema458 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema494 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const pattern235 = new RegExp("^rel_[A-Za-z0-9]{6,32}$", "u"); +const pattern237 = new RegExp("^[^\\x00-\\x1F\\x7F\\u202A-\\u202E\\u2066-\\u2069]+$", "u"); +const pattern243 = new RegExp("^rl1:[0-9a-f]{64}$", "u"); +const schema454 = {"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 schema456 = {"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 schema457 = {"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 validate167(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate191(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate167.evaluated; +const evaluated0 = validate191.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -30255,7 +32492,7 @@ errors++; if(data.hash !== undefined){ let data1 = data.hash; if(typeof data1 === "string"){ -if(!pattern31.test(data1)){ +if(!pattern45.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]; @@ -30288,16 +32525,16 @@ vErrors.push(err7); } errors++; } -validate167.errors = vErrors; +validate191.errors = vErrors; return errors === 0; } -validate167.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate191.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate166(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate190(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate166.evaluated; +const evaluated0 = validate190.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -30374,7 +32611,7 @@ vErrors.push(err5); errors++; } if(!(data0 === "set")){ -const err6 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema435.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: schema454.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err6]; } @@ -30442,7 +32679,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: schema437.enum},message:"must be equal to one of the allowed values"}; +const err12 = {instancePath:instancePath+"/nodeKind",schemaPath:"#/$defs/ReleaseNodeKind/enum",keyword:"enum",params:{allowedValues: schema456.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err12]; } @@ -30453,14 +32690,14 @@ errors++; } } if(data.target !== undefined){ -if(!(validate167(data.target, {instancePath:instancePath+"/target",parentData:data,parentDataProperty:"target",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate167.errors : vErrors.concat(validate167.errors); +if(!(validate191(data.target, {instancePath:instancePath+"/target",parentData:data,parentDataProperty:"target",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate191.errors : vErrors.concat(validate191.errors); errors = vErrors.length; } } if(data.base !== undefined){ -if(!(validate167(data.base, {instancePath:instancePath+"/base",parentData:data,parentDataProperty:"base",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate167.errors : vErrors.concat(validate167.errors); +if(!(validate191(data.base, {instancePath:instancePath+"/base",parentData:data,parentDataProperty:"base",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate191.errors : vErrors.concat(validate191.errors); errors = vErrors.length; } } @@ -30548,7 +32785,7 @@ vErrors.push(err19); errors++; } if(!(data5 === "remove")){ -const err20 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema435.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: schema454.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err20]; } @@ -30616,7 +32853,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: schema437.enum},message:"must be equal to one of the allowed values"}; +const err26 = {instancePath:instancePath+"/nodeKind",schemaPath:"#/$defs/ReleaseNodeKind/enum",keyword:"enum",params:{allowedValues: schema456.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err26]; } @@ -30627,8 +32864,8 @@ errors++; } } if(data.base !== undefined){ -if(!(validate167(data.base, {instancePath:instancePath+"/base",parentData:data,parentDataProperty:"base",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate167.errors : vErrors.concat(validate167.errors); +if(!(validate191(data.base, {instancePath:instancePath+"/base",parentData:data,parentDataProperty:"base",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate191.errors : vErrors.concat(validate191.errors); errors = vErrors.length; } } @@ -30678,21 +32915,21 @@ vErrors = null; } } } -validate166.errors = vErrors; +validate190.errors = vErrors; evaluated0.props = props0; return errors === 0; } -validate166.evaluated = {"dynamicProps":true,"dynamicItems":false}; +validate190.evaluated = {"dynamicProps":true,"dynamicItems":false}; -const schema442 = {"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 schema444 = {"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 schema445 = {"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 pattern228 = 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 schema461 = {"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 schema463 = {"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 schema464 = {"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 pattern242 = 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 validate172(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate196(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate172.evaluated; +const evaluated0 = validate196.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -30759,7 +32996,7 @@ vErrors.push(err4); errors++; } if(!(data0 === "at")){ -const err5 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema442.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: schema461.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err5]; } @@ -30807,7 +33044,7 @@ vErrors.push(err8); } errors++; } -if(!pattern45.test(data2)){ +if(!pattern59.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]; @@ -30832,7 +33069,7 @@ errors++; if(data.localTime !== undefined){ let data3 = data.localTime; if(typeof data3 === "string"){ -if(!pattern228.test(data3)){ +if(!pattern242.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]; @@ -30908,7 +33145,7 @@ vErrors.push(err16); errors++; } if(!(data4 === "on-approval")){ -const err17 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema442.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: schema461.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err17]; } @@ -30964,22 +33201,22 @@ vErrors = null; } } } -validate172.errors = vErrors; +validate196.errors = vErrors; evaluated0.props = props0; return errors === 0; } -validate172.evaluated = {"dynamicProps":true,"dynamicItems":false}; +validate196.evaluated = {"dynamicProps":true,"dynamicItems":false}; -const schema448 = {"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 schema449 = {"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 pattern233 = new RegExp("^[A-Za-z0-9_-]{6,64}$", "u"); -const schema453 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; -const schema454 = {"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 schema467 = {"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 schema468 = {"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 pattern247 = new RegExp("^[A-Za-z0-9_-]{6,64}$", "u"); +const schema472 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; +const schema473 = {"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 validate175(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate199(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate175.evaluated; +const evaluated0 = validate199.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -31032,7 +33269,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: schema454.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema473.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -31123,16 +33360,16 @@ vErrors.push(err11); } errors++; } -validate175.errors = vErrors; +validate199.errors = vErrors; return errors === 0; } -validate175.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate199.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate174(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate198(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate174.evaluated; +const evaluated0 = validate198.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -31199,7 +33436,7 @@ vErrors.push(err4); errors++; } if(!(data0 === "person")){ -const err5 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema448.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: schema467.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err5]; } @@ -31237,7 +33474,7 @@ errors++; if(data1.id !== undefined){ let data2 = data1.id; if(typeof data2 === "string"){ -if(!pattern207.test(data2)){ +if(!pattern221.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]; @@ -31485,7 +33722,7 @@ vErrors.push(err28); errors++; } if(!(data7 === "auto")){ -const err29 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema448.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: schema467.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err29]; } @@ -31496,15 +33733,15 @@ errors++; } } if(data.by !== undefined){ -if(!(validate175(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate175.errors : vErrors.concat(validate175.errors); +if(!(validate199(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate199.errors : vErrors.concat(validate199.errors); errors = vErrors.length; } } if(data.keyId !== undefined){ let data9 = data.keyId; if(typeof data9 === "string"){ -if(!pattern233.test(data9)){ +if(!pattern247.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]; @@ -31622,20 +33859,20 @@ vErrors = null; } } } -validate174.errors = vErrors; +validate198.errors = vErrors; evaluated0.props = props0; return errors === 0; } -validate174.evaluated = {"dynamicProps":true,"dynamicItems":false}; +validate198.evaluated = {"dynamicProps":true,"dynamicItems":false}; -const schema458 = {"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 pattern240 = new RegExp("^[^\\x00-\\x1F\\x7F\\u202A-\\u202E\\u2066-\\u2069]*$", "u"); -const schema465 = {"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 schema477 = {"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 pattern254 = new RegExp("^[^\\x00-\\x1F\\x7F\\u202A-\\u202E\\u2066-\\u2069]*$", "u"); +const schema484 = {"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 validate179(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate203(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate179.evaluated; +const evaluated0 = validate203.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -31711,14 +33948,14 @@ errors++; } } if(data.expected !== undefined){ -if(!(validate167(data.expected, {instancePath:instancePath+"/expected",parentData:data,parentDataProperty:"expected",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate167.errors : vErrors.concat(validate167.errors); +if(!(validate191(data.expected, {instancePath:instancePath+"/expected",parentData:data,parentDataProperty:"expected",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate191.errors : vErrors.concat(validate191.errors); errors = vErrors.length; } } if(data.found !== undefined){ -if(!(validate167(data.found, {instancePath:instancePath+"/found",parentData:data,parentDataProperty:"found",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate167.errors : vErrors.concat(validate167.errors); +if(!(validate191(data.found, {instancePath:instancePath+"/found",parentData:data,parentDataProperty:"found",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate191.errors : vErrors.concat(validate191.errors); errors = vErrors.length; } } @@ -31733,16 +33970,16 @@ vErrors.push(err6); } errors++; } -validate179.errors = vErrors; +validate203.errors = vErrors; return errors === 0; } -validate179.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate203.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate178(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate202(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate178.evaluated; +const evaluated0 = validate202.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -31849,7 +34086,7 @@ vErrors.push(err8); errors++; } if(!(data0 === "published")){ -const err9 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema458.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: schema477.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err9]; } @@ -32057,7 +34294,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: schema458.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: schema477.oneOf[0].properties.purge.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err27]; } @@ -32141,7 +34378,7 @@ vErrors.push(err33); errors++; } if(!(data7 === "conflict")){ -const err34 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema458.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: schema477.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err34]; } @@ -32246,8 +34483,8 @@ errors++; } const len0 = data10.length; for(let i0=0; i0; + +/** Per-type rules beyond the shared ones (dispatch table, one entry per type that has any). */ +const TYPE_RULES: Partial< + Record FieldIssue[]> +> = { + number(def, at) { + if (def.type !== "number") return []; + const { minimum, maximum } = def; + if (minimum !== undefined && maximum !== undefined && minimum > maximum) + return [{ path: `${at}/minimum`, code: "field_bounds", message: "minimum is above maximum" }]; + return []; + }, + select(def, at) { + if (def.type !== "select") return []; + const out: FieldIssue[] = []; + if (def.options.length > FIELD_LIMITS.selectOptions) + out.push({ + path: `${at}/options`, + code: "too_many_fields", + message: `at most ${FIELD_LIMITS.selectOptions} options`, + }); + const seen = new Set(); + def.options.forEach((o, i) => { + if (seen.has(o.value)) + out.push({ + path: `${at}/options/${i}/value`, + code: "field_option_duplicate", + message: `option ${o.value} is listed twice`, + }); + seen.add(o.value); + }); + return out; + }, +}; + +function checkCollection(key: string, settings: CollectionSettings): FieldIssue[] { + const fields = settings.fields ?? []; + const base = `collections/${key}/fields`; + const out: FieldIssue[] = []; + if (fields.length > FIELD_LIMITS.fieldsPerCollection) + out.push({ + path: base, + code: "too_many_fields", + message: `at most ${FIELD_LIMITS.fieldsPerCollection} fields per collection`, + }); + const seen = new Set(); + fields.forEach((def, i) => { + const at = `${base}/${i}`; + if (RESERVED_FIELD_KEYS.has(def.key)) + out.push({ + path: `${at}/key`, + code: "field_key_reserved", + message: `${def.key} is reserved (a built-in entry property or JavaScript name)`, + }); + if (seen.has(def.key)) + out.push({ + path: `${at}/key`, + code: "field_key_duplicate", + message: `${def.key} is used by another field of ${key}`, + }); + seen.add(def.key); + out.push(...(TYPE_RULES[def.type]?.(def, at) ?? [])); + }); + return out; +} + +/** One change between two definition lists that a plain edit may not make, or may make only when no value is affected. */ +export type FieldDefinitionChange = + /** Needs `rename_field`/`remove_field` when entries still hold values under the key. */ + | { collection: string; key: string; change: "removed" } + /** Needs `change_field_type`. */ + | { collection: string; key: string; change: "type"; from: string; to: string } + /** Needs `set_field_localized`. */ + | { collection: string; key: string; change: "localized"; from: boolean; to: boolean } + /** Needs `change_field_type` (single ↔ multiple). */ + | { collection: string; key: string; change: "multiple"; from: boolean; to: boolean } + /** Allowed only while no entry uses one of `options`. */ + | { collection: string; key: string; change: "optionsRemoved"; options: string[] } + /** The operation marker; only field operations write it. */ + | { collection: string; key: string; change: "migration" }; + +/** Changes that need a value-free check: always refused by a plain edit. */ +const ALWAYS_REFUSED = new Set([ + "type", + "localized", + "multiple", + "migration", +]); + +function compareField( + collection: string, + was: FieldDefinition, + now: FieldDefinition, +): FieldDefinitionChange[] { + const key = now.key; + const out: FieldDefinitionChange[] = []; + if (was.type !== now.type) + out.push({ collection, key, change: "type", from: was.type, to: now.type }); + if (was.localized !== now.localized) + out.push({ collection, key, change: "localized", from: was.localized, to: now.localized }); + if (!sameJson(was.migration, now.migration)) out.push({ collection, key, change: "migration" }); + if (was.type === "select" && now.type === "select") { + const [a, b] = [was.multiple === true, now.multiple === true]; + if (a !== b) out.push({ collection, key, change: "multiple", from: a, to: b }); + const kept = new Set((now as SelectFieldDefinition).options.map((o) => o.value)); + const removed = was.options.map((o) => o.value).filter((v) => !kept.has(v)); + if (removed.length) out.push({ collection, key, change: "optionsRemoved", options: removed }); + } + return out; +} + +/** + * Everything between `before` and `after` that a plain edit could lose or reinterpret values with + * (§6). A diff is never read as a rename: a key that disappears is `removed`, and the new key is + * simply added. + */ +export function fieldDefinitionChanges( + before: SiteFields, + after: SiteFields, +): FieldDefinitionChange[] { + const out: FieldDefinitionChange[] = []; + const was: Collections = before.collections ?? {}; + const now: Collections = after.collections ?? {}; + for (const [collection, settings] of Object.entries(was)) { + const next = new Map((now[collection]?.fields ?? []).map((d) => [d.key, d])); + for (const def of settings?.fields ?? []) { + const match = next.get(def.key); + if (match) out.push(...compareField(collection, def, match)); + else out.push({ collection, key: def.key, change: "removed" }); + } + } + // A marker on a field that did not exist before is an operation's too. + for (const [collection, settings] of Object.entries(now)) { + const old = new Set((was[collection]?.fields ?? []).map((d) => d.key)); + for (const def of settings?.fields ?? []) + if (!old.has(def.key) && def.migration) + out.push({ collection, key: def.key, change: "migration" }); + } + return out; +} + +const OPERATION: Record = { + type: "change_field_type", + multiple: "change_field_type", + localized: "set_field_localized", + migration: "a field operation", +}; + +/** + * Problems with a site's field definitions (§1 code rules, §4 caps). With `before`, also the §6 + * 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. + */ +export function checkFieldDefinitions(site: SiteFields, before?: SiteFields): FieldIssue[] { + const collections: Collections = site.collections ?? {}; + const out: FieldIssue[] = []; + let total = 0; + for (const [key, settings] of Object.entries(collections)) { + if (!settings) continue; + total += settings.fields?.length ?? 0; + out.push(...checkCollection(key, settings)); + } + if (total > FIELD_LIMITS.fieldsPerSite) + out.push({ + path: "collections", + code: "too_many_fields", + message: `at most ${FIELD_LIMITS.fieldsPerSite} fields per site (${total})`, + }); + if (site.collections && jsonBytes(site.collections) > FIELD_LIMITS.collectionsBytes) + out.push({ + path: "collections", + code: "too_large", + message: `collection settings are over ${FIELD_LIMITS.collectionsBytes / 1024} KB`, + }); + if (before) + for (const c of fieldDefinitionChanges(before, site)) + if (ALWAYS_REFUSED.has(c.change)) + out.push({ + path: `collections/${c.collection}/fields/${c.key}`, + code: "field_change_needs_operation", + message: `changing ${c.change === "migration" ? "the migration marker" : c.change} of ${c.key} needs ${OPERATION[c.change]}`, + }); + return out; +} diff --git a/packages/core-ts/src/fields/index.ts b/packages/core-ts/src/fields/index.ts new file mode 100644 index 00000000..af665179 --- /dev/null +++ b/packages/core-ts/src/fields/index.ts @@ -0,0 +1,57 @@ +/** + * `@cloudhal/core-ts/fields` (RFC-0041 / ADR-0043): typed custom fields per collection. One + * eval-free checker for the store, the API and MCP's precheck: definition rules the schema cannot + * express, values compiled to ADR-0030's props subset (`validateProps`), the JSON Schema agents + * receive, and the values readers see (`resolveFields`). Worker-safe and Temporal-free. + */ +export { + checkFieldDefinitions, + type FieldDefinitionChange, + fieldDefinitionChanges, +} from "./definitions.ts"; +export { + fieldJsonSchema, + fieldsJsonSchema, + JSON_SCHEMA_KEYWORDS, + type JsonSchema, +} from "./json-schema.ts"; +export { + ASSET_ID_PATTERN, + DATE_PATTERN, + fieldPropSchema, + fieldsPropsSchema, + INSTANT_PATTERN, +} from "./props.ts"; +export { + type FieldsEntry, + type FieldsOriginal, + hasFieldValue, + type ResolvedField, + type ResolvedFields, + resolveFields, +} from "./resolve.ts"; +export { + FIELD_LIMITS, + FIELD_TYPES, + type FieldIssue, + type FieldIssueCode, + type FieldType, + fieldDefinitions, + fieldHelp, + fieldLabel, + optionLabel, + RESERVED_FIELD_KEYS, + type SiteFields, +} from "./types.ts"; +export { + isCalendarDay, + isKnownTimeZone, + type ValueCheckOptions, + valueIssues, +} from "./value-checks.ts"; +export { + type CheckInput, + checkFieldValues, + checkRequiredToPublish, + type PublishCheckEntry, +} from "./values.ts"; diff --git a/packages/core-ts/src/fields/json-schema.ts b/packages/core-ts/src/fields/json-schema.ts new file mode 100644 index 00000000..2682932d --- /dev/null +++ b/packages/core-ts/src/fields/json-schema.ts @@ -0,0 +1,159 @@ +/** + * RFC-0041 §3: the JSON Schema agents receive for a collection's `fields` (`list_collections` + * `fieldsSchema`). Only keywords both OpenAI's and Anthropic's strict structured-output modes + * accept: `type`, `enum`, fixed `pattern`s, `format` (`date`, `date-time`), `items`, `properties`, + * `required` (inside the datetime object only), `additionalProperties: false`, `title` and + * `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. + */ +import type { FieldDefinition } from "@cloudhal/core-contracts"; +import { ASSET_ID_PATTERN } from "./props.ts"; +import { FIELD_LIMITS, fieldHelp, fieldLabel, optionLabel } from "./types.ts"; + +/** The subset of JSON Schema this emitter writes. */ +export interface JsonSchema { + type: "object" | "string" | "number" | "integer" | "boolean" | "array"; + title?: string; + description?: string; + enum?: string[]; + pattern?: string; + format?: "date" | "date-time"; + items?: JsonSchema; + properties?: Record; + required?: string[]; + additionalProperties?: false; +} + +/** The keywords `fieldsJsonSchema` may emit (tests hold it to this list). */ +export const JSON_SCHEMA_KEYWORDS: readonly string[] = [ + "type", + "title", + "description", + "enum", + "pattern", + "format", + "items", + "properties", + "required", + "additionalProperties", +]; + +interface Words { + locale: string; + defaultLocale: string; +} + +type Emit = (def: FieldDefinition, words: Words) => { schema: JsonSchema; notes: string[] }; + +const EMIT: Record = { + text: (def) => { + const max = (def.type === "text" && def.maxLength) || FIELD_LIMITS.textLength; + const multiline = def.type === "text" && def.multiline === true; + return { + schema: { type: "string" }, + notes: [ + `Plain text, at most ${max} characters.`, + multiline ? "Line breaks allowed." : "One line.", + ], + }; + }, + richText: () => ({ + schema: { type: "string" }, + notes: [`Markdown (Cloudhal Markdown v1), at most ${FIELD_LIMITS.richTextLength} characters.`], + }), + number: (def) => { + const n = def.type === "number" ? def : undefined; + const notes = [n?.integer ? "A whole number." : "A number."]; + if (n?.minimum !== undefined) notes.push(`At least ${n.minimum}.`); + if (n?.maximum !== undefined) notes.push(`At most ${n.maximum}.`); + return { schema: { type: n?.integer ? "integer" : "number" }, notes }; + }, + boolean: () => ({ schema: { type: "boolean" }, notes: ["Absent reads as false."] }), + date: () => ({ + schema: { type: "string", format: "date" }, + notes: ["A calendar day, YYYY-MM-DD, without a time zone."], + }), + datetime: () => ({ + schema: { + type: "object", + properties: { + at: { + type: "string", + format: "date-time", + description: "The instant in UTC, RFC 3339 ending in Z, e.g. 2026-11-05T16:30:00Z.", + }, + timeZone: { + type: "string", + description: "The IANA time zone it happens in, e.g. Europe/Istanbul.", + }, + }, + required: ["at", "timeZone"], + additionalProperties: false, + }, + notes: ["A date and time with its time zone."], + }), + url: () => ({ + // No `format: uri`: site-relative paths and #fragments are allowed and are not URIs. + schema: { type: "string" }, + notes: [ + `A link: https:, http:, mailto: or tel:, a site path (/about) or a #fragment; at most ${FIELD_LIMITS.urlLength} characters.`, + ], + }), + image: () => ({ + schema: { type: "string", pattern: ASSET_ID_PATTERN }, + notes: ["The id of an image asset of this site (asset_…)."], + }), + select: (def, { locale, defaultLocale }) => { + const options = def.type === "select" ? def.options : []; + const values = options.map((o) => o.value); + const listed = options + .map((o) => `${o.value} (${optionLabel(def, o.value, locale, defaultLocale)})`) + .join(", "); + const one: JsonSchema = { type: "string", enum: values }; + return def.type === "select" && def.multiple + ? { + schema: { type: "array", items: one }, + notes: [`Any of: ${listed}; each at most once.`], + } + : { schema: one, notes: [`One of: ${listed}.`] }; + }, +}; + +/** One field's schema, with its label as `title` and help plus limits as `description`. */ +export function fieldJsonSchema( + def: FieldDefinition, + locale: string, + defaultLocale = locale, +): JsonSchema { + const words = { locale, defaultLocale }; + 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.type !== "boolean" && def.requiredToPublish) notes.push("Needed to publish."); + return { + ...schema, + title: fieldLabel(def, locale, defaultLocale), + description: [help, ...notes].filter(Boolean).join(" "), + }; +} + +/** + * 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. + */ +export function fieldsJsonSchema( + defs: readonly FieldDefinition[], + locale: string, + defaultLocale = locale, +): JsonSchema { + const properties: Record = {}; + for (const def of defs) properties[def.key] = fieldJsonSchema(def, locale, defaultLocale); + return { + type: "object", + description: + "Typed custom fields of this collection. Every field is optional; leave a key out to leave it unset.", + properties, + additionalProperties: false, + }; +} diff --git a/packages/core-ts/src/fields/props.ts b/packages/core-ts/src/fields/props.ts new file mode 100644 index 00000000..f1eaeb93 --- /dev/null +++ b/packages/core-ts/src/fields/props.ts @@ -0,0 +1,111 @@ +/** + * RFC-0041 §3: field definitions compiled into ADR-0030's props subset, so `validateProps` — the + * eval-free interpreter the store, Studio and the theme generator already share — does the type + * 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 { FIELD_LIMITS } from "./types.ts"; + +/** `YYYY-MM-DD`; whether the day exists is checked in code. */ +export const DATE_PATTERN = "^[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])$"; +/** RFC 3339 in UTC (`Z`), seconds required, fraction optional. */ +export const INSTANT_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]:[0-5][0-9](?:\\.[0-9]{1,9})?Z$"; +/** The contract's `TimeZone` shape; the zone list is checked in code. */ +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}$"; + +/** The subset's `Localized`: per-locale words when `en` is among them, else the first one. */ +function localized( + words: Readonly>, + fallback: string, +): Localized { + const entries = Object.entries(words).filter((e): e is [string, string] => e[1] !== undefined); + if (entries.some(([l]) => l === "en")) return Object.fromEntries(entries); + return entries[0]?.[1] ?? fallback; +} + +function annotations(def: FieldDefinition): { title: Localized; description?: Localized } { + const labels: Record = {}; + const help: Record = {}; + for (const [locale, l] of Object.entries(def.labels)) { + if (!l) continue; + labels[locale] = l.label; + if (l.help) help[locale] = l.help; + } + const title = localized(labels, def.key); + return Object.keys(help).length ? { title, description: localized(help, "") } : { title }; +} + +type Compile = (def: FieldDefinition) => PropSchema; + +/** One compiler per type (dispatch table, RFC-0041 §2's value column). */ +const COMPILE: Record = { + text: (def) => ({ + type: "string", + maxLength: (def.type === "text" && def.maxLength) || FIELD_LIMITS.textLength, + ...(def.type === "text" && def.multiline ? { "x-widget": "textarea" as const } : {}), + }), + // No maxLength: the subset caps strings at 10 000; values.ts checks the 50 000 limit. + richText: () => ({ type: "string", format: "markdown", "x-widget": "textarea" }), + number: (def) => { + const n = def.type === "number" ? def : undefined; + return { + type: n?.integer ? "integer" : "number", + ...(n?.minimum !== undefined ? { minimum: n.minimum } : {}), + ...(n?.maximum !== undefined ? { maximum: n.maximum } : {}), + }; + }, + boolean: () => ({ type: "boolean" }), + date: () => ({ type: "string", pattern: DATE_PATTERN }), + datetime: () => ({ + type: "object", + properties: { + at: { type: "string", pattern: INSTANT_PATTERN }, + timeZone: { type: "string", maxLength: 64, pattern: TIME_ZONE_SHAPE }, + }, + required: ["at", "timeZone"], + additionalProperties: false, + }), + url: () => ({ type: "string", format: "url", maxLength: FIELD_LIMITS.urlLength }), + image: () => ({ type: "string", pattern: ASSET_ID_PATTERN }), + select: (def) => { + const options = def.type === "select" ? def.options : []; + const values = options.map((o) => o.value); + const labels = Object.fromEntries(options.map((o) => [o.value, localized(o.labels, o.value)])); + const one = { type: "string" as const, enum: values, "x-labels": labels }; + return def.type === "select" && def.multiple + ? { type: "array", items: one, maxItems: values.length } + : one; + }, +}; + +const fieldCache = new WeakMap(); +const listCache = new WeakMap(); + +/** One field's prop schema, with its labels and help as `title`/`description`. */ +export function fieldPropSchema(def: FieldDefinition): PropSchema { + const hit = fieldCache.get(def); + if (hit) return hit; + const schema = { ...COMPILE[def.type](def), ...annotations(def) } as PropSchema; + fieldCache.set(def, schema); + return schema; +} + +/** + * The props schema of a collection's fields: every property optional (values are optional in + * the contract; `requiredToPublish` is checked at review and publish). Cached per definitions + * array, so a site node read once compiles once. + */ +export function fieldsPropsSchema(defs: readonly FieldDefinition[]): PropsSchema { + const hit = listCache.get(defs); + if (hit) return hit; + const properties: Record = {}; + for (const def of defs) properties[def.key] = fieldPropSchema(def); + const schema: PropsSchema = { type: "object", properties, additionalProperties: false }; + listCache.set(defs, schema); + return schema; +} diff --git a/packages/core-ts/src/fields/resolve.ts b/packages/core-ts/src/fields/resolve.ts new file mode 100644 index 00000000..865633d5 --- /dev/null +++ b/packages/core-ts/src/fields/resolve.ts @@ -0,0 +1,107 @@ +/** + * RFC-0041 §5: the values a reader sees. Pure: callers pass the translation group's original when + * the entry is a translation; nothing here loads nodes or throws. + * + * - localized: the entry's own value, else the original's (`fromOriginal`); + * - non-localized: the original's value on a translation, the entry's own otherwise; + * - a language edition (an original in another site) owns all its values; + * - 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 resolves to `undefined` + * with an issue until the operation converts it (the closed conversion table is RFC-0041 §6's, + * applied by the operation runner). + */ +import type { ContentEntry, FieldDefinition } from "@cloudhal/core-contracts"; +import { type FieldIssue, fieldDefinitions, isRecord, type SiteFields } from "./types.ts"; +import { valueIssues } from "./value-checks.ts"; + +export interface ResolvedField { + def: FieldDefinition; + /** `undefined` when there is no value, or the stored one does not fit `def`. */ + value: unknown; + /** The value came from the translation group's original. */ + fromOriginal: boolean; + /** Why a stored value resolved to `undefined`. */ + issue?: FieldIssue; +} + +/** Every defined field by key, in definition order. */ +export type ResolvedFields = Record; + +export type FieldsEntry = Pick & { + translation?: ContentEntry["translation"] | undefined; +}; +export type FieldsOriginal = Pick; + +/** True when the entry is a translation whose original is in the same site. */ +function readsOriginal(entry: FieldsEntry, original: FieldsOriginal | undefined): boolean { + const t = entry.translation; + if (!t || t.group === entry.id || t.source?.siteId !== undefined) return false; + return original !== undefined && original.id === t.group; +} + +function read(values: unknown, def: FieldDefinition): unknown { + if (!isRecord(values)) return undefined; + if (Object.hasOwn(values, def.key)) return values[def.key]; + const from = def.migration?.fromKey; + return from && Object.hasOwn(values, from) ? values[from] : undefined; +} + +function pick( + def: FieldDefinition, + entry: FieldsEntry, + original: FieldsOriginal | undefined, +): { value: unknown; fromOriginal: boolean } { + const fromOriginal = readsOriginal(entry, original); + if (!def.localized) + return fromOriginal + ? { value: read(original?.fields, def), fromOriginal: true } + : { value: read(entry.fields, def), fromOriginal: false }; + const own = read(entry.fields, def); + if (own !== undefined || !fromOriginal) return { value: own, fromOriginal: false }; + const theirs = read(original?.fields, def); + return { value: theirs, fromOriginal: theirs !== undefined }; +} + +/** The entry's typed values under its collection's definitions (`{}` when it has none). */ +export function resolveFields( + site: SiteFields, + entry: FieldsEntry, + original?: FieldsOriginal, +): ResolvedFields { + const out: ResolvedFields = {}; + for (const def of fieldDefinitions(site, entry.collection)) { + const { value, fromOriginal } = pick(def, entry, original); + if (value === undefined) { + out[def.key] = { def, value: undefined, fromOriginal: false }; + continue; + } + const issues = valueIssues(def, value, `fields/${def.key}`); + const first = issues[0]; + if (!first) { + out[def.key] = { def, value, fromOriginal }; + continue; + } + const converting = def.migration?.fromType; + out[def.key] = { + def, + value: undefined, + fromOriginal, + issue: { + path: first.path, + code: "field_value_unreadable", + message: converting + ? `still a ${converting} value, being converted to ${def.type}` + : first.message, + }, + }; + } + return out; +} + +/** "Has a value" for `requiredToPublish`: not absent, not an empty string, not an empty list. */ +export function hasFieldValue(value: unknown): boolean { + if (value === undefined || value === null) return false; + if (typeof value === "string") return value.trim() !== ""; + if (Array.isArray(value)) return value.length > 0; + return true; +} diff --git a/packages/core-ts/src/fields/types.ts b/packages/core-ts/src/fields/types.ts new file mode 100644 index 00000000..e19e15ba --- /dev/null +++ b/packages/core-ts/src/fields/types.ts @@ -0,0 +1,180 @@ +/** + * 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"; + +export type FieldType = FieldDefinition["type"]; +export type SelectFieldDefinition = Extract; +/** What field code reads of a site: its collection settings. */ +export interface SiteFields { + collections?: Site["collections"] | undefined; +} + +/** The nine closed types, in the contract's order. */ +export const FIELD_TYPES: readonly FieldType[] = [ + "text", + "richText", + "number", + "boolean", + "date", + "datetime", + "url", + "image", + "select", +]; + +/** RFC-0041 §2, §4 and Security: every limit in one place. */ +export const FIELD_LIMITS = { + /** Per collection (also in the schema's `maxItems`). */ + fieldsPerCollection: 30, + /** Per site, across collections. */ + fieldsPerSite: 200, + /** `JSON.stringify(site.collections)` in UTF-8 bytes. */ + collectionsBytes: 256 * 1024, + /** `JSON.stringify(entry.fields)` in UTF-8 bytes. */ + valuesBytes: 256 * 1024, + /** A text field's `maxLength` when absent, and its ceiling. */ + textLength: 5000, + richTextLength: 50_000, + urlLength: 2048, + selectOptions: 50, +} as const; + +/** + * RFC-0041 §1: keys a field may not take. Every `ContentEntry` property (themes merge fields into + * an entry view, so `title` would shadow the real one), the view's own names, and JavaScript + * prototype names. `__proto__` cannot match the key pattern. + */ +export const RESERVED_FIELD_KEYS: ReadonlySet = new Set([ + "id", + "siteId", + "collection", + "slug", + "locale", + "status", + "title", + "summary", + "body", + "fields", + "tags", + "terms", + "author", + "coverImage", + "publishedAt", + "availability", + "rev", + "createdAt", + "updatedAt", + "translation", + "metadata", + "corrections", + "url", + "path", + "href", + "entry", + "site", + "constructor", + "prototype", + "toString", + "valueOf", + "hasOwnProperty", +]); + +export type FieldIssueCode = + /** A value does not fit its definition. */ + | "invalid_value" + /** A new key without a definition in a collection that has definitions. */ + | "unknown_field" + /** A translation holds its own copy of a non-localized value. */ + | "field_not_localized" + /** An entry in review or published lacks a `requiredToPublish` value. */ + | "required_field" + /** `entry.fields` or `site.collections` over its byte cap. */ + | "too_large" + /** Definitions: a reserved key. */ + | "field_key_reserved" + /** Definitions: a key used twice in one collection. */ + | "field_key_duplicate" + /** Definitions: a select option value used twice. */ + | "field_option_duplicate" + /** Definitions: `minimum` above `maximum`. */ + | "field_bounds" + /** Definitions: more fields than a collection or a site may have. */ + | "too_many_fields" + /** 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"; + +/** One problem, at a slash path (`fields/startsAt/at`, `collections/events/fields/2/key`). */ +export interface FieldIssue { + path: string; + code: FieldIssueCode; + message: string; +} + +/** A collection's definitions (empty when it has none). */ +export function fieldDefinitions(site: SiteFields, collection: string): readonly FieldDefinition[] { + return site.collections?.[collection]?.fields ?? []; +} + +export const isRecord = (v: unknown): v is Record => + typeof v === "object" && v !== null && !Array.isArray(v); + +/** Structural equality of JSON values (key order ignored). */ +export function sameJson(a: unknown, b: unknown): boolean { + if (a === b) return true; + if (Array.isArray(a) || Array.isArray(b)) { + if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false; + return a.every((x, i) => sameJson(x, b[i])); + } + if (!isRecord(a) || !isRecord(b)) return false; + const keys = Object.keys(a); + if (keys.length !== Object.keys(b).length) return false; + return keys.every((k) => Object.hasOwn(b, k) && sameJson(a[k], b[k])); +} + +const encoder = new TextEncoder(); +/** UTF-8 bytes of a value's compact JSON. */ +export const jsonBytes = (value: unknown): number => + encoder.encode(JSON.stringify(value) ?? "").length; + +/** A label in `locale`, then `defaultLocale`, then any, else the key. */ +export function fieldLabel(def: FieldDefinition, locale: string, defaultLocale: string): string { + const labels = def.labels; + return ( + labels[locale]?.label ?? + labels[defaultLocale]?.label ?? + Object.values(labels).find((l) => l !== undefined)?.label ?? + def.key + ); +} + +/** A field's help text in `locale`, then `defaultLocale` (no fallback to other locales). */ +export function fieldHelp( + def: FieldDefinition, + locale: string, + defaultLocale: string, +): string | undefined { + return def.labels[locale]?.help ?? def.labels[defaultLocale]?.help; +} + +/** An option's words in `locale`, then `defaultLocale`, then any, else its value. */ +export function optionLabel( + def: FieldDefinition, + value: string, + locale: string, + defaultLocale: string, +): string { + if (def.type !== "select") return value; + const option = def.options.find((o) => o.value === value); + if (!option) return value; + const labels = option.labels; + return ( + labels[locale] ?? + labels[defaultLocale] ?? + Object.values(labels).find((l) => l !== undefined) ?? + value + ); +} diff --git a/packages/core-ts/src/fields/value-checks.ts b/packages/core-ts/src/fields/value-checks.ts new file mode 100644 index 00000000..9b69d016 --- /dev/null +++ b/packages/core-ts/src/fields/value-checks.ts @@ -0,0 +1,147 @@ +/** + * RFC-0041 §2: one value against one definition. `validateProps` (ADR-0030) does the type, length, + * bound, enum and pattern checks from the compiled prop schema; the per-type table adds what the + * subset cannot say. Eval-free and Temporal-free (the site Worker reads values too). + */ +import type { FieldDefinition } from "@cloudhal/core-contracts"; +import { type BlockRegistry, validateProps } from "../blocks.ts"; +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"; + +export interface ValueCheckOptions { + /** Directives and surface fences in rich text are checked against it (absent: shape only). */ + blocks?: BlockRegistry | undefined; +} + +type Extra = ( + def: FieldDefinition, + value: unknown, + path: string, + options: ValueCheckOptions, +) => FieldIssue[]; + +const issue = (path: string, message: string): FieldIssue => ({ + path, + code: "invalid_value", + message, +}); + +/** True when `YYYY-MM-DD` names a day of the proleptic Gregorian calendar. */ +export function isCalendarDay(value: string): boolean { + const m = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(value); + if (!m) return false; + const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])]; + const date = new Date(0); + date.setUTCFullYear(y, mo - 1, d); + return date.getUTCFullYear() === y && date.getUTCMonth() === mo - 1 && date.getUTCDate() === d; +} + +const zones = new Map(); +let supported: Set | undefined; + +/** + * True when the runtime's `Intl` knows the zone: in `Intl.supportedValuesOf("timeZone")`, or an + * alias it accepts (`UTC`, `Europe/Kiev`), as `isTimeZone` does for site zones (RFC-0008). + */ +export function isKnownTimeZone(name: string): boolean { + const hit = zones.get(name); + if (hit !== undefined) return hit; + supported ??= new Set(Intl.supportedValuesOf("timeZone")); + let ok = supported.has(name); + if (!ok) + try { + new Intl.DateTimeFormat("en-US", { timeZone: name }); + ok = true; + } catch { + ok = false; + } + zones.set(name, ok); + return ok; +} + +function markdownIssues(markdown: string, path: string, blocks: BlockRegistry): FieldIssue[] { + const out: FieldIssue[] = []; + if (markdown.includes("surface")) + for (const i of checkBodySurfaces(markdown, createCatalog({ blocks }))) + out.push(issue(path, `surface ${i.path}: ${i.message}`)); + if (markdown.includes("::")) + for (const d of bodyDirectives(markdown)) { + const found = checkDirective(blocks, d) ?? []; + if (found.length) + out.push( + issue( + path, + `${d.name}: ${found.map((i) => `${i.path || "props"} ${i.message}`).join("; ")}`, + ), + ); + } + return out; +} + +/** What each type adds to the compiled schema's checks (run only when those passed). */ +const EXTRA: Partial> = { + text(def, value, path) { + const multiline = def.type === "text" && def.multiline === true; + const s = value as string; + for (let i = 0; i < s.length; i++) { + const c = s.charCodeAt(i); + if (c < 0x20 && !(c === 0x0a && multiline)) + return [ + issue( + path, + multiline ? "no control characters but line breaks" : "one line, no control characters", + ), + ]; + } + return []; + }, + richText(_def, value, path, options) { + const s = value as string; + if (s.length > FIELD_LIMITS.richTextLength) + return [issue(path, `at most ${FIELD_LIMITS.richTextLength} characters`)]; + return options.blocks ? markdownIssues(s, path, options.blocks) : []; + }, + date(_def, value, path) { + return isCalendarDay(value as string) ? [] : [issue(path, "a real calendar day (YYYY-MM-DD)")]; + }, + datetime(_def, value, path) { + const v = value as { at: string; timeZone: string }; + const out: FieldIssue[] = []; + if (!isCalendarDay(v.at.slice(0, 10)) || !Number.isFinite(Date.parse(v.at))) + out.push(issue(`${path}/at`, "a real instant (RFC 3339 in UTC)")); + if (!isKnownTimeZone(v.timeZone)) out.push(issue(`${path}/timeZone`, "an IANA time zone")); + return out; + }, + url(_def, value, path) { + return /^\s*asset:/i.test(value as string) + ? [issue(path, "a link (https, http, mailto, tel, /path or #fragment), not an asset")] + : []; + }, + select(def, value, path) { + if (def.type !== "select" || !def.multiple) return []; + const list = value as string[]; + return new Set(list).size === list.length ? [] : [issue(path, "each option at most once")]; + }, +}; + +/** Problems with one value (`path` is where it sits, e.g. `fields/startsAt`). */ +export function valueIssues( + def: FieldDefinition, + value: unknown, + path: string, + options: ValueCheckOptions = {}, +): 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 found = validateProps( + { type: "object", properties: { v: fieldPropSchema(def) }, additionalProperties: false }, + { v: typed }, + ); + if (found.length) return found.map((i) => issue(`${path}${i.path.slice(1)}`, i.message)); + return EXTRA[def.type]?.(def, value, path, options) ?? []; +} diff --git a/packages/core-ts/src/fields/values.ts b/packages/core-ts/src/fields/values.ts new file mode 100644 index 00000000..17b18d8c --- /dev/null +++ b/packages/core-ts/src/fields/values.ts @@ -0,0 +1,115 @@ +/** + * RFC-0041 §3: the value checker the store, the API and MCP's precheck share, and the + * `requiredToPublish` check for review and publish. + * + * Only values that changed are checked: an old value that a later, narrower definition rejects + * does not block unrelated edits (Contentful re-checks on re-publish, not retroactively). + */ +import type { FieldDefinition, PublishStatus } from "@cloudhal/core-contracts"; +import { type BlockRegistry, createBlockRegistry } from "../blocks.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"; + +export interface CheckInput { + /** The entry's `fields` as they would be stored after the write. */ + values: Readonly>; + /** The stored `fields` before the write; absent for a new entry (every key counts as changed). */ + previous?: Readonly> | undefined; + /** + * When the entry is a translation (same site): its original's id and `fields`. A + * non-localized value of its own is refused unless equal to the original's. + */ + original?: { id: string; values: Readonly> } | undefined; + /** Rich text directives and surfaces are checked against it (absent: the built-in blocks). */ + blocks?: BlockRegistry | undefined; +} + +let builtIns: BlockRegistry | undefined; + +/** Keys whose value differs from `previous` (all keys without it). */ +function changedKeys(input: CheckInput): string[] { + const { values, previous } = input; + return Object.keys(values).filter( + (k) => !(previous && Object.hasOwn(previous, k) && sameJson(previous[k], values[k])), + ); +} + +/** + * Problems with an entry's field values (empty when the write may go ahead). + * + * - The 256 KB cap on `JSON.stringify(values)` applies in every collection. + * - With no definitions, `fields` stays "any JSON". + * - With definitions, a changed key without one is refused (`unknown_field`); stored unknown keys + * that do not change are kept, and removing a key is always allowed. + */ +export function checkFieldValues( + defs: readonly FieldDefinition[], + input: CheckInput, +): FieldIssue[] { + const out: FieldIssue[] = []; + const bytes = jsonBytes(input.values); + if (bytes > FIELD_LIMITS.valuesBytes) + out.push({ + path: "fields", + code: "too_large", + message: `field values are ${Math.ceil(bytes / 1024)} KB; at most ${FIELD_LIMITS.valuesBytes / 1024} KB`, + }); + if (defs.length === 0) return out; + const byKey = new Map(defs.map((d) => [d.key, d])); + builtIns ??= createBlockRegistry(); + const blocks = input.blocks ?? builtIns; + for (const key of changedKeys(input)) { + const def = byKey.get(key); + const path = `fields/${key}`; + const value = input.values[key]; + if (!def) { + out.push({ + path, + code: "unknown_field", + message: `${key} is not a field of this collection; its fields are ${[...byKey.keys()].join(", ")}`, + }); + continue; + } + const { original } = input; + if (original && !def.localized && !sameJson(value, original.values[key])) { + out.push({ + path, + code: "field_not_localized", + message: `${key} is not localized: set it on ${original.id}`, + }); + continue; + } + out.push(...valueIssues(def, value, path, { blocks })); + } + return out; +} + +export type PublishCheckEntry = FieldsEntry & { status: PublishStatus }; + +const NEEDS_VALUES = new Set(["review", "published"]); + +/** + * RFC-0041 §3: an entry in review or published needs a resolved value for every + * `requiredToPublish` field (a translation reads non-localized ones from its original). Drafts and + * archived entries pass. A stored value that no longer fits its definition counts as missing. + */ +export function checkRequiredToPublish( + site: SiteFields, + entry: PublishCheckEntry, + original?: FieldsOriginal, +): FieldIssue[] { + if (!NEEDS_VALUES.has(entry.status)) return []; + const out: FieldIssue[] = []; + for (const [key, field] of Object.entries(resolveFields(site, entry, original))) { + const def = field.def; + if (def.type === "boolean" || def.requiredToPublish !== true) continue; + if (!hasFieldValue(field.value)) + out.push({ + path: `fields/${key}`, + code: "required_field", + message: `${key} needs a value before the entry goes to review or is published`, + }); + } + return out; +} diff --git a/packages/core-ts/test/fields-definitions.test.ts b/packages/core-ts/test/fields-definitions.test.ts new file mode 100644 index 00000000..307f0f4e --- /dev/null +++ b/packages/core-ts/test/fields-definitions.test.ts @@ -0,0 +1,237 @@ +/** RFC-0041 §1, §4, §6: definition rules beyond the schema, the caps and the plain-edit guard. */ +import { readdir, readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { type FieldDefinition, isValid, type Site } from "@cloudhal/core-contracts"; +import { describe, expect, it } from "vitest"; +import { + checkFieldDefinitions, + FIELD_LIMITS, + FIELD_TYPES, + fieldDefinitionChanges, + fieldDefinitions, + RESERVED_FIELD_KEYS, +} from "../src/fields/index.ts"; +import { EVENT_FIELDS, site } from "./fields-fixture.ts"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const FIXTURES = path.resolve(here, "../../core-contracts/fixtures/valid/site"); +const text = (key: string, extra: Partial = {}) => + ({ + key, + type: "text", + labels: { en: { label: key } }, + localized: true, + ...extra, + }) as FieldDefinition; +const codes = (issues: { code: string }[]) => issues.map((i) => i.code); +const withFields = (fields: FieldDefinition[]) => site(fields); + +describe("checkFieldDefinitions", () => { + it("accepts the shared fixture and every valid contract fixture", async () => { + expect(checkFieldDefinitions(site())).toEqual([]); + 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([]); + } + }); + + it("refuses every reserved key: entry properties, view names, prototype names", () => { + for (const key of [ + "title", + "coverImage", + "corrections", + "url", + "href", + "site", + "constructor", + "toString", + "hasOwnProperty", + ]) { + const issues = checkFieldDefinitions(withFields([text(key)])); + expect(issues, key).toEqual([ + expect.objectContaining({ + path: "collections/events/fields/0/key", + code: "field_key_reserved", + }), + ]); + } + expect(RESERVED_FIELD_KEYS.size).toBe(32); + }); + + it("reserves every ContentEntry property of the contract", async () => { + const schema = JSON.parse( + await readFile( + path.resolve(here, "../../core-contracts/schemas/content-entry.schema.json"), + "utf8", + ), + ) as { properties: Record }; + const camel = Object.keys(schema.properties).filter((k) => /^[a-z][A-Za-z0-9]*$/.test(k)); + for (const key of camel) + if (key !== "schemaVersion") expect(RESERVED_FIELD_KEYS.has(key), key).toBe(true); + }); + + it("refuses duplicate keys and option values", () => { + expect(codes(checkFieldDefinitions(withFields([text("venue"), text("venue")])))).toEqual([ + "field_key_duplicate", + ]); + const select: FieldDefinition = { + key: "format", + type: "select", + labels: { en: { label: "Format" } }, + localized: false, + options: [ + { value: "talk", labels: { en: "Talk" } }, + { value: "talk", labels: { en: "Talk again" } }, + ], + }; + expect(checkFieldDefinitions(withFields([select]))).toEqual([ + expect.objectContaining({ + path: "collections/events/fields/0/options/1/value", + code: "field_option_duplicate", + }), + ]); + }); + + it("refuses minimum above maximum, allows equal bounds", () => { + const num = (minimum: number, maximum: number): FieldDefinition => ({ + key: "price", + type: "number", + labels: { en: { label: "Price" } }, + localized: false, + minimum, + maximum, + }); + expect(codes(checkFieldDefinitions(withFields([num(5.5, 1.5)])))).toEqual(["field_bounds"]); + expect(checkFieldDefinitions(withFields([num(2.5, 2.5)]))).toEqual([]); + }); + + it("caps fields per collection and per site", () => { + const many = (n: number, prefix: string) => + Array.from({ length: n }, (_, i) => text(`${prefix}${i}`)); + expect(codes(checkFieldDefinitions(withFields(many(31, "f"))))).toEqual(["too_many_fields"]); + const collections = Object.fromEntries( + Array.from({ length: 7 }, (_, c) => [`c${c}`, { fields: many(29, "f") }]), + ) as Site["collections"]; + const issues = checkFieldDefinitions({ collections }); + expect(issues).toEqual([ + expect.objectContaining({ path: "collections", code: "too_many_fields" }), + ]); + expect(issues[0]?.message).toContain("203"); + const fits = Object.fromEntries( + Array.from({ length: 4 }, (_, c) => [`c${c}`, { fields: many(30, "f") }]), + ) as Site["collections"]; + expect(checkFieldDefinitions({ collections: fits })).toEqual([]); + }); + + it("caps the collections setting at 256 KB", () => { + const labels = Object.fromEntries( + Array.from({ length: 30 }, (_, i) => [ + `l${i}`, + { label: "x".repeat(100), help: "y".repeat(300) }, + ]), + ); + const heavy = (key: string) => ({ ...text(key), labels }) as FieldDefinition; + const collections = Object.fromEntries( + Array.from({ length: 7 }, (_, c) => [ + `c${c}`, + { fields: Array.from({ length: 28 }, (_, i) => heavy(`f${i}`)) }, + ]), + ) as Site["collections"]; + expect(codes(checkFieldDefinitions({ collections }))).toEqual(["too_large"]); + expect(FIELD_LIMITS.collectionsBytes).toBe(262_144); + }); + + it("reads a collection's definitions", () => { + expect(fieldDefinitions(site(), "events")).toBe(EVENT_FIELDS); + expect(fieldDefinitions(site(), "posts")).toEqual([]); + expect(fieldDefinitions({}, "events")).toEqual([]); + expect(FIELD_TYPES).toHaveLength(9); + }); +}); + +describe("the plain-edit guard (§6)", () => { + const before = withFields([ + text("venue"), + { + key: "format", + type: "select", + labels: { en: { label: "Format" } }, + localized: false, + options: [ + { value: "talk", labels: { en: "Talk" } }, + { value: "workshop", labels: { en: "Workshop" } }, + ], + }, + ]); + const fields = (b: typeof before) => b.collections?.events?.fields ?? []; + + it("lets free edits through: add, reorder, labels, options added, toggles, bounds", () => { + const [venue, format] = fields(before) as [ + FieldDefinition, + FieldDefinition & { type: "select" }, + ]; + const after = withFields([ + { + ...format, + labels: { en: { label: "Kind" } }, + options: [...format.options, { value: "panel", labels: { en: "Panel" } }], + }, + { + ...venue, + maxLength: 10, + multiline: true, + searchable: true, + requiredToPublish: true, + } as FieldDefinition, + text("room"), + ]); + expect(checkFieldDefinitions(after, before)).toEqual([]); + expect(fieldDefinitionChanges(before, after)).toEqual([]); + }); + + it("refuses type, localization, multiple and migration-marker changes", () => { + const [venue, format] = fields(before) as [ + FieldDefinition, + FieldDefinition & { type: "select" }, + ]; + const after = withFields([ + { ...venue, type: "richText", localized: false } as FieldDefinition, + { ...format, multiple: true, migration: { id: "snap_01HZX8K3Q9MG" } }, + ]); + const issues = checkFieldDefinitions(after, before); + expect(codes(issues)).toEqual(Array(4).fill("field_change_needs_operation")); + expect(issues.map((i) => i.message).join(" ")).toMatch( + /change_field_type.*set_field_localized/, + ); + expect(fieldDefinitionChanges(before, after).map((c) => c.change)).toEqual([ + "type", + "localized", + "migration", + "multiple", + ]); + }); + + it("lists removals and removed options for the store to count, never as a rename", () => { + const [, format] = fields(before) as [FieldDefinition, FieldDefinition & { type: "select" }]; + const after = withFields([text("place"), { ...format, options: [format.options[0] as never] }]); + expect(checkFieldDefinitions(after, before)).toEqual([]); + expect(fieldDefinitionChanges(before, after)).toEqual([ + { collection: "events", key: "venue", change: "removed" }, + { collection: "events", key: "format", change: "optionsRemoved", options: ["workshop"] }, + ]); + expect(fieldDefinitionChanges(before, { collections: {} })).toEqual([ + { collection: "events", key: "venue", change: "removed" }, + { collection: "events", key: "format", change: "removed" }, + ]); + }); + + it("treats a marker on a new field as an operation's", () => { + const after = withFields([ + ...fields(before), + text("speaker", { migration: { id: "snap_01HZX8K3Q9MG", fromKey: "venue" } }), + ]); + expect(codes(checkFieldDefinitions(after, before))).toEqual(["field_change_needs_operation"]); + }); +}); diff --git a/packages/core-ts/test/fields-fixture.ts b/packages/core-ts/test/fields-fixture.ts new file mode 100644 index 00000000..e25c4292 --- /dev/null +++ b/packages/core-ts/test/fields-fixture.ts @@ -0,0 +1,86 @@ +/** Shared definitions for the RFC-0041 field tests: one field of every type and option. */ +import type { FieldDefinition, Site } from "@cloudhal/core-contracts"; + +const L = (label: string, help?: string) => ({ en: help ? { label, help } : { label } }); + +export const EVENT_FIELDS: [FieldDefinition, ...FieldDefinition[]] = [ + { + key: "venue", + type: "text", + labels: { en: { label: "Venue", help: "Where it happens." }, tr: { label: "Mekan" } }, + localized: true, + requiredToPublish: true, + maxLength: 20, + searchable: true, + }, + { key: "notes", type: "text", labels: L("Notes"), localized: false, multiline: true }, + { key: "agenda", type: "richText", labels: L("Agenda"), localized: true }, + { + key: "price", + type: "number", + labels: L("Price", "In euros."), + localized: false, + minimum: 0.5, + maximum: 999.5, + }, + { key: "seats", type: "number", labels: L("Seats"), localized: false, integer: true }, + { key: "online", type: "boolean", labels: L("Online"), localized: false }, + { key: "deadline", type: "date", labels: L("Deadline"), localized: false }, + { + key: "startsAt", + type: "datetime", + labels: L("Starts"), + localized: false, + requiredToPublish: true, + }, + { key: "ticketUrl", type: "url", labels: L("Tickets"), localized: true }, + { key: "poster", type: "image", labels: L("Poster"), localized: false }, + { + key: "format", + type: "select", + labels: L("Format"), + localized: false, + multiple: true, + options: [ + { value: "talk", labels: { en: "Talk", tr: "Konuşma" } }, + { value: "workshop", labels: { en: "Workshop" } }, + ], + }, + { + key: "level", + type: "select", + labels: { tr: { label: "Seviye" } }, + localized: false, + requiredToPublish: true, + options: [ + { value: "beginner", labels: { tr: "Başlangıç" } }, + { value: "expert", labels: { tr: "Uzman", en: "Expert" } }, + ], + }, +]; + +export const site = ( + fields: readonly FieldDefinition[] = EVENT_FIELDS, +): Pick => ({ + defaultLocale: "en", + collections: { + posts: {}, + events: { fields: fields as [FieldDefinition, ...FieldDefinition[]] }, + }, +}); + +/** Values that pass every check of EVENT_FIELDS. */ +export const GOOD_VALUES: Record = { + venue: "Kadıköy Hall", + notes: "Line one\nLine two", + agenda: "# Agenda\n\n- Talks\n- **Lunch**", + price: 12.5, + seats: 120, + online: false, + deadline: "2026-02-28", + startsAt: { at: "2026-11-05T16:30:00Z", timeZone: "Europe/Istanbul" }, + ticketUrl: "https://example.com/tickets", + poster: "asset_01HZX8K3Q9PO", + format: ["talk", "workshop"], + level: "beginner", +}; diff --git a/packages/core-ts/test/fields-resolve.test.ts b/packages/core-ts/test/fields-resolve.test.ts new file mode 100644 index 00000000..62f849f1 --- /dev/null +++ b/packages/core-ts/test/fields-resolve.test.ts @@ -0,0 +1,258 @@ +/** RFC-0041 §3, §5: resolved values, `requiredToPublish`, and the JSON Schema agents receive. */ +import type { ContentEntry, FieldDefinition } from "@cloudhal/core-contracts"; +import { describe, expect, it } from "vitest"; +import { + checkRequiredToPublish, + fieldsJsonSchema, + JSON_SCHEMA_KEYWORDS, + type JsonSchema, + resolveFields, +} from "../src/fields/index.ts"; +import { EVENT_FIELDS, GOOD_VALUES, site } from "./fields-fixture.ts"; + +const ORIGINAL_ID = "entry_01HZX8K3Q9OR"; +const original = { id: ORIGINAL_ID, fields: { ...GOOD_VALUES } }; +type Entry = Pick & { + translation?: ContentEntry["translation"]; +}; +const entry = (fields: Record, extra: Partial = {}): Entry => ({ + id: "entry_01HZX8K3Q9TR", + collection: "events", + status: "draft", + fields, + ...extra, +}); +const translation = (fields: Record, extra: Partial = {}) => + entry(fields, { + translation: { + group: ORIGINAL_ID, + method: "machine", + source: { id: ORIGINAL_ID, locale: "en", rev: 3, hash: "h".repeat(64) }, + }, + ...extra, + }); +const values = (r: ReturnType) => + Object.fromEntries(Object.entries(r).map(([k, f]) => [k, f.value])); + +describe("resolveFields", () => { + it("returns every defined field in order; {} for a collection without fields", () => { + const r = resolveFields(site(), entry(GOOD_VALUES)); + expect(Object.keys(r)).toEqual(EVENT_FIELDS.map((d) => d.key)); + expect(values(r)).toEqual(GOOD_VALUES); + expect(Object.values(r).every((f) => !f.fromOriginal && !f.issue)).toBe(true); + expect(resolveFields(site(), { ...entry({ x: 1 }), collection: "posts" })).toEqual({}); + expect(resolveFields(site(), entry({})).venue).toEqual({ + def: EVENT_FIELDS[0], + value: undefined, + fromOriginal: false, + }); + }); + + it("a translation: own localized values, original's when missing, non-localized from the original", () => { + const r = resolveFields(site(), translation({ venue: "Salon", price: 99 }), original); + expect(r.venue).toMatchObject({ value: "Salon", fromOriginal: false }); + expect(r.agenda).toMatchObject({ value: GOOD_VALUES.agenda, fromOriginal: true }); + // a stale own copy of a non-localized value is ignored + expect(r.price).toMatchObject({ value: 12.5, fromOriginal: true }); + expect(r.startsAt).toMatchObject({ value: GOOD_VALUES.startsAt, fromOriginal: true }); + }); + + it("missing everywhere stays undefined, not fromOriginal", () => { + const r = resolveFields(site(), translation({}), { id: ORIGINAL_ID, fields: {} }); + expect(r.ticketUrl).toEqual(expect.objectContaining({ value: undefined, fromOriginal: false })); + }); + + it("the original reads its own values; a wrong or missing original is not used", () => { + const self = entry( + { price: 1 }, + { id: ORIGINAL_ID, translation: { group: ORIGINAL_ID, method: "original" } }, + ); + expect(resolveFields(site(), self, original).price?.value).toBe(1); + expect( + resolveFields(site(), translation({ price: 2 }), { + id: "entry_01HZX8K3Q9XX", + fields: { price: 3 }, + }).price?.value, + ).toBe(2); + expect(resolveFields(site(), translation({ price: 2 })).price?.value).toBe(2); + }); + + it("a language edition (original in another site) owns its values", () => { + const edition = translation({ price: 7 }); + if (edition.translation?.source) edition.translation.source.siteId = "site_01HZX8K3Q9OT"; + const r = resolveFields(site(), edition, original); + expect(r.price).toMatchObject({ value: 7, fromOriginal: false }); + expect(r.agenda).toMatchObject({ value: undefined, fromOriginal: false }); + }); + + it("a value that no longer fits resolves to undefined with an issue, never a throw", () => { + const r = resolveFields( + site(), + entry({ venue: "x".repeat(50), deadline: "2026-02-30", level: 3 }), + ); + expect(r.venue).toMatchObject({ + value: undefined, + issue: { code: "field_value_unreadable", path: "fields/venue" }, + }); + expect(r.deadline?.issue?.message).toContain("calendar day"); + expect(r.level?.value).toBeUndefined(); + }); + + it("reads through a migration marker: fromKey fallback, and unconverted values flagged", () => { + const renamed: FieldDefinition = { + key: "speaker", + type: "text", + labels: { en: { label: "Speaker" } }, + localized: true, + migration: { id: "snap_01HZX8K3Q9MG", fromKey: "presenter" }, + }; + const s = site([renamed]); + expect(resolveFields(s, entry({ presenter: "Ada" })).speaker?.value).toBe("Ada"); + expect(resolveFields(s, entry({ presenter: "Ada", speaker: "Grace" })).speaker?.value).toBe( + "Grace", + ); + const retyped: FieldDefinition = { + key: "seats", + type: "number", + labels: { en: { label: "Seats" } }, + localized: false, + migration: { id: "snap_01HZX8K3Q9MG", fromType: "text" }, + }; + const r = resolveFields(site([retyped]), entry({ seats: "120" })); + expect(r.seats?.value).toBeUndefined(); + expect(r.seats?.issue?.message).toContain("still a text value"); + expect(resolveFields(site([retyped]), entry({ seats: 120 })).seats?.value).toBe(120); + }); +}); + +describe("checkRequiredToPublish", () => { + it("drafts and archived entries pass", () => { + expect(checkRequiredToPublish(site(), entry({}))).toEqual([]); + expect(checkRequiredToPublish(site(), entry({}, { status: "archived" }))).toEqual([]); + }); + + it("review and published need every required field; empty strings and lists count as missing", () => { + const issues = checkRequiredToPublish(site(), entry({ venue: " " }, { status: "review" })); + expect(issues.map((i) => [i.path, i.code])).toEqual([ + ["fields/venue", "required_field"], + ["fields/startsAt", "required_field"], + ["fields/level", "required_field"], + ]); + expect(checkRequiredToPublish(site(), entry(GOOD_VALUES, { status: "published" }))).toEqual([]); + }); + + it("a value that no longer fits counts as missing", () => { + const fields = { ...GOOD_VALUES, level: "gone" }; + expect( + checkRequiredToPublish(site(), entry(fields, { status: "published" })).map((i) => i.path), + ).toEqual(["fields/level"]); + }); + + it("a translation reads non-localized required values from its original", () => { + const t = translation({ venue: "Salon" }, { status: "published" }); + expect(checkRequiredToPublish(site(), t, original)).toEqual([]); + // localized required values fall back to the original's too + expect(checkRequiredToPublish(site(), translation({}, { status: "review" }), original)).toEqual( + [], + ); + expect(checkRequiredToPublish(site(), t).map((i) => i.path)).toEqual([ + "fields/startsAt", + "fields/level", + ]); + }); + + it("an empty multiple select is missing", () => { + const multi: FieldDefinition = { + key: "format", + type: "select", + labels: { en: { label: "Format" } }, + localized: false, + multiple: true, + requiredToPublish: true, + options: [{ value: "talk", labels: { en: "Talk" } }], + }; + expect( + checkRequiredToPublish(site([multi]), entry({ format: [] }, { status: "review" })), + ).toHaveLength(1); + expect( + checkRequiredToPublish(site([multi]), entry({ format: ["talk"] }, { status: "review" })), + ).toEqual([]); + }); +}); + +/** Every keyword in the tree, with where it sits. */ +function keywords(schema: JsonSchema, at = ""): [string, string][] { + const out: [string, string][] = Object.keys(schema).map((k) => [at, k]); + for (const [k, child] of Object.entries(schema.properties ?? {})) + out.push(...keywords(child, `${at}/${k}`)); + if (schema.items) out.push(...keywords(schema.items, `${at}/items`)); + return out; +} + +describe("fieldsJsonSchema (strict structured output)", () => { + const schema = fieldsJsonSchema(EVENT_FIELDS, "tr", "en"); + + it("matches the golden file", async () => { + await expect(`${JSON.stringify(schema, null, 2)}\n`).toMatchFileSnapshot( + "golden/fields-json-schema.snap", + ); + }); + + it("uses only keywords both OpenAI and Anthropic strict modes accept", () => { + const used = new Set(keywords(schema).map(([, k]) => k)); + for (const k of used) expect(JSON_SCHEMA_KEYWORDS).toContain(k); + for (const banned of [ + "minimum", + "maximum", + "minLength", + "maxLength", + "minItems", + "maxItems", + "uniqueItems", + "default", + "$ref", + "oneOf", + "anyOf", + "const", + ]) + expect(used.has(banned), banned).toBe(false); + }); + + it("closes every object, requires nothing at the top, and only date formats", () => { + const walk = (s: JsonSchema, top: boolean): void => { + if (s.type === "object") { + expect(s.additionalProperties).toBe(false); + if (top) expect(s.required).toBeUndefined(); + else expect(s.required).toEqual(Object.keys(s.properties ?? {})); + } + if (s.format) expect(["date", "date-time", "uri"]).toContain(s.format); + for (const c of Object.values(s.properties ?? {})) walk(c, false); + if (s.items) walk(s.items, false); + }; + walk(schema, true); + // the top object's children are fields; nested objects are datetime values only + expect(schema.required).toBeUndefined(); + }); + + it("writes limits, labels, help and localization into words", () => { + const p = schema.properties ?? {}; + expect(p.venue?.title).toBe("Mekan"); + expect(p.venue?.description).toContain("at most 20 characters"); + expect(p.venue?.description).toContain("Needed to publish."); + expect(p.price?.title).toBe("Price"); + expect(p.price?.description).toMatch( + /In euros\..*At least 0\.5\. At most 999\.5\..*original only/, + ); + expect(p.seats?.type).toBe("integer"); + expect(p.level?.title).toBe("Seviye"); + expect(p.level?.description).toContain("beginner (Başlangıç), expert (Uzman)"); + expect(p.format).toMatchObject({ + type: "array", + items: { type: "string", enum: ["talk", "workshop"] }, + }); + expect(p.poster?.pattern).toBe("^asset_[A-Za-z0-9]{6,32}$"); + expect(p.deadline?.format).toBe("date"); + expect(p.startsAt?.properties?.at?.format).toBe("date-time"); + expect(fieldsJsonSchema([], "en")).toMatchObject({ type: "object", properties: {} }); + }); +}); diff --git a/packages/core-ts/test/fields-values.test.ts b/packages/core-ts/test/fields-values.test.ts new file mode 100644 index 00000000..748d9e05 --- /dev/null +++ b/packages/core-ts/test/fields-values.test.ts @@ -0,0 +1,237 @@ +/** RFC-0041 §2–§3: the value checker, type by type, and its changed-only rules. */ +import type { FieldDefinition } from "@cloudhal/core-contracts"; +import { describe, expect, it } from "vitest"; +import { checkPropsSchema, createBlockRegistry, type PropsSchema } from "../src/blocks.ts"; +import { + checkFieldValues, + FIELD_LIMITS, + fieldsPropsSchema, + isCalendarDay, + isKnownTimeZone, + valueIssues, +} from "../src/fields/index.ts"; +import { EVENT_FIELDS, GOOD_VALUES } from "./fields-fixture.ts"; + +const def = (key: string) => EVENT_FIELDS.find((d) => d.key === key) as FieldDefinition; +const check = (key: string, value: unknown) => + valueIssues(def(key), value, `fields/${key}`, { blocks: createBlockRegistry() }); +const codes = (issues: { code: string }[]) => issues.map((i) => i.code); + +describe("compiled props schema (ADR-0030 subset)", () => { + it("is inside the subset, every property optional", () => { + const schema = fieldsPropsSchema(EVENT_FIELDS); + expect(checkPropsSchema(schema)).toEqual([]); + expect(schema.required).toBeUndefined(); + expect(Object.keys(schema.properties)).toEqual(EVENT_FIELDS.map((d) => d.key)); + }); + + it("maps each type to the subset and keeps labels as title/description", () => { + const p = fieldsPropsSchema(EVENT_FIELDS).properties; + expect(p.venue).toMatchObject({ + type: "string", + maxLength: 20, + title: { en: "Venue", tr: "Mekan" }, + description: { en: "Where it happens." }, + }); + expect(p.notes).toMatchObject({ type: "string", maxLength: 5000, "x-widget": "textarea" }); + expect(p.agenda).toMatchObject({ type: "string", format: "markdown" }); + expect(p.price).toMatchObject({ type: "number", minimum: 0.5, maximum: 999.5 }); + expect(p.seats).toMatchObject({ type: "integer" }); + expect(p.online).toMatchObject({ type: "boolean" }); + expect(p.startsAt).toMatchObject({ type: "object", required: ["at", "timeZone"] }); + expect(p.ticketUrl).toMatchObject({ type: "string", format: "url", maxLength: 2048 }); + expect(p.format).toMatchObject({ + type: "array", + maxItems: 2, + items: { enum: ["talk", "workshop"] }, + }); + // labels without `en` become a plain string (the subset's Localized needs `en`) + expect(p.level).toMatchObject({ title: "Seviye", enum: ["beginner", "expert"] }); + expect((p.level as { "x-labels": unknown })["x-labels"]).toEqual({ + beginner: "Başlangıç", + expert: { tr: "Uzman", en: "Expert" }, + }); + }); + + it("compiles once per definitions array", () => { + expect(fieldsPropsSchema(EVENT_FIELDS)).toBe(fieldsPropsSchema(EVENT_FIELDS)); + const copy = [...EVENT_FIELDS]; + expect(fieldsPropsSchema(copy)).not.toBe(fieldsPropsSchema(EVENT_FIELDS)); + expect(checkPropsSchema(fieldsPropsSchema([]) as PropsSchema)).toEqual([]); + }); +}); + +describe("values by type", () => { + it("accepts every good value", () => { + for (const [key, value] of Object.entries(GOOD_VALUES)) + expect(check(key, value), key).toEqual([]); + }); + + it.each([ + ["venue", 42, "text"], + ["venue", "x".repeat(21), "at most 20 characters"], + ["venue", "two\nlines", "one line"], + ["venue", "tab\there", "one line"], + ["notes", "bell\u0007", "line breaks"], + ["notes", "x".repeat(5001), "at most 5000"], + ["agenda", 1, "text"], + ["agenda", "x".repeat(50_001), "at most 50000"], + ["agenda", '::button{label="Go"}', "button"], + ["price", "12", "a number"], + ["price", 0.25, "at least 0.5"], + ["price", 1000, "at most 999.5"], + ["price", Number.NaN, "a number"], + ["seats", 1.5, "a whole number"], + ["online", "yes", "true or false"], + ["deadline", "2026-02-30", "real calendar day"], + ["deadline", "2026-2-3", "expected form"], + ["deadline", "2026-02-28T00:00:00Z", "expected form"], + ["startsAt", "2026-11-05T16:30:00Z", "an object"], + ["startsAt", { at: "2026-11-05T19:30:00+03:00", timeZone: "Europe/Istanbul" }, "expected form"], + ["startsAt", { at: "2026-11-05T16:30Z", timeZone: "Europe/Istanbul" }, "expected form"], + ["startsAt", { at: "2026-02-30T16:30:00Z", timeZone: "Europe/Istanbul" }, "real instant"], + ["startsAt", { at: "2026-11-05T16:30:00Z", timeZone: "Mars/Olympus_Mons" }, "IANA"], + ["startsAt", { at: "2026-11-05T16:30:00Z" }, "required"], + ["startsAt", { at: "2026-11-05T16:30:00Z", timeZone: "UTC", offset: 3 }, "not a prop"], + ["ticketUrl", "javascript:alert(1)", "safe link"], + ["ticketUrl", "data:text/html,x", "safe link"], + ["ticketUrl", "//evil.example", "safe link"], + ["ticketUrl", "asset:asset_01HZX8K3Q9PO", "not an asset"], + ["ticketUrl", `https://example.com/${"x".repeat(2048)}`, "at most 2048"], + ["poster", "asset:asset_01HZX8K3Q9PO", "expected form"], + ["poster", "https://example.com/a.png", "expected form"], + ["format", "talk", "a list"], + ["format", ["talk", "workshop", "talk"], "at most 2 items"], + ["format", ["talk", "talk"], "at most once"], + ["format", ["talk", "panel"], "one of talk, workshop"], + ["level", "panel", "one of beginner, expert"], + ["level", ["beginner"], "text"], + ["venue", null, "null is never stored"], + ])("%s refuses %j (%s)", (key, value, words) => { + const issues = check(key, value); + expect(issues.length, JSON.stringify(issues)).toBeGreaterThan(0); + expect(codes(issues).every((c) => c === "invalid_value")).toBe(true); + expect(issues.map((i) => i.message).join(" | ")).toContain(words); + expect(issues[0]?.path.startsWith(`fields/${key}`)).toBe(true); + }); + + it("finds repeats in a multiple select of more options than repeats", () => { + const three: FieldDefinition = { + key: "tags2", + type: "select", + labels: { en: { label: "T" } }, + localized: false, + multiple: true, + options: ["a", "b", "c"].map((value) => ({ value, labels: { en: value } })) as [ + { value: string; labels: { en: string } }, + ], + }; + expect(valueIssues(three, ["a", "b", "a"], "fields/tags2")[0]?.message).toContain( + "at most once", + ); + expect(valueIssues(three, [], "fields/tags2")).toEqual([]); + }); + + it("allows line breaks only in multiline text, and markdown directives that check out", () => { + expect(check("notes", "a\nb")).toEqual([]); + expect(check("agenda", ':::callout{tone="warning"}\nCareful.\n:::')).toEqual([]); + expect(check("agenda", '::button{label="Go" href="/go"}')).toEqual([]); + // Rich text over the props subset's 10 000-character string ceiling is still fine. + expect(check("agenda", "x".repeat(20_000))).toEqual([]); + }); + + it("puts datetime issues at the inner path", () => { + expect( + check("startsAt", { at: "2026-11-05T16:30:00Z", timeZone: "Nowhere/Land" })[0]?.path, + ).toBe("fields/startsAt/timeZone"); + }); + + it("knows calendar days and zones without Temporal", () => { + expect(isCalendarDay("2024-02-29")).toBe(true); + expect(isCalendarDay("2026-02-29")).toBe(false); + expect(isCalendarDay("0000-01-01")).toBe(true); + expect(isKnownTimeZone("Europe/Istanbul")).toBe(true); + expect(isKnownTimeZone("UTC")).toBe(true); + expect(isKnownTimeZone("Europe/Kiev")).toBe(true); + expect(isKnownTimeZone("Etc/GMT+3")).toBe(true); + expect(isKnownTimeZone("Nowhere/Land")).toBe(false); + }); +}); + +describe("checkFieldValues", () => { + it("passes good values, and anything in a collection without definitions", () => { + expect(checkFieldValues(EVENT_FIELDS, { values: GOOD_VALUES })).toEqual([]); + expect(checkFieldValues([], { values: { anything: { nested: [1, null] } } })).toEqual([]); + }); + + it("refuses new unknown keys, naming the defined ones", () => { + const issues = checkFieldValues(EVENT_FIELDS, { values: { venue: "Hall", colour: "red" } }); + expect(issues).toEqual([ + expect.objectContaining({ path: "fields/colour", code: "unknown_field" }), + ]); + expect(issues[0]?.message).toContain("venue, notes, agenda"); + }); + + it("keeps unknown keys already stored, refuses changing them, allows removing them", () => { + const previous = { venue: "Hall", legacy: "kept" }; + expect(checkFieldValues(EVENT_FIELDS, { values: previous, previous })).toEqual([]); + expect( + codes(checkFieldValues(EVENT_FIELDS, { values: { ...previous, legacy: "new" }, previous })), + ).toEqual(["unknown_field"]); + expect(checkFieldValues(EVENT_FIELDS, { values: { venue: "Hall" }, previous })).toEqual([]); + }); + + it("checks only values that changed", () => { + // a value a later, narrower definition rejects does not block unrelated edits + const previous = { venue: "x".repeat(30), price: 5 }; + expect(checkFieldValues(EVENT_FIELDS, { values: { ...previous, price: 6 }, previous })).toEqual( + [], + ); + // equal by structure (key order) counts as unchanged + const at = { at: "2026-11-05T16:30:00Z", timeZone: "Mars/Base" }; + expect( + checkFieldValues(EVENT_FIELDS, { + values: { startsAt: { timeZone: at.timeZone, at: at.at } }, + previous: { startsAt: at }, + }), + ).toEqual([]); + // but a change is checked + expect( + codes( + checkFieldValues(EVENT_FIELDS, { + values: { ...previous, venue: "y".repeat(31) }, + previous, + }), + ), + ).toEqual(["invalid_value"]); + // and without previous every key is new + expect(codes(checkFieldValues(EVENT_FIELDS, { values: previous }))).toEqual(["invalid_value"]); + }); + + it("refuses a translation's own copy of a non-localized value unless equal", () => { + const original = { id: "entry_01HZX8K3Q9OR", values: { price: 10, venue: "Hall" } }; + const own = checkFieldValues(EVENT_FIELDS, { values: { price: 11 }, original }); + expect(own).toEqual([ + expect.objectContaining({ path: "fields/price", code: "field_not_localized" }), + ]); + expect(own[0]?.message).toContain("entry_01HZX8K3Q9OR"); + expect(checkFieldValues(EVENT_FIELDS, { values: { price: 10 }, original })).toEqual([]); + // localized fields are the translation's own + expect(checkFieldValues(EVENT_FIELDS, { values: { venue: "Salon" }, original })).toEqual([]); + }); + + it("caps the values at 256 KB in UTF-8, with or without definitions", () => { + const big = { blob: "ü".repeat(FIELD_LIMITS.valuesBytes / 2) }; + expect(codes(checkFieldValues([], { values: big }))).toEqual(["too_large"]); + const fits = { blob: "u".repeat(FIELD_LIMITS.valuesBytes - 20) }; + expect(checkFieldValues([], { values: fits })).toEqual([]); + }); + + it("checks rich text directives against the given registry (built-ins by default)", () => { + const values = { agenda: '::button{label="Go"}' }; + expect(codes(checkFieldValues(EVENT_FIELDS, { values }))).toEqual(["invalid_value"]); + expect(checkFieldValues(EVENT_FIELDS, { values: { agenda: "::unknown-thing{a=1}" } })).toEqual( + [], + ); + }); +}); diff --git a/packages/core-ts/test/golden/fields-json-schema.snap b/packages/core-ts/test/golden/fields-json-schema.snap new file mode 100644 index 00000000..c56a38b8 --- /dev/null +++ b/packages/core-ts/test/golden/fields-json-schema.snap @@ -0,0 +1,96 @@ +{ + "type": "object", + "description": "Typed custom fields of this collection. Every field is optional; leave a key out to leave it unset.", + "properties": { + "venue": { + "type": "string", + "title": "Mekan", + "description": "Where it happens. Plain text, at most 20 characters. One line. Needed to publish." + }, + "notes": { + "type": "string", + "title": "Notes", + "description": "Plain text, at most 5000 characters. Line breaks allowed. Set on the original only; translations read it from there." + }, + "agenda": { + "type": "string", + "title": "Agenda", + "description": "Markdown (Cloudhal Markdown v1), at most 50000 characters." + }, + "price": { + "type": "number", + "title": "Price", + "description": "In euros. A number. At least 0.5. At most 999.5. Set on the original only; translations read it from there." + }, + "seats": { + "type": "integer", + "title": "Seats", + "description": "A whole number. Set on the original only; translations read it from there." + }, + "online": { + "type": "boolean", + "title": "Online", + "description": "Absent reads as false. Set on the original only; translations read it from there." + }, + "deadline": { + "type": "string", + "format": "date", + "title": "Deadline", + "description": "A calendar day, YYYY-MM-DD, without a time zone. Set on the original only; translations read it from there." + }, + "startsAt": { + "type": "object", + "properties": { + "at": { + "type": "string", + "format": "date-time", + "description": "The instant in UTC, RFC 3339 ending in Z, e.g. 2026-11-05T16:30:00Z." + }, + "timeZone": { + "type": "string", + "description": "The IANA time zone it happens in, e.g. Europe/Istanbul." + } + }, + "required": [ + "at", + "timeZone" + ], + "additionalProperties": false, + "title": "Starts", + "description": "A date and time with its time zone. Set on the original only; translations read it from there. Needed to publish." + }, + "ticketUrl": { + "type": "string", + "title": "Tickets", + "description": "A link: https:, http:, mailto: or tel:, a site path (/about) or a #fragment; at most 2048 characters." + }, + "poster": { + "type": "string", + "pattern": "^asset_[A-Za-z0-9]{6,32}$", + "title": "Poster", + "description": "The id of an image asset of this site (asset_…). Set on the original only; translations read it from there." + }, + "format": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "talk", + "workshop" + ] + }, + "title": "Format", + "description": "Any of: talk (Konuşma), workshop (Workshop); each at most once. Set on the original only; translations read it from there." + }, + "level": { + "type": "string", + "enum": [ + "beginner", + "expert" + ], + "title": "Seviye", + "description": "One of: beginner (Başlangıç), expert (Uzman). Set on the original only; translations read it from there. Needed to publish." + } + }, + "additionalProperties": false +} diff --git a/packages/core-ts/test/worker-time-free.test.ts b/packages/core-ts/test/worker-time-free.test.ts index 33ed125c..ce1f671e 100644 --- a/packages/core-ts/test/worker-time-free.test.ts +++ b/packages/core-ts/test/worker-time-free.test.ts @@ -78,10 +78,11 @@ const usesTemporal = (external: Set) => [...external].filter((s) => s === "temporal-polyfill" || s.startsWith("temporal-polyfill/")); describe("the site Worker never bundles the time-zone library (§9.4)", () => { - it("core-ts's index, its availability entry and the schedules engine reach no temporal-polyfill", async () => { + it("core-ts's index, its availability and fields entries and the schedules engine reach no temporal-polyfill", async () => { for (const entry of [ path.join(PACKAGES, "core-ts/src/index.ts"), path.join(PACKAGES, "core-ts/src/availability.ts"), + path.join(PACKAGES, "core-ts/src/fields/index.ts"), path.join(PACKAGES, "schedules/src/engine.ts"), path.join(PACKAGES, "astro-renderer/src/cloudflare-entry.ts"), path.join(PACKAGES, "astro-renderer/src/runtime-workerd.ts"),